Star Child Dev Log #22
Jay Ingle - lead developer, designer, and artist:
My devlog is late this week. Because I completely forgot I needed to make one! How do you forget to do a thing that has been due every week for several months? Let me tell you. If you are a solo game developer, there are 1 million things you need to do to finish your project. It is not an easy task to remember what you are supposed to be doing, when you get lost in so many things that need to be done.
Anyway! The last couple weeks we have been looking at an enemy I have created, going deep into the node structure and coding. Let's continue.
Here is our enemy and its behavior, with placeholder graphics:

First, we can take a look at some exported variables:

@export just means that we can adjust these values directly in the editor, per enemy, rather than having to go into the code itself. Knockback strength and damage_dealt are self-explanatory, other than the fact that this damage_dealt is how much damage the player takes when running into the enemy, not the damage from the actual lava bomb the enemy is dropping. first_delay lets you set how long the bomber initially takes before firing, from the moment the scene is loaded, per enemy. If we did not have this value, every bomber would fire their first shot at the same time. This can look unnatural if there are several on the screen.

The above code is several signals that are built into Godot's nodes. the first _on_body_entered(body) performs its code whenever a body enters the hitbox of the enemy, such as the player, which is a CharacterBody2D. What can be detected by this hitbox is specified by collision layers. I have specified that nothing except the player, or the player's attacks, can interact with the enemy's hitbox. So we can check if the player is the body that has entered, and if so, damage the player. Otherwise, it must be an attack from the player, and thus run the appropriate code, i.e. setting dead to true and running explode_and_die(). Some of the player's attacks are actually bodies (such as RigidBody2D), and some are areas (Area2D), which is why we need the _on_area_entered(area) signal at the bottom as well. Godot also has convenient groups we can add things to, and check things against, and I have one called "attacks" that all player attacks are in.
The middle signal is called when the death timer times out. The death timer is called when the enemy is killed, which allows time for death effects and such to play out, before the node is completely removed from the scene. queue_free() = remove enemy completely right now.

Above we can see how we damage the player. First of all, this should be in an enemy class script. And it is! But here is the problem: Godot classes are tied to node types. They can only extend a specific node type. Let's say I have one enemy that has a root node of RigidBody2D, and another enemy, such as this one, that has a root node of Area2D. Well, these enemies cannot be part of the same class. I have tried making the root node the same for all enemies, but Godot doesn't like it when you try to do physics on a node that is a child of something that does not have physics interaction. As in, I tried to make every enemy root node Node2D, and have a child that is the RigidBody2D for physics. This does not work, as far as I know. So make every enemy a RigidBody2D? Then I would have to write specific code for every enemy that is a RigidBody2D that does not need to interact with physics, just to stop it from interacting with physics. Another idea is to have 2 enemy classes. One for one that uses physics, and one for enemies that don't. But then I would have 2 of the same exact script. And I would have to remember to update both anytime I wanted to update one of them. This is not good. So, for now, I have an enemy class script for enemies that interact with the physics engine, and enemies that are of the Area2D type, they have all of their own code. I do not like any of these answers, but this way is the one I'm least likely to mess up, over the course of the project.
And second of all, this should not even be in an enemy class, damage_player() should be in the player script. This caused problems that I cannot even remember. So here we are, let's not work on fixing something that works already. Do it better next time. Explaining the details of this function isn't important for talking about this enemy, so let's move on.
_on_fire_cooldown_timeout() is called when it is time to fire a bomb. As long as the enemy is not dead, wouldn't want a dead enemy to be firing at you. After that is a solution to a problem. Currently, if the enemy reaches the edge of the ceiling, he turns around. What if he reaches the edge, and it also just so happens to be the time to start firing a shot? Well he might be turning around the entire time he is firing a shot. And if so, where the merry-go-round stops is anyone's guess, he might just walk off into thin air. So he will only fire a shot if he is not too close to the edge. Now, the tradeoff for this fix is he can skip a cycle of firing, doubling the time between shots. This is rare, and the player will surely not even notice.
The last line: animations.play("fire") is important of course. It not only plays the visual animation, but it also calls the fire() function, and the start_walking() function.

Our fire() function is a standard method of instantiating a scene in Godot, which means I am adding a preloaded scene into this scene (lava_bomb variable holding this preloaded scene). I do this to add the lava bomb into the world, at the right time. I have also added a Marker2D node in the enemy scene, which gives us an easy way to say where exactly to spawn the bomb.
At the end of the "fire" animation track, it calls start_walking() and the cycle continues.
At the bottom we see the explode_and_die() function that was referenced previously. It starts the death timer, disables the hitbox collision, makes the enemy invisible, plays the death audio sound, and shows you some particle effects. If we just did a queue_free() to delete the enemy the moment it was hit by an attack, it would instantly be gone, and you would not hear the audio, or see the effects. So we gotta give it a couple seconds to finish its work, and then delete when death timer completes.
We are nearing the end of this story, but we have at least one more week to talk about this guy. Specifically, we need to talk about the lava_bomb that is being added to the scene when the enemy fires. This is a completely unique scene that we can take a look at. So tune in next week!
My devlog is late this week. Because I completely forgot I needed to make one! How do you forget to do a thing that has been due every week for several months? Let me tell you. If you are a solo game developer, there are 1 million things you need to do to finish your project. It is not an easy task to remember what you are supposed to be doing, when you get lost in so many things that need to be done.
Anyway! The last couple weeks we have been looking at an enemy I have created, going deep into the node structure and coding. Let's continue.
Here is our enemy and its behavior, with placeholder graphics:

