Alright! Back to the meat and potatoes. This one’s gonna be about implementing our main mechanic, the gun! Bear with me on this one, because there’s a lot of muddling around with design. Lots of uncertainties. But, if you never make decisions, even potentially bad ones, you’ll never get anything done. There are some questionable design choices I’ve begun to make, mostly in the interest of moving development forwards; mainly, there’s a lot of coupling between my classes. I’m just trying to approach the design realistically as I’m not expecting the codebase to get very large.
Anyways, gun. I started off by setting up some simple variables pertinent to the gun’s functionality; knockback for movement, firing speed, reload time, clip size, and so on. I set up a simple bullet class that the gun holds, which is currently just a skeleton to get the ball rolling; its base implementation can be spawned at a position with a direction, and it will move at a set speed until it collides or its lifetime expires. Different bullets can inherit and alter the base behavior here, and in the future the goal is to hold arrays that reference player perks or effects. I’ve designed a modular effects system for this, but that’ll be a different post.
So the simple bullet is called to spawn from the gun when its fire() method is called, which instantiates returns the bullet scene to the script that calls it. My current approach is to have the script that calls the gun to fire handle the ownership of the resulting bullet. With Godot’s hierarchical transform model, you can’t simply have the bullet be a child of the gun or the player, as the bullets would inherently be transformed along with the movement of their parents. While you can try to code around this by tracking and updating each bullet’s global_position every frame, this would be antithetical to Godot’s node system. My current approach is to designate a node centered at the origin of the base scene that the player holds a reference to; I would then be able to call this scene to manipulate the bullets. However, I’m also considering using Godot’s grouping system instead, which would allow me to mess with the bullets from anywhere, and as a bonus I can just throw them into the root node of a scene. This would reduce dependencies at the risk of control, but it’s become more appealing to me as the project has shaped up.
As it stands my system is as follows: player is in a state that allows them to shoot. Player shoots, making the gun fire. Gun instantiates bullet and modifies its damage and speed with its own parameters, then passes it back to the player. The player sets its direction, adds its current perks to the bullet (not yet implemented) and adds the node to a neutral node outside of its own hierarchy. At this point the player tosses its direct access to the bullet node.
Neat, so we’re firing bullets now! But our gun doesn’t visually exist, so let’s set that up. I threw in a quick gun sketch I made as a placeholder sprite and set up 3 Position2D nodes to track some important points, such as the point the gun is “held” from, the muzzle point where the bullet should spawn, and the point where the sprite should attach to the player’s back when they’re on the ground. This will allow me to quickly set up new gun sprites of varying shapes and sizes, so I don’t have to worry about standardization. In my code I set up a few different methods to manipulate the gun sprite’s location in reference to its origin, mainly by shifting the location of its offset. This way I can easily rotate the sprite around this point.
func switch_held() -> void:
#switches to "hand" position in air
_held = true
#gun_sprite.scale = Vector2(1,1)
gun_sprite.flip_v = false
#set held gun distance and rotation point
gun_sprite.offset = Vector2.ZERO - held_position.position/abs(gun_sprite.scale)
gun_sprite.offset = (gun_sprite.offset+Vector2.RIGHT*_hold_length/abs(gun_sprite.scale))
func switch_back() -> void:
#switches to back position on ground
_held = false
gun_sprite.flip_v = true
gun_sprite.offset = Vector2.ZERO + back_position.position/abs(gun_sprite.scale)
You may notice that I’m dividing the offset by the sprite scale; this is so I can easily scale the gun sprite on the fly without worrying about messing up the offset calculations. The “hold_length” variable shifts the position of the gun sprite along its right vector, which is where the sprite should point forwards to. While I could just change the location of the hold position to similar effect, I found this helpful for getting the positioning right on the fly, and I find it a bit more intuitive, personally. In my code I also set up methods for flipping the gun sprite, getting knockback and so on. Right now the biggest design flaw is that currently I rely on checking the rotation of the gun sprite itself when spawning a new bullet. This is a little icky; now that I have the functionality of how I want the gun to work plotted out, I’d rather switch from manipulating the sprite to manipulating the gun node itself. I’d move from using the offset of the sprite to simply spacing the gun from the origin using the reference points. This is just busywork though, so I’ll throw it on my Trello and do it on a slow day.
To properly display the gun, as well as to properly update the hitboxes of the player when they walk vs jump, I set up a global enum for the player state, and set up a field for each state in the player State Machine to hold it. Quiz time! Which of the following implementations is best for interpreting the outcome of the state? Is is option 1, a gross looking set of conditionals? Is it option 2, the most disgusting repetitive switch statement? Or is it option 3, which couples itself to the enum values but looks all clean and pretty?
#if conditional nightmare
func switch_hitboxes(value:Globals.PLAYERSTATE) -> void:
if (value==Globals.PLAYERSTATE.IDLE or value==Globals.PLAYERSTATE.MOVE
or value==Globals.PLAYERSTATE.GDASH or value==Globals.PLAYERSTATE.ADASH):
ground_collider.disabled = false
jump_collider.disabled = true
elif (value==Globals.PLAYERSTATE.JUMP or value==Globals.PLAYERSTATE.FALL
or value==Globals.PLAYERSTATE.STOMP or value==Globals.PLAYERSTATE.HURT):
ground_collider.disabled = true
jump_collider.disabled = false
#switch hell nightmare
func switch_hitboxes(value:Globals.PLAYERSTATE) -> void:
match value:
Globals.PLAYERSTATE.IDLE:
ground_collider.disabled = false
jump_collider.disabled = true
Globals.PLAYERSTATE.MOVE:
ground_collider.disabled = false
jump_collider.disabled = true
Globals.PLAYERSTATE.JUMP:
ground_collider.disabled = true
jump_collider.disabled = false
Globals.PLAYERSTATE.FALL:
ground_collider.disabled = true
jump_collider.disabled = false
Globals.PLAYERSTATE.GDASH:
ground_collider.disabled = false
jump_collider.disabled = true
Globals.PLAYERSTATE.ADASH:
ground_collider.disabled = false
jump_collider.disabled = true
Globals.PLAYERSTATE.STOMP:
ground_collider.disabled = true
jump_collider.disabled = false
Globals.PLAYERSTATE.HURT:
ground_collider.disabled = true
jump_collider.disabled = false
#kinda iffy coupled code
#setup enum like this
enum PLAYERSTATE {IDLE=3, MOVE=6, JUMP=1, FALL=4, GDASH=9, ADASH=12, STOMP=7, HURT=10, SHOOT=2}
func switch_hitboxes(value:Globals.PLAYERSTATE) -> void:
match value%3:
0:
#ground hitbox active
ground_collider.disabled = false
jump_collider.disabled = true
1:
#aerial hitbox active
ground_collider.disabled = true
jump_collider.disabled = false
The answer is…none of them! I wasted my time, and now I’m wasting yours! Somewhere down the line I forgot that I was literally using a state machine, so I just setup the hitbox and gun state changes within the individual states, which gives me superior control, and won’t break if I add more states or hitbox types. Goodness gracious. Though, if I was to process it one of these ways, I’d probably go for option 1. While it does look a little overwhelming, it’s easy enough to figure out and doesn’t rely on specific enum values that might need to be changed in the future. I also value the readability of the code over the ambiguous nature of the third option. Option 2 is obviously out.
Moving on from that debacle, I started working on reload and fire rate systems. So far I’ve handled timers in the process loops, but this implementation tends to gum up what should be treated as holy real estate, so I wanted to utilize a more clandestine approach to restricting firing capabilities.
For now (though this might come back to bite me in the butt later) I’m trying out handling some timer variables with coroutines instead. Godot 4 uses “await” now instead of yield, referencing a signal as a member variable rather than looking for a signal as a string. While this is definitely a cleaner approach to code design (Godot’s reliance on string identifiers still continues to baffle me), part of me is just slightly unsettled by its similarities to Javascript. I’m going to try to ignore that. Something interesting I noticed after the auto-upgrade to Godot 4 is that the technically deprecated emit_signal calls were not automatically converted to the new structure. Seems weird not to do that? I’d imagine that there are some cases with the old format that the new one cannot emulate.
#Godot 3.5
emit_signal("reload_started", _reload_time)
yield(get_tree().create_timer(_reload_time), "timeout")
emit_signal("reload_finished")
#Godot 4
reload_started.emit(_reload_time)
await get_tree().create_timer(_reload_time).timeout
reload_finished.emit()
I hooked up some booleans to check the “fireability” against, and set up my firing rate limiter in a similar way. As it stands, the functions to check fireability are ultimately called within the states that can transition to the shooting state, but honestly, this might change. I originally wished to have a separate state for shooting because I envisioned it as being more customizable in the future, but so far it’s just been an irritation. The advantage it gives me as a state instead of a simple function call is that I get fine control over what state it transitions into after the shot, but in most cases this is going to be the state it was already in. If I decide to implement ground shooting in the future, I’ll need to pass the proper state from the physics frame before the shot so that it can transition correctly, which is just slightly irritating. I’m keeping it as-is for now, but it’s on my radar.
Alright, so there’s pretty much a base-function gun here, but the bullets do nothing! Let’s fix that. While they’re deleting themselves on collision, the bullets aren’t passing any data to their collisions. I contemplated on whether to have the bullets initiate collision handling or the entities, and I settled on the bullets, at this point arbitrarily. Currently the bullets just check to see if the collision body is an Actor and in the enemy group, but I’ll probably change this later to work on any Actor so that I can recycle bullet code if I want my enemies to shoot at the player.
We need something to shoot, so I scaled up the prototype enemies which I called “Gooby”, subconsciously channeling some Canadian children’s movie of the same name starring this uncompromising badass.
Anyways, I overrode my health setter functions in the goobies (this is the proper plural) to trigger destruction (right now just queue_free, animations and signals for a combo system later) in entities that fall to 0 or under. Very simple stuff right now, but now I have a semblance of a game! I don’t think I mentioned it before, but I also set up some raycasts that extend to the player’s of the physics step when they are falling, that interact with an area hitbox I can modularly add to my Actors. Bouncy! This is something that interacts with Actors by accessing one of their member functions, though each inheritor should define their specific behavior on their own. This could be just ordinary destruction, could maybe trigger an explosion, the enemy might be resistant to stomps and get enraged, whatever. I’m leaving that design space open for now.
Lastly, I set up some signals coming out of the gun and the player that can be utilized by GUI elements. As it stands the gun is temporarily relaying its signals through the player class so that I have easy access to connecting them to my Control elements, but this might change in the future if I decouple the gun structure from the player. For now I just have some helpful little outputs that will help me debug things moving forwards, and can easily add more with this system. As it stands these values are all signal based, so they probably won’t remain infallible as I alter my codebase, but because of the design I shouldn’t encounter any catastrophic errors due to the inherently decoupled nature of signals.
That’s all for now. My next goals are to work on some visual elements, set up a combo system, get some basic placeholder win/lose states, and set up an enemy base that I can use for all my baddies. I’d like to try can get to a semi-playable state so that I can start using expectations of gameplay to inform my design as I move forward. Thanks for reading, and give me a job, please.
Update: I love the new signals. I don’t even care that they look like JavaScript. If I change or delete a signal I don’t have to trudge around looking for string references anymore. I want them to completely remove the old signals so I can type the signal arguments more aggressively.