Godot Architecture: 7 steps for more flexible, extensible, testable code (video)

Hello!

I made a video about how I like to structure my projects, including a refactor of some of my older code.

This is probably not a good use of time on a very small project, but larger and collaborative projects tend to benefit from more structure. Most of these techniques are about increasing readability and decreasing coupling, and culminate in fairly unit-testable code (if anyone wants that?). Anyways, all of these are tools you can take or leave, but I do explain some of the benefits!

Here’s what I go through:

- (briefly) static typing

- removing autoloads and moving to a simple version of dependency injection

- restructuring our ā€œmain sceneā€ to provide hierarchical contexts that manage their own non-global services

- blowin’ up the signal hub and localizing signals

- colocating code and other assets into feature-group folders with maximum cohesion and minimal outside dependencies

- setting up unit testing with GDUnit

- probably some other stuff too, I don’t remember, it’s been a long day

There’s lots more I’d like to do but these are, I think, useful for a lot of projects.

Anyone else have any essential architecture best practices?

2 Likes

Really good intro.

Around 12:20

You recommend replacing hardcoded $NodePaths. That was good. You then said you can use @export variables. That was less good. $NodePaths should be replaced with @onready variables. For example, in your code, you changed this to an @export variable.

var start_button = $layout_margin/layout_options/start
start_button.pressed.connect(on_press_start)

This is a common thing I see in people with Unreal/Unity experience. The drawback to @export var methods are it clutters up the Inspector, and more importantly if you forget to link an object the failures are a lot more cryptic. I have found that even when creating a generic node reference, it causes less headaches to use an @onready variable and search for what you need in the tree rather than rely on yourself to remember to link the variable.

You can fix this with much less typing.

  1. Click and drag the start button in the Scene Tree over the editor window.
  2. Press the Ctrl button. (It has to be pressed after clicking and before dropping.)
  3. Drop the button into your code.
  4. This line will appear:
var start: Button =  $layout_margin/layout_options/start

It will be statically typed, and have a variable you can use immediately. It will be named the same as your Node is named. So, you can either call it Start Button, or rename the variable name it created to match your code. This brings up another thing. Usually node names in the editor are not snake_case. They’re either PascalCase or they’re more often just human readable with every word capitalized. (Hence my suggested name above.)

The other thing I noticed is the convention of calling functions that are only referenced in the class is to prepend the function name with an underscore to indicate they are ā€œprivateā€. I put that in quotes, because as I’m sure you know GDScript doesn’t really have access modifiers. Still, it’s convention to make the code clearer. (If you address these later in the video, I just haven’t gotten there yet.)

Around 13:20

You mention that people should follow ā€œsomeā€ convention. I’d recommend you not prevaricate on this point. Almost every day we get people posting questions, and they’re following some weird convention that makes their code much harder to understand. Marching to the beat of your own drummer is fine, as long as you never have to ask for help. But it can become a real problem, and people get very offended and frustrated when they’re told their code is confusing and/or hard to read.

Around 14:00

You talk about state machines. I’m a big fan. Something you might look into for your own enjoyment is Resource-based or Node-based state machines. I have adopted a pull-based (as opposed to push-based) Node state machine. You can look at it and play with it using my State Machine Plugin if you’re interested. I use it for game state, as well as player states.

I also think this State Chart Plugin is pretty nifty. And while I don’t like it for myself in practical application, I think it’s a neat design pattern and I had fun playing with it. I chose a pull machine based on Kanban project management and the idea that states control when they are entered. This allows me to keep the code completely atomic and add/remove states even during runtime without having tightly coupled code.

Around 15:15

You talk about contexts here. I use a state machine instead. Same idea, different name. (Unless you feel there are differences, in which case I’d be interested to hear them.) For me this was a journey of trial and error. I originally had four state machines that all did the same thing: Node, Control, Node2D and Node3D. The icons were color-coded and they could have spatial knowledge as part of the machine.

After implementing this, I realized that I did not need to hang the HitBox off the AttackState, and in fact from an encapsulation point of view, I had to view States as Components of the larger object the StateMachine was operating on. Basically they needed to be treated as if the _subject (object was taken) of the StateMachine and it’s Nodes had protected access modifiers that ā€œallowedā€ the States to see and manipulate them.

Once I got to that point, I started seeing how they could all be nodes, and as I got used to using the StateMachine I realized there was never any need to extend it - only the States.

