Ideas Behind General Design of the Game Loop
Importance of Scenes Architecture
An RPG game commonly comes with dozens of scenes which needs to interact with each other, be present or not loaded at all during gameplay.
Note: the whole codebase can be downloaded here.
So it’s important to come up as far as we can with structure the game scenes hierarchy before development even started.
Otherwise, making changes to one scene or script could affect other scenes and script which often causes a lot of debugging, code rewriting and even redesigning features.
For example, if we change single attribute in character’s class script – we would have to go through save system, item system and few menus in order to avoid not only bugs but possible game crash points.
Our Scenes Architecture Choice
As you probably guessed, there are no-one way to make “right” choice for game scene architectures. Even games of the same genre are often distinguished with several features – so applying one architecture to the every game might not be even possible.
Let’s talk now about this game, our RPG and how we are going to prepare controllable environment for the development and later on the playable game.
We will start with only two scenes: Node called “main” and Canvas Layer as it’s child called “UI”.
Node “main” will be starting point of the entire game and it’s loop. It will also serve us to control the game during development – to skip parts of the game or jump to the features which need to be tested without playing the game until we reach them – so yes, this will save us a lot of time and speed up debugging and development in general a lot!
Canvas Layer node or “UI” will have similar purpose when it comes to debugging but it will be mostly used for user interface elements since every element of this category will be instantiated here as a child.
Now when we have two most important scenes we need to make sure they are easily accessible from anywhere in the game. Godot provides feature called “Autoload” which comes handy in this case.
We will create now script called “global.gd” and add it to the autoloads.
As shown on the image “global.gd” is fulfilled with several “objects” of the game, including “main” and “UI”.
Notice how there are “UI” and “ui”?! Well, “UI” is shortcut to Canvas Layer which will never be removed during game loop, while “ui” is link to the at the moment loaded “sub-control” child, like main menu, gameplay user interface, character creation screen, etc. So basically “ui” will always be child of “UI” which will be in control of it.
Now, let’s attach script to the “main” scene.
Now, let’s attach script to the “main” scene.
As you can see at fourth line, node “main” is linked to the variable “main” in the autoload “global.gd” – meaning that no matter where we are in the game “main” scene/script can be called by typing “Global.main”.
Under the “_process” function you can see placeholder of instantiating boss while action “ui_accept” is pressed. This can also be “spawn character to the certain point of the map”, “play certain animation”, etc.
Just as “main” is defined in fourth line, “UI” is linked to the autoload “global.gd” below it and then in the same node we are loading and instantiating “Main Menu”.
And we will do this for every scene which needs to be accessed often during the game: Game world, player’s character, etc.
Let’s add other autoloads to the list. We are going to need: “combat.gd”, “audio.gd”, “options.gd”, “items.gd” and “drop.gd”.
Once when we are done the autoload panel should look like following image.
Each newly added script will have unique role in our RPG.
Autoload – Global Scripts
“combat.gd” – will be in charge to calculate and deal with all physical and magic hits, to decide if targeted being will continue ot fight or die. There will be coded all functions for combat triggers and aftermaths.
Our combat results will come from function “hit” placed in “combat.gd”.
As shown on the image, function “hit” has several lines of code and accepts two arguments, “dealer” and “target”. First argument “dealer” is game object that makes damage to the second argument “target”.
At the first part of the function, we can see how damage being calculated and transformer in order to apply armor decrease percentage. Then, using random number between integer 1 and 100 we will give critical strike chance to occur. After that target’s current health will be lowered for final damage variable and in case where target’s current health is equal zero or below it, “death” state will be called for target.
Rest of the function lines will be triggered only if player’s character is the dealer.In that case, popup text will be instantiated as indicator of damage value as well as call for function “add_experience” in the player’s character script, which will, as it’s name says, add experience to the current experience.
While single function might work for this game – there other cases where dozens of functions could be made in order to deal with specific combat calculations. For example, there might me skill that deals damage which depends on percentage that comes from some features – so we might want to expand “hit” functions or separate it from it and create the new to serve this goal especially. Also, health, mana, rage, stamina or some other source of “energy” gain calculations could also be placed here in “combat.gd”.
“audio.gd” – we are going to call whenever there is need to play music or sound effect.
Here will be coded three functions: “instance_music”, “instance_sound” and “instance_sound2D”. While similar, especially “instance_sound” and “instance_sound2D”, each of them has unique purpose.
Going through codelines of function “instance music” we can see that first it will check if there is node “music” already instantiated. If statement is satisfied next line will make sure that current music is removed from the game – this is how we make sure to always have one music playing in the background. After that the new music will be instanced and played.
Note: It’s important to make sure that music files are set to loop, there are several ways to make this work.
Next function is “instance_sound”, and just as “instance_music” it will accept one argument of type “String” wchich will be the name of the sound used to make “path” to the wanted audio file.
This one is used exclusively for user interface sounds: Buttons, fade in or out, loadings, menus popups, sliders, etc.
After node is instanced we can see that it is connected to the signal “finished”.
This signal also comes with argument which is node AudioStreamPlayer, binded to it. It’s one way to make sure that sound leaves the game after it’s finished playing since since we can’t use single “name” like we done for the music. Since during the game there will be multiple sounds playing at the same time.
The final function in “audio.gd” will be “instance_sound2D” and it’s differ from “instance_sound” just a bit.
The final function in “audio.gd” will be “instance_sound2D” and it’s differ from “instance_sound” just a bit. First, the sound will be instantiated only if there is no same sound already playing.
This is lazy way to make sure that two same audio samples will not be instanced at the single frame – since this often produces “ugly”, distorted sounds.There are other, better way to take care of this audio problem but for our purpose this one will do just fine.
Second, as you can see on the image, this function accepts second argument besides the sound name and it’s position represented by type Vector2. This function will be used to bring “world” sound to the scene. So instead of using “AudioStreamPlayer” we will use “AuidoStreamPlayer2D” which provides instantiation to the position and this is where our Vector2 will be applied.
So basically it makes sure that sounds of casting are coming from the caster, sounds of hit from where hit is happening, etc.
This function will also be connected to finish signal in order to make the sound go away when it’s completed.
Note: If you look at the instantiation for all three fucntions, you will notice how each sound is set to the “bus”. For “music” it’s “bus” called “music” and for “sound” and “sound2D” nodes it’s “bus” called “sound”. Playing them on different channels means that we can offer player more control over sound and music volumes or mute in general.
This allow us to separate options to make sure that sounds volume can be lowered while music volume still be louder or other way around. Or mute the music when other sounds still can be played.
“options.gd” – similar to “audio.gd” will be used to deal with current settings setup as well as apply saved settings at the beginning of the game – if there is such a save file, so let’s see how it works.
There are three variables defined at the beginning of the script, “mute_audio” as bool set to false, “music_volume” and “sounds_volume” as floats set to value 0.5.
These settings are also default options settings.
If there is saved fine at the forth defined variable called “path, it will be loaded and current settings will be rewritted. This happens under “_ready” function followed by direct changes to the “AudioServer-s” by setting “bus” to mute for “Master” and applying values to the “bus” volumes.
Second part of “options.gd” is represented by two functions: “save_options” and “load_options”. In short, as images shows – we use “FileAccess” to save/load options.
“items.gd” – will contain all items represented by dictionaries pre-created or randomly generated.
Image shows example of a pre-created item “Iron Sword” – defined by “Dictionary” which contains all needed data. The downside of this is that item is “stable”, we can get it’s data into the player’s inventory slot, drop chance, merchant’s menu, etc., but it will always be the same.
This is where our function “generate_iron_sword” comes handy.
While we want to place pre-created items in the character’s equipment slot at the beginning of the game to make sure each player starts equial, we want to make so that drop in the world differ.
So every “Iron Sword” don’t need to have same attributes attached to it. This function allow us to do that – by randomizing the stats.
First, and empty “Dictionary” is created and then common things are added to it, like name of the item, texture path, model path, etc. Then random numbers will define “lower_damage” and use it to define “upper_damage”. After this is done, function will provide random change for attributes “power” and “critical_chance” to be added to the list as well as also defined by random numbers.
Finally, semi randomized “dictionary” will be returned.
Note: This might be the fastest way to do it and while it will serves us well in this MPV, it might not be the best options for entire game since we would end with large script of thousand codelines. This could be chaotic to navigate and make changes, even just search for certain items. So it’s a way better to use outside program for datasheet of item attributes, like Microsoft Excel or maybe even code Item Editor exclusively for the game (can be done with Godot).
“drop.gd” – it’s another name for “Drop Table” script.
Script that manages drop in the games are often long and complex. Sometimes there are even multiple scripts in charge of loot and even files made outside of engine itself.
Our script will be simple, regardless – it will make so that chests and monsters leaves some items for player’s character to pick up, including gold coins and equipment.
Let’s look at the code lines now! It’s single function where we can pass single argument, position of type “Vector2”. This position is point where item will spawn in the world.
So, if enemy make drop from it’s death state, it will pass it’s position to this function so it can create item on the enemy’s fall point.
For chests it’s a bit different. When player opens chest, for each spawned item we want to pass different position to avoid overlapping. Otherwise it could be hard to pick wanted item from the ground.
This counts for equipment item, gold coin are automated loot, meaning that player’s character just needs to be or pass near them in order to pick it.
Next up, part 2: