Hey gang! Welcome back to another Gravity Ace devlog.
Hope you're all doing well. We're still social distancing here but mostly it's fine. I mean, I'm pretty used to working from home so it doesn't feel super different to me but… I also haven't been able to really focus on the game very much these past few weeks so progress has been slower than I'd like. It's like a combo of covid anxiety and burnout… But it's getting there! :-)
This week I want to do something a little different. A brave soul named Martal (@Martal on Twitter) asked if I could give them some advice on their game. They sent me the source code and I looked it over and it's pretty good. Nice work, Martal! So, first, I want to talk about all the things that I think are good about what they've done. And remember, this is an early look at a game in progress so there are lots of placeholders and rough edges. I'm going to ignore those because obviously those will be the types of things that'll get cleaned up and polished later as the game comes together.
A nice thing that Martal does is take advantage of Godot's node architecture. They make good use of child scenes and encapsulate functionality into those child scenes to make maintenance and coding easier. So here's a scene for a level. And in that scene you can see they've got child scenes for jump pads, the player, the level exit, and treasure.
Godot has a sort of object-oriented nature and it's good not to fight that if you're going to use it.
That reminds me that someone else asked me recently if Godot was right for them… basically, it depends on you. Some people love Godot and some people don't. Some people love Unity and some people don't. Not every game engine works the same and not evey person is the same. So you've got to try them out and see which one feels right to YOU. What works for you doesn't work for others and THAT'S FINE.
Another good thing that Martal has done is built a singleton for managing overall game state, transitions between scenes, and a library of common functions. So what's a singleton? In Godot, it's a SCRIPT or SCENE that is loaded once and persists throughout the entire lifecycle of the game. They allow you to store global values like player information and share that information between scenes and to handle transitions between scenes like levels or menus. They also allow you to create global functions that are available to every other node in your game like a library. You implement them by writing a script or creating a scene and then adding it to the project in the Autoload tab. The name you choose for the singleton is used to reference it from code. You can read more about singletons/autoloads in the Godot documentation.
Martal's singleton is called Game and so is one of mine! Martal's is a GDScript. Most of mine are full scenes with child nodes. One of mine has an animation player that draws the curtains you see during scene transitions. Super useful because it's loaded automatically and I can just call Game.transition("…") to switch scenes from anywhere. Another example is an audio player for music that continues playing between scenes.
Now there are some people who'll say “singletons are evil” and others will say “singletons are good” like it's an absolute. I don't know, man. Things go in and out of fashion all the time and coding patterns are no exception. I don't think anything is that black and white. I say use the right tool for the job. And if that means use a singleton then don't twist yourself into knots to avoid it just because a rando on the internet said they're bad. We're making games. It's supposed to be fun.
Another neat thing that Martal does is manipulate
Engine.time_scale. This is a property that lets you manipulate how fast the game runs so you can do effects like slow motion or bullet time.
So, some tips.
Not too many. Like I said, I think you've done a good job so far and this is a good base for building out the rest of the game.
One of the first things I noticed was
goto_scene() in your Game singleton. It felt overly complicated to me. I think you can replace all of THIS [showing old code] with THIS [showing new code]. And then if you want to get fancier you can put in a check here if the scene doesn't exist and go back to the main menu or something.
Here's a small nitpicky thing… It's just a style thing but I like making constants and global variables ALL CAPS. That just lets me visually scan a method and know immediately which variables are global and which are local to the method.
IndividualJumpPad.gd… you've got a ready function here that just plays an animation. Another way of doing this is to set the animation to auto play here [showing autoplay button]. Then you can eliminate that code. Since there's nothing else here you could even remove the entire script. This is partly a matter of taste but in general I prefer less code. Every line of code is a potential bug so the less code the better.
While we're looking at the animation here's a quick tip that might make these easier. If you want, you could replace this Sprite and AnimationPlayer with a single AnimatedSprite node. Those work with spritesheets now. For more control, you can use an AnimationPlayer and here's a little shortcut.
You can set the Update Mode to Continuous and the Loop Wrap Mode to Clamp. Continuous let's the animation update between key frames and Clamp keeps the last frame until the end of the timeline. It's easier to show than tell. So, here we have a classic frame animation. When you animate frames on a Sprite the AnimationPlayer creates a track with Update Mode set to Discrete and Wrap Mode set to Wrap. Then you put in each frame and everything works. If you've got a lot of frames that can be a pain. So I like to use Continuous and just put the first and last frame. Then I also set Clamp so that the animation holds the last keyframe to the end. Makes it a lot faster for me to setup the animations. And I control the timing of these types of animations in the drawings themselves. It's a nice workflow where an artist can control the timing without needing to mess around with the AnimationPlayer.
OK, last thing. You're using
lerp() in your player controller to control the speed and direction of your player movement. Fine.
lerp() is short for linear interpolate and it basically just does some simple math for you to return a number that is some percentage or weight between two other numbers. The first value is where you start, the second value is where you're going to, and the third value is the percentage (weight) between them. If the weight is 0 then
lerp() will return the first value. If it's 1 it'll return the second value. And it's between 0 and 1 then it'll return some value in between.
So you've got this state machine in the
_physics_process() and you're setting the velocity of the player by lerping from the current velocity to the max velocity. The problem here is the weight. Because this is in the physics process and the physics process always runs at 60fps and
ground_acceleration is also a constant, weight will always be a constant: 0.83 or 83%. So based on the values in your script, your first frame, velocity.x will become 333; second frame: 388; third frame: 398.
Now say delta changes because you've changed your
Physics FPS to 120 in the project settings. That's twice the frame rate but your character will behave slightly diffently. Your first frame, velocity.x will be 166. Second frame: 263. And by the sixth frame: 384. In the same TIME, the player will be moving slightly slower.
Given that you're trying to handle variances in delta this doesn't quite work. And on the other hand, if you don't expect Physics FPS to vary (which it probably won't unless you actually change the value in your settings) then it might be easier to reason about what's happening by replacing
ground_acceleration * delta with just
I don't know what the right solve is because it depends on what you're going for in your game. And, bottom line, this works already as-is. If it feels right to you and you understand it then there's not really any reason to change it. Just something to think about.
Alrighty gang, this has been fun. Thanks Martal for being so courageous and sharing your code with us. I look forward to seeing your game on itch.io where we can all play it!
Thanks for watching everybody. Stay healthy and see you next time!
Published April 19, 2020