Around 18:17

I get where you’re coming from about Autoloads I think they get overused and used incorrectly a lot. I’m typically not a fan of a SignalBus style Autoload. Having said that, as I have modularized my code, I’ve ended up with a bunch of little Autoloads.

Each one handles atomic things that I want handled at a high level. They all have as few functions in them as possible, and they are specifically not load dependent. This list is generated from my Game Template Plugin, which brings a number of my commonly used plugins under one roof with a basic UI and scaffolding for 2D and 3D games. It also has the Localization Plugin, which simply provides a dynamically created language option button based on the languages you localize in the game.


(From my game Katamari Mech Spacey)

That node tree also has my great shame, the Camera2DSignalBus which comes from the Camera2D Plugin. That plugin provides a number of components that can be added to a Camera2D to extend its functionality.

About 19:25

I do agree that changing things in Autoloads is can be a big problem. Which is why I typically only use them in plugins, and those are all under version control and use semantic versioning. Even that Game autoload is from a plugin, and it carries a very few things that multiple contexts need to have access to that I haven’t yet figured how to pass in other ways. It’s also my central clearinghouse for when inheritance when I need a place to put some messy variables or signals for prototyping in a larger game until I can factor them out. It’s a convenient way for me to know all my trash is in one script.

I found your discussion of dependency injection very interesting, but I wish it was longer, because even though I’ve used it in other contexts (web development and automated testing), I couldn’t decide what I thought on the pros/cons of using it here, and the implementation details.

I also saw your use of a variable name gsh. I find the abbreviation of variables in modern languages obfuscation for obfuscation’s sake. You know what it is, but I had to look for a reference to what it meant, and it seemed unnecessary to shorten that name. Especially in the context of teaching - though I get that this was old code and we end up with short hand that often means things to us as developers. I’ve just found no reason not to have descriptive variables names as we are no longer in the 80s.

About 20:30

I saw a code smell, which is a bunch of classes named ā€œControllerā€. This seems like the Manager Anti-Pattern with a different name. I watched a talk years ago on C++ naming conventions, and the speaker talked about how our brains do things to our objects when we call them ā€œManagerā€. And by changing the name to what they actually do, we tend to delegate a lot of the responsibility they are holding onto to the objects that really should be doing them. If we take a look at my Autoloads again:

You’ll see that in essence they could all be named Manager or Controller. Funnily enough Controller is in fact the synonym I landed on for Input because that name is already taken by Godot. Gamepad Take Keyboard for example. This is the entire script:

@icon("res://addons/dragonforge_controller/assets/textures/icons/keyboard.svg")
## Keyboard Autoload
extends Node

const icon_path = "res://addons/dragonforge_controller/assets/key_icons/"


## Returns the Texture2D representation of the keyboard key event passed
func get_key_icon(event: InputEventKey) -> Texture2D:
	var keyname = event.as_text().trim_prefix("Kp ").trim_suffix(" (Physical)").trim_suffix(" - Physical").to_lower()
	var filename = icon_path + "keyboard_" + keyname + "_outline.png"
	return load(filename)

All it does is integrate in with Controller and when called with an InputEventKey, pull up a graphical representation in the form of a Texture2D. It’s only use is for the GUI to display the correct key to press.

If I called it KeyboardManager or KeyboardController, I would be tempted to give it more responsibility. Maybe all keyboard input should go through it - or special keyboard input. I decided not.

Likewise, if we look at the Mouse script (sans some comments):

@icon("res://addons/dragonforge_controller/assets/textures/icons/mouse.svg")
## Mouse Autoload
extends Node

## Look sensitivity modifier for 3D camera controls
@export var sensitivity: float = 0.0075:
	set(value):
		sensitivity = value
		if get_tree().root.has_node("Disk"):
			var disk: Variant = get_tree().root.get_node("Disk")
			disk.save_setting(sensitivity, "mouse_sensitivity")
@export var mouse_button_images: Array[Texture2D]


func _ready() -> void:
	if get_tree().root.has_node("Disk"):
		var disk: Variant = get_tree().root.get_node("Disk")
		var returned_value = disk.load_setting("mouse_sensitivity")
		if returned_value:
			sensitivity = returned_value


func _input(event: InputEvent) -> void:
	if Controller.enable_3d_look and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED and event is InputEventMouseMotion:
		Controller.look = -event.relative * sensitivity