First, we can take a look at some exported variables:

@export just means that we can adjust these values directly in the editor, per enemy, rather than having to go into the code itself. Knockback strength and damage_dealt are self-explanatory, other than the fact that this damage_dealt is how much damage the player takes when running into the enemy, not the damage from the actual lava bomb the enemy is dropping. first_delay lets you set how long the bomber initially takes before firing, from the moment the scene is loaded, per enemy. If we did not have this value, every bomber would fire their first shot at the same time. This can look unnatural if there are several on the screen.

The above code is several signals that are built into Godot's nodes. the first _on_body_entered(body) performs its code whenever a body enters the hitbox of the enemy, such as the player, which is a CharacterBody2D. What can be detected by this hitbox is specified by collision layers. I have specified that nothing except the player, or the player's attacks, can interact with the enemy's hitbox. So we can check if the player is the body that has entered, and if so, damage the player. Otherwise, it must be an attack from the player, and thus run the appropriate code, i.e. setting dead to true and running explode_and_die(). Some of the player's attacks are actually bodies (such as RigidBody2D), and some are areas (Area2D), which is why we need the _on_area_entered(area) signal at the bottom as well. Godot also has convenient groups we can add things to, and check things against, and I have one called "attacks" that all player attacks are in.
The middle signal is called when the death timer times out. The death timer is called when the enemy is killed, which allows time for death effects and such to play out, before the node is completely removed from the scene. queue_free() = remove enemy completely right now.

Above we can see how we damage the player. First of all, this should be in an enemy class script. And it is! But here is the problem: Godot classes are tied to node types. They can only extend a specific node type. Let's say I have one enemy that has a root node of RigidBody2D, and another enemy, such as this one, that has a root node of Area2D. Well, these enemies cannot be part of the same class. I have tried making the root node the same for all enemies, but Godot doesn't like it when you try to do physics on a node that is a child of something that does not have physics interaction. As in, I tried to make every enemy root node Node2D, and have a child that is the RigidBody2D for physics. This does not work, as far as I know. So make every enemy a RigidBody2D? Then I would have to write specific code for every enemy that is a RigidBody2D that does not need to interact with physics, just to stop it from interacting with physics. Another idea is to have 2 enemy classes. One for one that uses physics, and one for enemies that don't. But then I would have 2 of the same exact script. And I would have to remember to update both anytime I wanted to update one of them. This is not good. So, for now, I have an enemy class script for enemies that interact with the physics engine, and enemies that are of the Area2D type, they have all of their own code. I do not like any of these answers, but this way is the one I'm least likely to mess up, over the course of the project.
And second of all, this should not even be in an enemy class, damage_player() should be in the player script. This caused problems that I cannot even remember. So here we are, let's not work on fixing something that works already. Do it better next time. Explaining the details of this function isn't important for talking about this enemy, so let's move on.
_on_fire_cooldown_timeout() is called when it is time to fire a bomb. As long as the enemy is not dead, wouldn't want a dead enemy to be firing at you. After that is a solution to a problem. Currently, if the enemy reaches the edge of the ceiling, he turns around. What if he reaches the edge, and it also just so happens to be the time to start firing a shot? Well he might be turning around the entire time he is firing a shot. And if so, where the merry-go-round stops is anyone's guess, he might just walk off into thin air. So he will only fire a shot if he is not too close to the edge. Now, the tradeoff for this fix is he can skip a cycle of firing, doubling the time between shots. This is rare, and the player will surely not even notice.
The last line: animations.play("fire") is important of course. It not only plays the visual animation, but it also calls the fire() function, and the start_walking() function.

Our fire() function is a standard method of instantiating a scene in Godot, which means I am adding a preloaded scene into this scene (lava_bomb variable holding this preloaded scene). I do this to add the lava bomb into the world, at the right time. I have also added a Marker2D node in the enemy scene, which gives us an easy way to say where exactly to spawn the bomb.
At the end of the "fire" animation track, it calls start_walking() and the cycle continues.
At the bottom we see the explode_and_die() function that was referenced previously. It starts the death timer, disables the hitbox collision, makes the enemy invisible, plays the death audio sound, and shows you some particle effects. If we just did a queue_free() to delete the enemy the moment it was hit by an attack, it would instantly be gone, and you would not hear the audio, or see the effects. So we gotta give it a couple seconds to finish its work, and then delete when death timer completes.
We are nearing the end of this story, but we have at least one more week to talk about this guy. Specifically, we need to talk about the lava_bomb that is being added to the scene when the enemy fires. This is a completely unique scene that we can take a look at. So tune in next week!