refactoring characters: black box
february 18, 2021
#character #refactor
The character script was one of blessfrey's first scripts. Since it's never seen a serious refactor, it has retained its clueless beginner style to this day. Every single line of code related to characters was buried somewhere in this unmanageable monolith. The time has finally come for refactoring.
The two biggest problems with my character script were the lack of structure + encapsulation.
adding structure
An entire game can fit in one mega script, but an object-oriented approach is more manageable. It's good to keep your code close to where it is implemented.
First, I expanded the character's node hierarchy. I combed through the code, forming abstract categories for each section. These categories became my subnodes. (Nodes are objects in Godot, by the way.) If a subnode was too complex, it was broken into another level of subnodes. Then, I cut+pasted as much code as I could into these subscripts. My tree now looks like this, when before, it just had the AI, Body, and UI nodes:
adding encapsulation
Within the monolith, every part of the character had direct access to every other part. A major step towards getting everything running again was adding encapsulation, or grouping related logic + data into objects then restricting direct access of its internal components from other objects. I did this through designating entrypoints as the only way of performing actions or accessing data. These entrypoints are the character's verbs and setters + getters.
verbs
Verbs are what the object does. Some of them are obvious, like "use skill," "be damaged," and "pick up item," but some of them are more abstract, like "calculate level."
The character script should act as a central hub, executing verbs by contacting subnodes for the actual logic and passing back the output. These subnodes should act as black boxes, so the character script only worries about input + output, not how the request is performed.
Before, I didn't apply this concept to the character at all. Outside objects would travel all over the tree to pick out specific methods. That resulted in code like body.get_parent().UI.skillbar.healthbar.label.set_value("100")
instead of something like character.set_health(100)
. As I modified systems, all the outside references made it difficult to remove old versions. Since everything didn't necessarily use the entrypoint, there was a lot of redundant code + unexpected behavior.
setters + getters
Another closely related concept is setters + getters. If you tack a setget
followed by method names onto a variable, those methods will be called whenever you access that variable. The first method is the setter and is conventionally prefixed with "set_", while the second is the getter. If you don't want a setter, don't write anything before the comma: setget , get_whatever
. If you don't want a getter, don't even add the comma: setget set_whatever
.
Setters + getters are related because they increase consistency. If something needs to happen every time a variable is changed, the setter is a good entrypoint. If a variable needs to be obtained in a specific way, that process can be taken care of inside a get_thing()
. This way, your variable-specific code is encapsulated, and you are no longer encouraged to manipulate them outside of their little box.
Unsurprisingly, using verbs, black boxes, and setters + getters fixed a lot of long-running bugs.
controlling flow
Another problem popped up concerning when the tree's nodes initialize. Now that everything isn't in the same script, everything isn't ready at the same time. To see the initialization order, I made a small project. Each node prints its name when at ready, and, as you see, it works from lowest level to highest level, top node to bottom node.
To have more control over the flow of my character, dependency-sensitive nodes rely more on a setup method than the _ready method. Also, characters who inherit from the base script use a super _setup method called from the setup method for their own specialized needs. (Super methods are discussed in tidying up my skill phases.) This way, I can ensure a node is ready and has all its properties set at the moment it's needed.
happy script
The refactor paid off immediately. So many "known issues" were immediately resolved. The character scripts are way easier to maintain now. The main script's halved in size. And I feel like I've improved a lot since I first started working on blessfrey. I wanted to go through the rest of the program and apply these concepts throughout, but it turned out I was already using them. It was just this ancient mess of a script that was left behind.
I'll finish with a before + after of the base character script.

Enjoy your day!
last updated 2/21/21