## Returns the Texture2D representation of the mouse button event passed.
## Returns null if the InputEvent is not a MouseButtonEvent.
func get_mouse_icon(action: InputEvent) -> Texture2D:
	if action is InputEventMouseButton:
		return mouse_button_images[action.button_index]
	return

It stores images for mouse buttons and will return them just like Keyboard. It also handles variables for mouse look in 3D. It also decouples itself from the Disk Plugin. If the Autoload exists, it uses it to save and load the sensitivity settings for the mouse. If not, it doesn’t. It also can track looking for using keyboard and mouse for 1st and 3rd person camera control. The Gamepad Autoload does the same thing for the gamepage, and the controller takes that information and puts it together so that both inputs always work and you can tell which one is being used.

But the Mouse isn’t a Manager or Controller. It only deals with things that need to be centralized, and lets everything else be handled by the scripts that need to handle mouse input.

About 21:13

I’d question the naming convention of Holder as well, but not seeing the code it’s hard to say. If it’s basically a synonym of List that could make sense, I’m just used to the FSM context of State Machine as the holder of game states.

About 22:00

Really good stuff. Call down, signal up. (Ha! You said it!)

About 20:26

Good stuff.

I put all assets in an assets folder, and then group scripts and scenes together under features. I have found that I often need to re-use assets in more than one context more often, and I sometimes need to do bulk import changes or something else. So in general, I have found it most useful to keep assets together. It also helps on game #10 when quick load can’t find a texture for example, and I want to find it, I always know that I am browsing to res://assets/textures/ and then looking for the folder name that makes sense. Over time my brain has decided on what works for me.

About 25:05

Really good point about the reduction of merge conflicts. It’s also really helpful when working with an artist or sound designer. All their stuff is in one place and they don’t need to muck about in your scenes and code. And while a lot of people seeing your video won’t be working on regular development teams, they may at some point be working on game jam teams, and this structure will really help if they want to pull in some assets designers in.

About 25:25

Preach!

I love that you’re talking about this.

I started using GDUnit a couple years ago. I created this lesson on implementing GDUnit intended for people who know nothing.

However, in practice I have found, as you touched on, that my games change so quickly that unit tests are not as useful for me as I hoped they would be. I also have made a fair number of game jam games, and the frenetic pace of game jams does not lend it self to unit testing.

Closing Thoughts

You had a lot of good things to say overall. Very good production quality. Engaging and interesting video.

4 Likes

Like you said, everyone has their own idea about coding and I have mine :slight_smile: I will guess this is for small one or two person teams. After all we are a gaming community.

What I tell people about this topic is the best laid architecture is the first thing that falls apart once you step into the mud. Architecture is good for overall top level designs. Beyond that is analysis followed by paralysis.

The best way to future proof workable code is to be consistent with your coding style and document. At the top of each file and function have a summary what this code does. Self documenting variables/function names is good but can only go so far. Create meaningful naming and folder structures. Error checks with meaningful messages. Error checks not only validate the functionality but can serve as documentation for what your code is supposed to do. Unit testing is good but only goes so far. That’s a topic in itself.

I’ve gone back to messy code many times and as long as it’s documented I and anyone else can work with it. Of course these days you can use AI to generate requirements, code and document. But that’s another story.

First of all… what a gift! Thank you SO much for taking the time to provide me with your detailed thoughts. Seriously.

In general a lot of things are… me being a bit sloppy! I couldn’t cover everything, but also I don’t know everything yet. I kinda just wanted to give some people a high level overview of some techniques I like to use.

In particular:

1) $ syntax: guilty as charged! I switched from Unity a couple years ago. I still don’t like $ syntax, because I find that I’m often tinkering with scene structure, moving stuff between layout nodes etc. I think if anyone likes this, it’s fine. I’m going to have to check out onready, i haven’t used it much.

2) Codestyle conventions: Yeah, but I think that it’s ok to deviate somewhat from codestyle within your organization if you’re being consistent, clear and intentional. I’ve worked at a few big companies in my life and they all have slightly different (but usually readable) conventions. The GDScript style guide is great though and I wouldn’t deviate unless I’ve got a good reason.

3) State machines: Yeah, it’s all trial and error. A lot of error. I’ll check out those state machine resources.

