Design Philosopy
It’s a developer wet dream to be able to build a highly decoupled system that allow new features to just slot in flawlessly without disturbing existing one. However, that’s rather difficult to achieve, even if somewhow we’re able to, it will certainly come with a cost like any other.
Ok then what’s the approach we should take to tackle this delicate matter?
okay first we’ll have to list our priorities
- Rapid feature release
- Ease of tweaking
- Maintainability of said features
My answer is to combine the speed of mobile warfare and the organization of grand battle plan, is that even possible?
Yes, I call it the Nimble Forsythia.
In short this war doctrine design philosopy start with anticipating apparent problems and ignoring scary but vague problems and then goes into full speed while doing a little cleaning along the way by the end of a major patch we’ll take a brake and see if there’s new apparent problem and then adress it if there’s any.
There’s three main component to this philosopy
- Compound Battle Plan
- Relentless Execution
- Litter pick-up
Compound Battle Plan
In traditional SDLC the planning will be done once at the start of the project, However we’ll do it after every major patch.
Contrary to other software project we don’t have the luxury of knowing everything at the very beginning, so to cope with that we’ll do smaller design phase multiple times.
This short planning phase will result in faster features being done but if we’re not careful the whole thing can crumble, that’s where the Litter Pick-up and the compound part come into play.
After the end of every major patch, the code will start to tangle into spaghet but it won’t be too spaghet, Litter Pick-up will clean the smaller spaghet and the planning phase at the start of the next cycle will fix glaring architectural problems.
Relentless Execution
After the planning phase is completed we’re going to switch into full flank speed and try to follow the set guidelines but don’t show any hesitation don’t think of problems that might come in the future.
But isn’t this dangerous? Yes, but our first priority is to ship the features first so it’s fine and also there’s two percautions set in place to prevent too much damage which are:
- Unit testing
- Litter Pick-up
Unit testing discourage us from coupling system too much, because well the test won’t run if the scene can’t work by itself.
And Litter Pick-up untangle mess before it reach a critical point.
Litter Pick-up
To put it simply Litter-pickup is a refactoring process that’s being done after every feature release which focus on cleaning the small but numerous mess we’ve created during the creation of said feature.
Info
This refactoring process focus on cleaning little spaghet tomfoolery instead of the bigger architectural problems.
Code Guidelines
Forced Static Typing
Unfortunately GDScript Static Typing isn’t mandatory even when the optional Static Typing is turned on it’s still not forced which is not aligned with our M mindset can lead into some weird unwanted behavior.
Our only option is to force it ourself, but sometimes as a human we forgor and that’s ogey just add the type on the Litter Pick-up phase.
Naming Convention
class = PascalCase
node = PascalCase
scene = PascalCase
variable = snake_case
function = snake_case
file = snake_case
Access Modifier
GDScript doesn’t really have an Access Modifier so this is just a convention to mark stuff therefore this will not effect the code in any way.
To mark a variable as private prefix it with an underscore.
leave public and protected variable as is.
Method doesn’t have an access modifier, a method with underscore prefix indicate that it’s somekind of callback function and not a private metod.
If an object can access another object method then it’s either safe to do so or there’s something wrong with the architecture, as a rule of thumb if the method you’re trying to access is part of the local scene tree then it’s fine to access it but if it’s not please proceed with caution.
Getter Setter
Only create getter setter when there’s an extra stuff taking place if the getter setter serve no purpose it’s better to not have them in the first place.
prefix the member variable with an underscore
getter: get_variable_name
setter: set_variable_name
Redundant:
func set_is_in_battle(new_state: bool) -> void:
is_in_battle = new_state
Reasonable:
func set_is_in_battle(new_state: bool) -> void:
if new_state == true:
battle_timer.start()
is_in_battle = new_state
emit_signal("battle_state_changed")
Exporting Variable
We want our designer workflow to be as frictionless as possible and providing them with the ability to tweak stuff inside without touching the code is a great value and this can be done with expoting the variables.
To export variable we need to prefix our variable with the export keyword followed by the export option variable name and then the type.
Example: export(String, MULTILINE) var skill_description: String
Pattern and Architecture
Base Paradigm
The official godot documentation favor traditional OOP over any other paradigm mainly ECS and we’re going to follow that, but it’s not restricting as in godot also offer a high level composition with scene but it’ll be primarily composed with Inheritance
Call Down Signal Up
As a rule of thumb Parent should manage it’s child, and the child shouldn’t call the parent directly.
If the child need the parent to do something it should use a signal.
Example:
// Do
// Call Down
get_node("child").do_something()
// Signal Up
signal child_hurt
func damaged():
emit_signal("child_hurt")
func _on_child_hurt():
// do_something()
// Don't
// Call up
get_node("../..").parent_do_something()
Inheritance over Composition
As mentioned above godot favor Inheritance over Composition for example A player class would look like this
instead of
Character
Control
Sprite
...
But since godot support combining scens togther it will usually looks like this
KinematicBody2D -> Entity -> Character
-> Sprite
-> Area2D -> Hitbox
-> Area2d -> Hurtbox
-> Node2D -> Weapon
In this specific project, we should encourage shallow Inheritance but wide.
Context Based Coupling
Objects should be coupled when they’re related enough.
Example:
A player will have a sprite and this instance of sprite is specifically exist for the player itself so any other object will not cares about its existence is the character base class therefore it’s safe to tightly couple them.
If it’s not related enough, do not couple them
Example:
An npc have the ability to prompt dialog, so at a first glance it is reasonable to put the dialog node inside the scene tree of that NPC but dialog can also can be called other than NPC so it’s a bad idea to couple them together, if this is the case please refer to Global Signal.
Signal and Global Signal
This is an extennsion of the regular Signal pattern the regular signal pattern usually used when a child needs to notify its parent that something happened, if that’s the case a regular signal will do wonders but when other Node needs to know something happened but doesn’t have a direct connection to the Node that’s when we should use Global Signal.
We will use a forbidden technique here:
Singleton
Calm dowm, this is probably one of the safest singleton that we can use because it doesn’t create any tight coupling
It will only provide an easy access to connect a signal.
Example:
HUD <-> Global Signal <-> Character
Character will send a signal that something happened but doesn’t care who received that signal so even when the signal sender is gone there will be no crash since there’s no direct donnection
So when to use Global Signal?
We want to use signal if two or more object cares about it’s significance
Example:
When enemy hitbox enters player’s hurtbox the player base class needs to know that it happened so it can reduce the player’s health and at the same time the UI also needs to know that to update the Health Bar.
When to not use Global Signal?
If the receiver has a more convinient way to connect to the signal then it’s best to not use a Global Signal
So there will be a singleton named SignalManager that’ll act as a bridge between nodes, and any node can send and receive signal to and from this singleton.
Assumption is Evil
Do not assume that something will exist, that’s a quick and easy way to create a debugging hell.
For example:
The enemy needs player position at all times for it to works, this is an assumption it assumes that the player will exist no matter what but in reality this is hard to achieve, the enemy ai can exist before the player or the player may die and dissapear from meory in that case the entire game will break.
Since GDScript is not enforcing any kind of safety regarding this we have to improvise and restrain ourself from using unsafe reference.
Unsafe refrence usually is a fruit of a quick solution that our head conjure and more often than not the first solution that came into our head isn’t the greatest, so just keep in mind that when we create an unsafe reference there might be a better way of doing it.
Finite State Machine
We’ll be using FSM to manage controlled entity, because it’s simple enough to impelement but more importantly controlled entity is predictable this prevent the FSM to blow up in lines, and we’re not going to use it to control uncontrolled entity.
For the current implementation of the State Machine please read the code under common/state_machine.gd
it heavily use polymorphism to generalize things.
Behaviour Tree
This is a solution to define the behaviour of an uncontrolled entity, Altho this is way more boiler plate up front and is harder to understand but it ultimately better for complex but predictable behavior.
If we’re using FSM for contorlling enemy behavior it can work for simpler enemy but at a certain threshold even adding just one state will require a lot of modification to existing code and that’s less than desirable with Behaviour Tree adding new behavior is more cost effective, and somewhere down the road we’ll a boss enemy which will definitely have a lot behavior and that’s going to be realy messy.
We’re not gonna implement our own Behaviour Tree instead we’re going to use a Plugin for it.
Singleton
I know, I know, I’ve been saying that I hate this one pattern but it’s somewhat unavoidable there’s no escaping it, we’re just going to have to use it better.
As a rule of them if the singleton can exist in another game without a lot of modification then it’s generic enough to be a singleton after all most of the problem with signleton is the tight coupling and it it’s not tight and just provide an easy acess to something then yeah that’s nice.