This post contains the aggregation of 7 devlogs for a game I attempted building with Godot between January and March. I didn't complete the project, but am glad I gave it a try; I learned lots and had fun during the process.
Part 1: Starting Development (January 10, 2022)
I've kicked off my first quarterly project - to build a game. It's going okay, but as I suspected, returning to my day job has been a small reality check on how much time I have to spend on projects. I got a bit ahead of myself with all the free time I had over the Christmas break.
With that said, Visitor, as I'm calling it, is underway. It's a game about a roving piece of light that interacts with other pieces of light in space. That would be the most literal description I've given it, as I tend to think of it much more abstractly.
The first week and a bit was comprised of writing up notes on what would need to be done, as well as a bit of drawing and sketching.
There's lots to do. Too much for 3 months, to be honest.
Starting out, the game will only be for mobile. Below, I've got a sketch of how I might approach the generation of the game's background. As a player moves through space, the stars need to move behind them. Rather than make one very, very, very large canvas of stars, I'm thinking I will need to generate the stars that are just outside the reach of the player, so that when the player leaves the current background they move into a newly existing background with a new set of stars.
On the outset, this will work fine - but that means there will be a cool feature.
This problem is a bit hard to wrap my head around as I get re-acquainted with Godot. I have a single background rendering, but now I need to figure out how to dynamically move and/or spawn ones.
There was an initial reluctance on this project as I've already hit some walls on the outset, but I'm happy to be coding on something multidisciplinary and to take a break from building command-line programs.
Part 2: Moving Compass (February 9, 2022)
I've finally gotten around to working on Visitor a bit more. I must admit that I've been procrastinating - mostly because game development is intimidating and I never feel completely in the know on what's happening (for now!). I don't expect to be done with this little game by the end of March, as I had planned as part of my "Quarterly Projects" - but I'm still glad I took it on.
Absorbing Light
I left off on the previous post hypothesizing how I might create the "absorb light" mechanism. Most of the pseudo code I wrote ended up being pretty much what I needed to do. Let's walk through the code (it's pretty rough.)
extends Area2D
var in_contact_with_player = false
var rate_of_absorption = Vector2(0.005, 0.005)
var has_been_absorbed = false
var original_size
var rng = RandomNumberGenerator.new()
# NOTE: this might not be ideal if every node gets the player from the tree?
# guess it might not matter much if there will never be that many light nodes on the screen...
onready var player = get_node("/root/Game/Player")
Above we set up the "class properties" so to speak. I can't remember if GDScript is technically thought of this way, but whatever.
Next up, we have the get_absorbed
method:
func get_absorbed():
if has_been_absorbed:
queue_free()
if in_contact_with_player and not has_been_absorbed:
scale -= self.rate_of_absorption
player._handle_absorb_light(Vector2(rate_of_absorption / 2))
# emit_signal("transfer_to_player", self.rate_of_absorption)
if scale <= Vector2(0, 0):
has_been_absorbed = true
func _process(_delta):
get_absorbed() # I guess I call this on every frame?
Originally, I was going to dispatch a signal whenever the player entered one of these areas to tell the player to get larger while the scale shrank for the piece of light. You can see that I left a comment in the above code sample where I tried that. I think I either got confused by signals or decided it was unnecessary and instead I pull the player node directly into this node and call its _handle_absorb_light
method.
So, I've maybe got some code smells, but I'm not going to dwell on it - my goal is to make a functioning game, not write the most idiomatic GDScript code (but believe me, the desire is real). I also am not sure if I should be calling the get_absorbed function on every frame. Oh well.
It seems one of the ways to get things done is to let go of the desire to do all things right.
Connecting the Compass
After finishing up the light absorption mechanism (for now) I started on prototyping a compass. This was particularly challenging because I'm not sold entirely on the mechanism of having this compass taking up a chunk of the bottom of the screen, nor do I believe I'll have it working nicely on the first try. All said, I ended up doing the following:
- Prototyping a basic (and mysterious!) compass in Affinity Designer
- Create a new GUI node/scene combo with a
TextureRect
node holding the image of the compass - Finding resources on detecting input touch in Godot and converting it to a directional value
- Converting the degrees to radians and applying that to player movement
All in all it seems pretty janky and difficult to move the player - at least to change directions smoothly. At the moment I'm downloading the SDKs to enable exporting Android builds - if I'm lucky I can skip a bunch of export-plumbing and just send an APK over to my phone quickly to start testing physically (simulating finger touches with a mouse is not super intuitive). Check in next time!
Part 3: Android Export (February 15, 2022)
Let's talk about setting up exports for Android and some mistakes I've made along the way.
I left off with the realization that I had to do some plumbing to get Visitor working on a physical device so I could test some touch detection. I googled around and found this link in the Godot docs on setting up the Android SDK. Thankfully I can (hopefully) just download the CLI tools and not the entirety of Android Studio (I just removed Xcode from my computer temporarily to free up some space and saved over 30GB).
After a short break to play a game of Duel, I returned to test the exported APK. It was rather broken, which is the second time I've (re)learned that I should be testing on device as soon as possible while developing. Ah well.
So - the APK was utterly broken, which led me to ask if it was possible to do live builds on device (which I was 99% sure was possible). Sure enough, once you enable debugging/development mode on your Android, you can build directly through Godot.
Now, instead of exporting an APK, it seems that the Godot build is actually somewhat working. Unfortunately, everything is very janky. I was able to fix the aspect ratio, but things like the navigation are quite slow and the star generation seems to be quite non-performant.
Looks like I'll have to revisit the following in the near future:
- Simplifying the compass mechanism (I'm thinking a single triangle/nav point representing direction is better)
- Fix janky performance - likely caused by the starfields, which I suspect are slowing things down
- My algorithm for generating new starfields is mostly working, but every now and then the player reaches a boundary and the starfield hasn't generated yet
Part 4: General Tuning (February 20, 2022)
Up next: performance issues!
First, I had to fix the aspect ratio, which was broken. This meant changing scaling in Godot's project preferences (Project Settings > Display > Window > Stretch > Mode
) from ignore
to expand
.
Then, I had to lower the number of stars that were generating - I was probably generating between 100-300, if I remember correctly, which was causing the device to slow down a fair bit. After reducing the count to between 10-30 per starfield, things are moving much faster. I like how it looks more too.
Improving Navigation
The compass idea I had originally thought of did not work. I realized that a) the design was too cryptic, b) it took up a fair bit of space on screen and c) it made navigating pretty unintuitive.
I fixed all of the above by switching to a simpler nav point, kept at the top of the screen. When a player taps on the screen it lights up to indicate that their character on screen is moving through space.
I also had to update how rotation of the nav point happened, which ended up being less code than I thought:
func _input(event):
if event is InputEventScreenDrag and Global.get_player_can_move():
var drag_val = clamp(event.relative.x, -10, 10 )
compass_texture.rect_rotation += ( drag_val )
Before, I was clamping values quite a bit and it was quite difficult to rotate the compass. This feels much smoother and more intuitive.
Adding a Beacon Effect to the Light Nodes
I was pleased with this one - I got to do some basic animating and it was satisfying to see some motion in the game. One part of the game's mechanics is to "collect light" - however, these inert light sprites were just spawning and sitting in space without any indication differentiating them from the stars (or the player). By adding a duplicate sprite of the light underneath the original, and using the Tween node, I was able to build a basic animation of a beacon.
This wasn't difficult (nor revolutionary) but it made quite a difference. One thing I learned that was tricky, was that the AnimationPlayer node would have worked great for this, but because the nodes of light are spawned at random locations, I was unable to link the AnimationPlayer changes to where the nodes were spawning. In the end, the code for tweening animations looked like this:
func _animate_light():
tween_node.interpolate_property(pulse_sprite, "scale",
pulse_sprite_tween_scale_vals[0], pulse_sprite_tween_scale_vals[1], 2,
Tween.EASE_OUT, Tween.EASE_IN)
tween_node.interpolate_property(pulse_sprite, "modulate",
pulse_sprite.modulate, Color(1, 1, 1, 0), 2,
Tween.EASE_OUT, Tween.EASE_IN)
# -- HACK --
# this is a noop tween so that we can have a "pause" before the animation resets.
tween_node.interpolate_property(pulse_sprite, "position",
pulse_sprite.position, pulse_sprite.position, 3,
Tween.EASE_OUT, Tween.EASE_IN)
tween_node.start()
# --
func _on_Tween_tween_all_completed():
pulse_sprite.modulate.a = 1
pulse_sprite.scale = pulse_sprite_tween_scale_vals[0]
_animate_light()
Scene Changing Issues on Android
This bug was discouraging me for a while. I followed some of the docs on changing scenes in Godot. It worked well on the render window, but was not working on Android. After googling I found several reports of people saying that Android has case-sensitivity to loading resources. All the issues reported were due to file names having a space in them - but none of mine did. Eventually I found that one of my assets had a space in it, so I fixed that. I think that might have fixed it, but if I remember correctly, I might have had to change the scenes to all lowercase too. At this point I don't remember, but at least I know that Android is more particular than Mac.
const scenes_map = {
Scenes.Title: "res://src/title.tscn",
Scenes.Game: "res://src/game.tscn",
}
Adding Titles
I used what I learned from the tweening of the light nodes to add some motion to my titles. Now, when a player starts the game we are met with a title describing a part of the game. After, I was able to wire up the player code so that when it reaches a certain size it will trigger the next "epoch" of the game.
What's Next
Next, I would like to fix a frustrating bug where the game opens, the sky renders all of the starfields, but all the stars themselves are not visible. It's a frustrating one because the bug seems non-deterministic - it happens, albeit rarely, when I start the game (both in Godot and on the Android).
Other than that, I'll make some stylistic changes next, I think, as well as start planning out the NPC interaction and dialogue (the other main mechanic of the game).
Part 5: Setting Up Dialogue (February 25, 2022)
It's been a little while! I've been busy with work and trying to find a new apartment, but I have managed to find some time to hack away at this game. If you don't know, Visitor is a game I'm building. This post is a general update, along with some notes on getting NPCs and their dialogue into the game.
Exceeding Deadlines
I gave myself a deadline until the end of March to build this game. For the most part, I will probably move on to other projects that I want to do, but come back to this one when I feel inspired or need a shift. I feel like I'm getting closer to being done, but I still have a ways to go. So, what's left?
Dialogue
I have a lot of dialogue to write. I've probably written seven or eight interactions with characters that can occur through the three parts of the game. Dialogue gets written in org-mode on Emacs, then gets put into Godot using Dialogic. Dialogic is a UI-driven plugin, which means that I'm dragging and dropping characters, text bubbles, questions, and if/end conditions to build the dialogue. Since I'm not using a data-driven approach I have to do some manual work, which is fine. Under the hood, Dialogic does use JSON, so it's very well possible to build my own scripts that would convert say, a spreadsheet of dialogue into Dialogic, but it would take longer than manually inputting the scripts at this point.
All in all, Dialogic is very nice! It took minimal fiddling out of the box to get it installed and working. Super impressive! Kudos to the developers.
NPC Spawning
I struggled with getting NPCs to spawn generatively. The NPCs only have one piece of state so far (how many times they have interacted with the player).
First, I tried using the "Starfield" system (described in Visitor 2 - Sky Generation) in which each "coordinate piece of sky" would be responsible for an NPC, just as they are currently responsible for spawning pieces of light to be absorbed. This worked at first, but I later realized that the NPCs need to live independently of the starfield they happen to be spawned in. This is because the starfields are continuously destroyed and recreated to create a "treadmill-generation". In my first attempt, any time a starfield was removed with queue_free
would mean that the NPC would also be removed (and their state lost).
So, after a refactor, I decided that whenever a starfield was created, it would pull an NPC out of a global queue held in a singleton, and be placed randomly based on the starfield's position. So long as that NPC is "in-play" we cannot spawn it again, but future starfields can spawn the next NPC in the queue.
So far this works with the two NPCs I've created and their respective dialogue scripts. I've got seven or eight more NPCs' dialogues written, so if all goes well they will happily spawn into place.
What's Next
I'm still running into a bug where periodically the sky/starfields are not generating at boot. It seems that if I wait before pushing the "start game" button, they are more likely to appear, but this isn't always the case. I'll try debugging this by printing when the generation is done the first time to see if it's just taking a long time computationally, and if that doesn't help, I'll dive into the weeds to see what else might be happening.
Most of the coming work will just be grunt work of getting dialogue into the game and testing. After that, it's onto writing the music, and maybe, maybe, if I have the energy, wrangling people to do voice recordings for NPCs (à la Hollow Knight).
General Game Development Thoughts
I've had a scattering of thoughts about game development from this experience:
-
Technically - try and make things stand alone. It has taken me a while to grasp how Godot scenes should live and work in relationship with other scenes / singletons. This happened in the last game I tried to build, but I would end up only really being able to run the parent "Game" scene to test things. I'm wondering if it should be the case that scenes should be able to stand alone a bit more. I imagine that might help reduce a certain amount of bugs.
-
Building a game takes a long time. So, try and find a part of it that you can get particularly excited about. I don't think it's wise to expect that every part will maintain the original "thrill of starting" throughout; rather, I've found that the grinding parts need to have accompanying thrilling parts to keep me motivated to the end (I mean, the project isn't done, so I'm just hypothesizing at this point.)
-
Test compilation on physical devices / your target platform as soon as possible. Nothing gets you discouraged like working hard to get everything running locally and then testing on your phone only to find it looks all squishy and out of sorts.
-
An important one: know your limitations. Yes, I could sit around and wish for the ability to make a more aesthetically pleasing or expansive game, but I don't have the artistic skills or external help for that (yet). For now, what I'm making is what I can do. I'm more confident than I have been in the past that this is the most important thing to help me finish this game.
That's all I've got for now. I'm looking forward to the next post, as I think it'll be a bit more exciting once the characters are in place and I can start thinking about how I want to do the music.
Thanks for reading!
Part 6: Final Retrospective (April 1, 2022)
Welcome to another post in a series of developer logs about the building of the game Visitor. At the beginning of the year I set out with 4 projects, to be worked on for 3 months each. My first was the game Visitor, which has just been an idea in my head for about a year leading up to 2022. It has officially been 3 months, so let's get to the answer:
Can I build a game in 3 months?
No.
But almost, yes.
What Do You Mean by "Game"?
I use the term "game" pretty loosely. To other indie game enthusiasts (and especially solo-game-devs) I don't think I need to qualify how broad the notion of "game" can be. Needless to say, my goal to create a game in 3 months was to create a publishable artifact that was accessible and playable by other people in the world.
At the end of three months, I'd say I'm about 75% done (and of course, they say the last 10% is the last 90%, so in reality, there's still a long way to go). I'll write a bit about the self-imposed limitations I had before we get into the "how far did I get?" section.
Why the Time Limit?
Why set a time limit of 3 months for a project? Isn't that too rushed? Your projects are probably going to take longer than 3 months?
Those are good questions. I'll start with the last one.
It's All About Quantity
As with my journey in learning how to draw and paint, I believe that I have to make a lot of bad stuff, so that I can get gradually less bad, and then eventually be good. I've decided that, for my personality, the best way to execute on the things I want to make in the world (the things I really daydream about, not just small projects) is to build up to them through a series of increasingly more challenging and stimulating projects. There are a few reasons for this:
- Finished projects give me more motivation and inspiration to continue than unfinished projects.
- I can avoid deathloops, as Derek Yu puts it so well.
- Starting small means I know how long certain things take, which help me budget (time) for the more complex things I'd like to make.
- Quantity over Quality has been discussed numerous times in other blogs, especially in the relating of the parable from the book Art & Fear.
Limited Scope Creep
A real deadline means learning what I will have time to make and what I won't. While I'm still learning some processes with game development (exporting is especially brutal), I can more easily shut down ideas.
I'm sure connoisseurs of game development could point out that I'm limiting what my game is capable of being if I don't make time to build X mechanic, or add Y to the story, etc. This is likely true, but there are so many things to learn about building games that I'd rather let a game be entirely too simple and lackluster in order to learn what you learn from completing a game.
OK, But You Didn't Finish the Game?
You're right, I didn't. More on that in the section below.
Why Stop Now?
Well, I want to follow through on my project plan, which means Visitor now has to take a back seat to other projects I want to build. If the next project is a total dumpster fire, I'll come back to it, but otherwise, Visitor will be a putter project I come back to when I'm inspired for short bursts. I certainly intend to finish it. If you go back and read the new year's post I link at the beginning of this post, you'll see some hesitancy in the framework I've worked up for myself - but I want to stick to it. The whole point of the 4 projects over three months each was to help me assess how I work. I have no problem with the way I've worked on projects in the past; I just wanted to try something new to see what I learn.
Getting Space from a Project
Getting space from a project seems like a good idea to me. This might be my way of making myself feel better that I didn't get the project done, but at the very least, similar to painting a painting, you often have to step back or step away for a time and then come back to see what needs work.
What Did I Learn / Have Yet to Learn?
I've covered a few of these in earlier posts, if you've been following along.
- If you're testing/working on building for hardware (in my case, Android), test, test, test on it. Don't just assume that what works in the editor renderer will work on your hardware (seems obvious, but I keep forgetting this, so I'm writing it out again)
- If your Android build is suddenly not working... you may have a renamed file issue. Android is surprisingly difficult and sensitive to how files are named. Beware of this.
- I still can't quite wrap my head around using signals in Godot when I could be using singletons. Either way, I haven't built a large enough game to see the benefits/downsides of either approach.
How Far Did I Get?
This was a screenshot from my first post.
Here's what's left:
- Music and sound
- Configure NPCs to have different behaviors based on their "storyline"
- One strange bug
- Proper end screen/end game state
I'm pleased with how far I got and I'm looking forward to returning to this project. The music I might be able to make from a detached perspective, which might be a nice way of "continuing to work on it" without really working on the project. I do feel at a loss, in some ways. I feel close to being done, and now I'm stopping?
I guess it's all part of the experiment! Keep an eye on the blog to see if I stick to my resolutions.
See ya next time, WT
❦