4) I think I come down too hard on autoloads; a lot of people are asking me if they are forbidden, which is not really what I mean. Rather, I think they are usually unnecessary and often problematic. There ARE a few things that are totally appropriate to keep up at that level. I can’t remember how well they play with GDUnit though? I assume there’s a mocking function for them. Love the example.

5) I agree about descriptive variables. I only crush em down into a dumb abbreviation if 1) it’s something that’s used pretty much in the next and line and has an obvious purpose (e.g. i/j/x/y in a loop over a range, or in this case I’m just passing in the only game-state holder service as an argument). Also you might adopt a common abbreviation for a type of thing across your game/org as a general shorthand. Counterpoint: the company I work for at my day job has so many damn abbreviations that it’s impossible to remember what anything is, so… yeah. Readability!

6) Manager classes.. You’ve found my great shame, and this is something I’m going to work on, and I’ll spread the word.

7) GameStateHolder: Eh, the holder is just the thing that holds the GameState for access elsewhere. GameStateProvider? I don’t know, it just holds it. I wanted a service that can hold the GameState and hold a different one if you start a new game without having to update refs everywhere.

One thing I’m curious if you or anyone else has thoughts on is this:

When I was mucking around with building Entity Controller Systems, I got really into the idea of treating game data as simple data classes with no functionality, and having outside services act on them; obviously, I’m not doing an ECS here (no components, and there’s likely no performance gain to do this in GDScript) but I did rather like this as it abstracts dependencies away from the game data classes themselves. I’ve heard it called ā€œan anemic domain modelā€ but I sort of like to think of the ā€œcontrollersā€ I have as the main interfaces of the objects themselves, and are able to orchestrate behaviour between domains. As with lots of OO principles, it works either way and the ā€œdownsidesā€ are a bit abstract; anyone have any concrete thoughts on this?

Anyone who likes $NodePaths all over their code is new to Godot. I don’t say this as a shaming thing. If you look at my early code, it’s there, because it is magic. But, as soon as you start moving nodes around, you now have tons of references to fix.

To coin a term here, $NodePath notation links are Magic Node Paths. They are fancy Magic Strings.

@onready variables are the solution to that. The only time they don’t work is when you need to reference them inside _ready(), in which case you can do:

var timer: Timer


func ready() -> void:
	timer = $Timer
	timer.start()

Potentially, yes. But my job for yeras has been to go into companies and train development teams, including architects and senior devs how to improve their SDLC. Which means if they’re not linting for that stuff in the CI/CD pipeline, we are gonna have a talk.

But my point is that 95% (SWAG number) of Godot developers are hobbyists with no team. So they often adopt style guides that are: ā€œThis is what I did in my last language.ā€ The only people they collaborate with are other Godot hobbyists - either when looking for help, or when doing a game jam.

The style guide is also used by the dev team, so if you follows it, your code looks like theirs and is easy to read. It’s like teaching someone grammar. They will use it without fully appreciating how much better it makes little things. Having bad grammar won’t necessarily affect their communication with others, but it can make them hard to understand.

So yes, in a corporate structure it’s different. But when you’re using a FOSS tool, it’s best in the long run to use the tool like everyone else.

@normalized would say you didn’t. We have different opinions on them. I do think they need to be used carefully and can lead to bad design. But they are not inherently evil - and as long as they are kept lightweight, they are very useful.

Which is why I have over a dozen plugins and some have dependencies. To let people choose what they want to use without bogging them down with unnecessary weight.

I do not remember having a problem with GDUnit and autoloads. They’re easy enough to reference in a test I would think.

Iterators and math operands get a pass. Just yesterdays I gave a pass to stats in a reply to someone. But I suggest they pick that or statistics and not mix both in the same file.

Heh. We all have things in our past.

GameStateMachine In games, that’s the naming standard. It says ā€œThis holds my game statesā€.

I’m sure @normalized has some thoughts. Mainly cause he feels different about OO than I do in many areas.

I’ve never used ECS. So I can have feelings about the architecture, but I haven’t mucked around inside code. My feeling is based on its history, is it didn’t gain much popularity until Apple implemented it in a toolkit in 2015, and then really took off when Unity implemented it in 2018. And like most complex architectures, I think a lot of hobbyists apply it to problems when it is not the hammer for that screw.

As for using ECS in Godot, here’s an article about why Godot doesn’t implement ECS. Here’s an ECS Solution by GDQuest. There was also a 3.x implementation called Godex that looks like it was never ported to 4.x and was abandoned 3 years ago. Emi addressed ECS in Godot in one of the recent Godot Tomorrow streams. He basically said, ā€œWe’re not doing that, but you can do it in Godot if you want.ā€

My personal feeling is that it’s not a good fit. The more you use Godot, the more you’ll see the benefits of using Inheritance AND Composition. I’m a huge fan of inheritance. But Godot really made me re-evaluate the usefulness of components as nodes. I discussed it at length in this post: A discussion about composition and code complexity - #14 by dragonforge-dev Including at the end, where I discussed my approach to my Camera3D and Camera2D plugins, and why I went from a primarily inheritance approach to a component-based approach.

The simple answer is Nodes. It’s why my StateMachine is Node-based. I used to think I needed to do everything purely code-focused to keep things optimized. I learned two things. First, this post talks about the size of Nodes in memory. Second, I learned that Godot only has performance issues if you are pushing 10s of thousands of 3D objects to the screen, or you’re using it crunch numbers for random world generation.

While yes, Node objects do take up a little more space - that space is not going to affect the performance of your game. But what you get is something that’s MUCH easier to use in building games. I’ve been using Finite State Machines since the 80s. When I started using Godot, I followed the examples of using simple Enum-based state machines.

Then I followed this excellent class from GameDev.tv: Godot 4 C# Action Adventure: Build your own 2.5D RPG IO used it to make my game Dash and His Ghost Girlfriend for a game jam. (The reason I don’t use C# with is after 5 minutes, Mono Godot is still loading. and luckily I don’t have to compile it) Here’s what the State Machine looks like in that project:

StateMachine.cs

using Godot;
using System;
using System.Linq;

public partial class StateMachine : Node
{
    [Export] private Node currentState;
    [Export] private CharacterState[] states;

    public override void _Ready()
    {
        currentState.Notification(GameConstants.NOTIFICATION_ENTER_STATE);
    }

    public void SwitchState<T>()
    {
        CharacterState newState = states.Where((state) => state is T).FirstOrDefault();

        if (newState == null) { return; }

        if (currentState is T) { return; }

        if (!newState.CanTransition()) { return; }

        currentState.Notification(GameConstants.NOTIFICATION_EXIT_STATE);
        currentState = newState;
        currentState.Notification(GameConstants.NOTIFICATION_ENTER_STATE);

    }
}

IdleState.cs

using Godot;
using System;

public partial class PlayerIdleState : PlayerState
{
    public override void _PhysicsProcess(double delta)
    {
        if (characterNode.direction != Vector2.Zero)
        {
            characterNode.StateMachineNode.SwitchState<PlayerMoveState>();
        }
    }

    public override void _Input(InputEvent @event)
    {
        CheckForAttackInput();

        CheckForBombInput();

        CheckForDashInput();
    }

    protected override void EnterState()
    {
        characterNode.AnimPlayerNode.Play(GameConstants.ANIM_IDLE);
    }
}

I liked it, but there were parts I didn’t like. For example, you had an @export Array for all the states in the machine. Just use the nodes under it and be done. There was also a Resource-based Stat system.

So when I ported it back to GDScript, I tried a Resource-based state machine to ā€œsave memoryā€. I also thought I was being clever, because they would be available in the Inspector, but not clutter up my scene tree. I discuss it more here.

I ended up just finding that the Node-based approach makes a lot of sense for a State Machine. It’s so much easier to test and modify. It’s so much easier to see all the states very clearly in your editor.

I found the same thing with the idea of a health component. When I first saw these being used, I hated them. At some point, someone asked how you would make one, so I replied: Am I doing Components/composition right? - #3 by dragonforge-dev As you can guess, I ended up making a long post and providing code. More recently, I started using it. I updated the code a bit.

health.gd
@tool
@icon("res://assets/textures/icons/heart.svg")
## A Health Component to add to players and enemies.
class_name Health extends Node

signal damaged
signal healed
signal zeroed

@export var max_health: float = 1.0:
	set(value):
		max_health = value
		health = max_health
@export var health: float = 1.0:
	set(value):
		if value <= 0:
			zeroed.emit()
			value = 0
		elif health > value:
			damaged.emit()
		elif health < value:
			healed.emit()
		health = value


func damage(amount: float) -> void:
	health -= amount


func heal(amount: float) -> void:
	health += amount

I’ve found that the more I use components and Node-based architecture, the easier making games in Godot becomes.

TLDR: I think ECS is overkill unless you are making a game with a company.

ā€œBinding servicesā€, ā€œroot contextsā€, ā€œscore controllersā€ā€¦ in a Tetris clone? You need to de-abstract your thinking.

1 Like

I wanted to use a Tetris clone to show these examples in a non-overwhelming way for a video. It would be ridiculous to do this for such a small game.

1 Like

One more q if you have time!
I still don’t understand how @onready is going to be a better solution to maintaining links to internal nodes than @export

My main issue is that the paths you’d put in @onready are going to be just as brittle to modifying scene structure as normal $ access is going to be. I’m frequently moving stuff around between margin containers and hboxes etc. in my UI scenes and find that linking by export is much more stable for me.

Probably not a big deal as either work, but I want to understand your thinking here.

Aha! Great question!

And the answer is, that’s because you don’t know about Scene Unique Nodes!

Lets take a look at this Player scene and its Health component.

Notice the percent sign (%) next to the script. I added that by Right-Clicking and selecting % Access as Unique Name from the context menu.

Now, its Node Path is %Health for any script attached to any node in the Player scene. (When Player is inside my Level scene, nothing in the level scene can use the Node Path %Health - they would have to use $Player/Health.)

So then if we open up the DeathCharacterState . . .

class_name DeathCharacterState extends CharacterState

@onready var health: Health = %Health


func _activate_state() -> void:
	super()
	health.zeroed.connect(switch_state)


func _deactivate_state() -> void:
	super()
	if health.zeroed.is_connected(switch_state):
		health.zeroed.disconnect(switch_state)
	if audio_stream_player_2d:
		audio_stream_player_2d.queue_free()


func _enter_state() -> void:
	super()
	can_transition = false
	character.set_physics_process(false) #Stop all movement
	character.set_move_state(GameConstants.MoveState.DEATH)
	Game.loss.emit()

You can see how the @onready variable will work no matter where I move the node.


It’s also important to know how I make @onready references. I never type them in. I let the IDE make them for me.

  1. Click and drag the node from the Scene Tree to the Script Editor.
  2. Press the Ctrl key after you start the click and drag.
  3. Drop the node onto your script where you want the line inserted.

This same thing allows you to use UIDs for preloading files as constants. For example, if I drag and drop the Health node’s heart icon image into a script, I get the path: "res://assets/textures/icons/resource_icons/heart.svg" But if I use the Ctrl trick, I get:

const HEART = preload("uid://bwjofo3fm2qcm")

No matter where I move that file, the file reference isn’t going to break.


There’s nothing technically wrong with using an @export variable instead, I just find it to be a bad practice from experience. Because if you use an @onready variable, you reduce human error. I do use @export variables for this sometimes, but that’s usually to get something working when connecting multiple scenes before refactoring it out.

I used @export variables in Eternal Echoes early on to tie Area2D nodes in the base Enemy to states for use and gave them no defaults. Because ranged and melee characters used different ones. The problem was that every time I made a new enemy, I would forget to assign those variables, and the errors I got when it failed were not always very informative.


@export variables are for things you want to customize in the game. One of the benefits of them, is if you change an @export variable while the game is running - it will immediately update in the game even if you don’t save the script. You can tweak things in real time to get whatever you’re looking for like jump height, speed, rate of fire, etc.

They are also useful in Autoloads when you don’t want to hardcode things, or want them easily editable. Take that Mouse autoload that gives you pictures for all the possible actions. I store those in an @export Array.

So if you don’t like the free Kenney images I’ve used, you can just drag-and-drop your own in.

2 Likes

I used to make good code look formal, and unstable testing code was all in a less formal style. If everything is super neat and in the convention then it looks like official and finalized code.

Oh my, this changes EVERYTHING, thanks!!! I’ll spread the word.

Anything of yours you want a shout out for when I make another vid?

1 Like

Well, the whole thing looks like it’s aimed at beginners who will typically make small games and they will likely take you literally and start doing things like this in their three-scene projects.

That said, doing this uber-abstraction stuff in large projects may be even worse. Why introduce those high level generic-sounding abstractions? Are you sure they establish higher degrees of order and reduce complexity? You kinda conflate good architecture with high abstraction here, which is fallacious. Besides, Godot already has strong abstractions that can handle most of the things you invent objects for, namely - node and scene.

That’s very kind. If you think any of my addons/plugins are useful, that would be great. I also have a bunch of devlogs linked at the top of my itch page: https://dragonforge-development.itch.io/ Again, if people want to read about how someone with development experience goes through the mistakes of making games mainly through game jams. :slight_smile: I also have a few 2D/3D shaders for sale there.

I’m working on getting a production-ready version of Skele-Tom up, so that’d be the thing that I’d love a mention for when it’s around. But really, anything that’s organic or germane to your videos.

And of course just send people to the forums in general when they ask for help and suggest they come here BEFORE going to LLMs.

@onready runs right before _ready(), why wouldn’t you want to use it in this example? I use it like that all the time.

@onready is protecting against breaking references when moving the node around the tree, but not against renaming the node unfortunately. And this is one advantage of @export over @onready that you skipped over. When you rename a Node, your @onready reference will break, but @export will not.

As you said yourself:

Thanks for the new word to my dictionary, btw :slight_smile:

And you can create the @export var now as easy as @onready by drag and drop with Alt instead of Ctrl.

I myself prefer the @export, although I can see advantages and disadvantages of both and I use both in different situations.

Overall, very interesting conversation and many good points have been said here.

2 Likes

I have not noticed that. I used to have issues with @onready variables not being available when _ready() runs. Perhaps that changed? I’ll try it out.

Fair enough. In which case the other solution is to give the node a class_name like MyClass and as long as there’s only one of them do something like:

var my_class: MyClass

func _ready() -> void:
	for node in get_children():
		if node is MyClass:
			my_class = node

I personally do not find myself renaming nodes a lot. But that’s a personal choice. There has to be a pretty strong reason for me to rename a node. But…if you do rename a node used by an @onready variable, you get an error right away that tells you where the problem is. Whereas if you forget to load an @export variable, the error is less helpful IMO.

LOL no problem. Always happy to help boost people’s vocabularies.

Ok, that’s pretty cool. I did not know that. When was that added? I will noodle on this one.

I would mention that a very good way of making your game modular is to use packed scenes.

I made heavy use of packed scenes, and the general idea I got was to use @export variables on the root node for interfacing with the host scene, or level, and internally, for things that are linked in the actual packed scene @onready.

The problems I encountered were with patterns involving one script with variable data, like for example NPC’s. What worked in code for one NPC was sometimes tricky for another, and the game objects / concepts / entities are then tightly coupled, as they have the same script but different scenes. When they have the same scene then the character has to be instantiated using ā€˜load()’ and they are a bit more complicated and even more difficult to implement. If the script breaks for any reason during development, then they all break.

So to cut a long story short, the most modular characters use different scripts. Then of course changes have to be applied to all scripts, so they can share components - but they often have an instance of a component that sits in their scene and gets linked to the @onready definition.

Makes sense, except I use const variables and preload packed scenes if they aren’t going to change. As I mentioned above, the UID means the reference is always good. One of the places I use @export variables for packed scenes is for configurable scenes. For example for my Game Template, I have a Game Autoload that has a reference to the player_scene. Then when I make a new game, I just drop the player in (2D or 3D) and the entire system loads up a version of the player when a new game is started.

I solve this problem with my State Machine.

All the Nodes in yellow are inherited from an Enemy scene.

Character States are shared between the Player and Enemies (everyone falls the same). And then any additional states (and code) just gets attached to them. Which goes back to what I was saying about ECS. Godot is Node-based, and if you use it that way, a lot of things become easier.

That’s true and it annoys me to no end. I created a helper function that mitigates this issue by throwing an error when the node is loaded with null value in the @export var.

It is new, it was added in 4.6

1 Like

That’s cool. You could also do it in the setter of the variable.

What is The Godot Barn? Is that your site, or like a clearinghouse of stuff?

Well hot damn!

I also use the @export variable for configurable scenes. I dont have @export linking to a packed scenes internal node, but i think it doesnt do any harm.

Yeah i am aware of this, perhaps its worth re-architecting everything, but at the moment, i dont need to do that.

I actually tend to avoid scene inheritance, i really dont have a problem writing another script for an instancable 100Mb-200Mb asset. The ECS just allows some level of code reuse.

My biggest issue with my game right now is the need for more content. I’ve got to actually design and produce levels. After the level set is healthy I will be refactoring the NPC’s.

1 Like