14 Lessons I Learned While Making A Video Game


14 Lessons I Learned While Making A Video Game

It's fair to say this game has created something of a buzz.

APICO is a laid-back beekeeping sim game released in May 2022, created on the GameMaker engine. Ell (One half of the game's development team, TNgineers) has written a technical post-mortem on finishing a game in GameMaker. 

It's Ell's hopes that maybe you won't run into some of the same trouble they did and maybe you'll be inspired by some of their solutions.

Hi! My name is Ell, developer of beekeeping simulator APICO, which is built in GameMaker! 

Five months ago, I talked about how we moved the game from the original engine (custom JavaScript) to GameMaker and some of the things I learnt on the way.

Now that the game is finished, I thought it'd be cool to revisit everything in a sort of GameMaker technical post-mortem and talk about some of the things I did, some of the weird things I had to do for workarounds, things that I wish I knew from the beginning of the project, or had time to do.

Disclaimer: Although I have been a software dev for many years, I'm not an expert with GameMaker by any measure - I only started using GameMaker in January! If you read this and at any point think "but wait, couldn't you have just done X" the answer is... yes, you're probably right lol

undefined

APICO is a casual game about breeding, collecting, and conserving bees!

Organisation

You probably already do this as it's in every single GameMaker tutorial ever, but use some consistent naming practices, and give each "type" of thing it's own name, i.e. all music is labelled "mu_trackname" or each sprite is labelled "sp_coolaxe" to make it easier to identify what everything is.

I took this a step further with objects, split between "fx_" (for effect objects), "ob_" (for 'class' objects), and "co_" (for controller objects).

Lessons learned from a video game

folders in APICO

I did the same thing with scripts. All "sc_bee_*" scripts are to do with bee related stuff (making bees, mutating bees, getting trait buffs etc) and with my custom event hooks (see "Script Hooks" later on).

This way, it's super clear where logic is mostly like going to be found, especially for others who might need to look at your code.

I'd also recommend looking into build configurations along with some macros so you can setup things like "dev" builds that automatically turn on global values. Matharoo has a great video of using configurations and macros.

I'd also also say try and keep all your globals in one controller object, but we all know that never happens! I did pretty well keeping them in the one controller object. In the final few months, I needed to get stuff done and now there are globals in a bunch of controllers.

But hey, it happens - don't beat yourself up about it!

Data Structures

When I first started moving the game from HTML, I was just using some GameMaker docs (an old set of docs I later realised!) to help me find all the stuff I needed. I knew what I wanted to do, I just didn't know the right words for things I needed!

For data structures, I was using a lot of ds_lists and ds_maps, which was a bit weird to me, coming from using a lot of JavaScript (later, I found out you're supposed to clean them up after using them. Woops). 

After a few months, I found out about structs and arrays and replaced every ds_* in the entire game with them.

Lessons Learned From a Video Game

the "stats" of a bee, as a struct ft. some legacy ds_map files :')

I would definitely recommend everyone uses structs and arrays instead of ds_maps and ds_lists. There wasn't anything I came across that couldn't be done with them, with the exception of a sorting function that I used a ds_grid for. 

The added benefit is you don't have to worry about memory issues due to forgetting to destroy your ds_* when you're finished with them (which you appreciate more as your script count grows). I think it's nice to be able to use some of the "normal" conventions you're used to from other languages for accessors (like arr[0] instead of list[| 0]).

For saving and loading JSON files, please remember that json_stringify() and json_parse() exists! 

If you use them instead of json_encode() and json_decode()you can work with structs/arrays instead of ds_lists/ds_maps as a result. Although I came across structs and arrays early on, I didn't come across these functions until a lot later, so the main file save and load system is still 'stuck' on using ds_maps. 

For future games I would pretty much just use structs and arrays from the start everywhere.

Async Files

Speaking of saving and loading files, I would definitely recommend setting up your file system to save/load files using the buffer_save_async() and buffer_load_async() functions from the start if you have any plans of potential console ports.

By using the async functions, you're not only getting into a good habit of running async events for larger file reads, but also consoles require you to use the async methods for loading files (you can't be hanging the thread basically).

Having to move your file system over mid-project to async for consoles is a total pain, so it's worth doing from the get go.

Lessons Learned From a Video Game

Basic async saving of a JSON file from a struct

Buffers are not as scary as they might seem. They're just a little box for you to dump stuff in! 

I ended up just making a single helper script that could be given some data and a location and it'd do the rest and callback to a single save controller object to handle any routing/loading messages.

Doing this is also good practice for learning the async events in general, which you'll most likely come across for other things anyway like HTTP requests.

Saving Big Files

On a similar note, for APICO, a save file is the entire world file in JSON format. As the game got bigger (more biomes and bigger islands), the save function was getting noticeably "slower" in the sense that it would hang the game for a second or two.

Obviously, this was a bit of a dead end, as I can't change anything about this built-in function. What I did instead is built a special save object that slowly created the JSON string with alarms bit by bit. 

This way, we don't hang the thread at any point as we're only stringifying small amounts of data at a time, rather than the entire world, and then dumping the whole thing in a buffer to save it.

Lessons Learned from a Video Game

Each "step" is staggered one after the other, building the raw save string as we go

This meant the save took an extra second or two because we were staggering the string building by .1s for each section of the save file, but it meant that you could just carry on doing whatever you wanted to do as the game saved without feeling like it "lagged".

Player As The Camera

In APICO, we don't actually have a camera object - something I later realised was a thing people seemed to do very often in GameMaker tutorials.

All we do is set the viewport based on the current player position so that the player is always dead centre. This is something we wanted specifically for APICO, because you can reach pretty far. It doesn't matter too much where the player actually is - we just want to give a good view of everything around you.

undefined

damn those lil arms got some reach

However, having no camera object came bacl to bite me in the ass a bit later, because I wanted to build some little animation points where we move the camera away from the player to show something else. This meant I had to add some workarounds to update the viewport separately to override this.

Lessons Learned From a Video Game

Setting the camera position based on the player and clamping to world boundary

I'd say it doesn't hurt to have a camera object and doesn't cost anything, so just chuck one in. That way, if you do need to move the camera to show something else, you don't have to add in some weird workarounds later on!

Child Objects

Maybe about halfway through the project, I realised child objects were a thing - and hoo-boy did I go crazy with them. 

Although it just looks like a stupid game about bees, APICO is a pretty big game and we actually have a lot of instances in the world. Worlds are 350x350 tiles (a tile being 16x16), so when a game loads, we're dealing with about 10,000 instances that get de- and re-activated as you move around the world.

Lessons Learned From a Video Game

activating areas as we walk

These are things like generic objects (shrubs, rocks, crystals, furniture), menu objects (beehives, apiaries, sawbenches), trees, and flowers. 

These were all split out to make certain things easier. For instance, the flowers are a seperate object, since flowers can do a bit more compared to generic objects, but also bees onscreen need to be able to find them, and it's quicker to do "with (ob_flower)" that do "with (ob_generic)" and filter out the flowers.

When I found child objects, I realised I could be doing just that with a bunch more stuff to make things quicker! 

At one point we had a lot of lag from the light rendering. First, we just looped through ob_generic, filtered by objects marked as lighting, then drew the lights. This ended up using more step in the profiler than I would have liked, so instead, I made an array and stored objects marked as lighting when they were created. 

This was quicker at first, but then there was occasionally a crash when we tried to draw a lighting object that had just been deleted (i.e. the player picked it up). To avoid this, we then used instance_exists(), which was then using up step in the profiler again by checking the existence every draw frame.

Lessons Learned From a Video Game

All 3 obj props here are cached vals, as mentioned below!

By using child objects, I could just set all the objects as ob_light instead of ob_generic when they were created, and it meant I could just use "with (ob_light)" to loop through a much smaller list without worrying about filtering or checking for instance existence.

It can make your life a lot easier if there are things you constantly filter for that could just be a child object. You don't have to write any extra logic for them, since you're just utilising the fact that you can now target that object using with().

Step/Draw Events & Caching

Step and draw events run every single frame. The code in your step event is run every single frame. The code in your step event is run 60 TIMES A SECOND.

Look at the code in your step event. Does that logic need to be run 60 times every single second? Chances are that unless it's tied to something visual (i.e moving an object position smoothly), the answer is a big fat no.

If you need some sort of constant logic to run (like say our Beehives slowly ticking down lifespan and looking for flowers), I'd recommend just using a looping alarm set to 0.1s - it'll be quick enough to get the updates you need, but only run 10 times a second instead, which will help speed things up in your game if you have a lot going on.

Lessons Learned from a Video Game

the only things in our menu step events are all related to stuff we need to update every frame or it would "lag" visually, like positions and animation curve vals

It's also worth looking at things you define in your step events (or fake alarm step events). 

What are you defining or what values are you retrieving from scripts that actually don't change, or don't change very often? There are always things that you could instead be caching on the object itself to save calling the same thing every time.

The same applies to draw events - I don't need to use asset_get_index() every frame to get a sprite if it doesn't change often; I can just set the index as a property and use that property in the draw event. 

When the sprite does need to change, you just update the property instead. It sounds simple enough, but there's guaranteed to be things you missed and going through both those events with the idea of "do I need to do this 60 times a second?" really helps to justify things.

You don't need to do this off the bat, but it's certainly easier to have taken some time to think about it and setup some cached properties in advanced, or use a fake alarm step from the start, rather than having to change things later down the line.

Script Hooks & User Events

As mentioned before, APICO has "menu objects", which are overworld objects you can click on to see a menu. This is one of the main parts of the game - we're basically just a big ol' bee themed inventory management game!

Lessons Learned from a Video Game

pls organise ur stuff better Ell

For these menu objects, I didn't want to make a seperate object for each menu object in the game (about 60 of them!) as I felt like it would end up being a lot of management to have all these objects with the scripts separated out. 

What I did instead was make one "menu object" obj that would act as the template, and in the scripts of this obj, I would call the various "hooks" I needed when I needed them (i.e. a draw hook during the draw event, or an alarm hook when an alarm is called).

With this setup, I could just have a single script file (which funnily enough I called "events" before realising User Events were a thing) with all the hooks I needed for a given menu object. If there was no script found for a given hook for a menu object, it wasn't called, but if it found one, it would run the logic there (eg when the menu object instance is created, we check to see if these hook scripts exist for our type of menu, and if they do, store them on the menu object to be called later).

Lessons Learned from a Video Game

All the hooks used by the "anvil" menu object

This meant for any given menu object, I had every single bit of logic for that menu object in one script file. If I need to change something with a sawbench, I know that everything I need to change will be in the "ev_sawbench" file. It also meant I could have a bunch of hooks not in the options for GML object scripts, eg I have a hook for when a menu gets moved around, or when a menu is "opened".

Towards the end of the project, I did noticed that custom User Events were a thing, so I guess I could have had seperate menu objects that were a child of the template menu object and used custom User Events to write the different hook logic. However, I feel like I would have missed out on having that single file maintenance, and lost the ability to have it explicitly clear in the code what hook I was calling and what it does, but I couldn't tell you which option is better!

Modding

From the beginning we always wanted there to be mods in the game - for a game inspired by mods, it was only fitting!

How to actually implement mods was a problem that future me did not appreciate - after a few different ideas, I ended up settling on YellowAfterLife's Apollo Extension, and honestly, I can't recommend it enough if you want some advanced scriptable modding functionality for your game (<3 u yal).

With Apollo, you can let people write LUA scripts and load/run those scripts in GameMaker. You can also inject your own functions from GML, so people can write LUA code that calls functions in the game and vice-versa! 

With this, you can write your own Modding API to expose all the functionality you want modders to be able to play with.

Lessons Learned From a Video Game

You can check the full API docs at https://wiki.apico.buzz/wiki/Modding_API

Once the APICO Modding API was setup through Apollo, I then used mod.io to handle the actual mods itself so it could be cross-platform and also cross-PC (not just Steam only).

I'd definitely recommend checking out mod.io as a platform for your mods, as it did makes things a lot easier to manage, and it was easy enough to implement in GameMaker with basic http_requests(). 

It also let us have an approval process for mods, which is important since the content rating for the game is for kids, and potentially risque or inapproproiate mods can be downloaded veru easily. You probably know that consoles can be brutal and we're taking no chances.

Lessons Learned from a Video Game

Downloading and loading mods in-game

If you're interested, I wrote a three-part set of dev logs around adding Modding which you can find here, here and here - they're all far too long to put in this already too long post!

Outline Shader

I always find it interesting when games have some straight-up weird choices, and you always wonder why the game still has it.

One of those things for APICO is the highlighting - if you highlight anything in the game, it has a nice little white outline and you get a little tooltip with a bunch of info in it.

undefined

You can see the highlight as we paint over the objects

However, this is actually another sprite being drawn! 

Every single object in the game has a highlighted variant and I mean EVERY object. This is a throwback from when the game was built in HTML as everything needed a highlight sprite, since you couldn't do anything cheaply to make outlines on canvases. When I moved the game over, I kept this system as I didn't know any better on a way to do it.

Lessons Learned from a Video Game

yeah...

Towards the end of the game, I'd written a bunch of shaders (night time, dawn/dusk "golden hour", water reflections, player palette swap) and realised I could have just done an outline shader to render these outlines from the sprite. Whether it's quicker to do a shader draw call vs just the sprite drawing, I don't know, but it certainly would have cut down on our Texture Page sizes massively which can only have been a good thing.

9 Slices

On the same sort of note, every single menu is actually it's own sprite!

When I started building the game in GameMaker, 9-slicing didn't actually exist yet, so I built the system as I had in HTML (i.e. each menu having a unique sprite) and re-used all the menu sprites we already had. Let's just say I was a little miffed when I saw 9-slices had been added and I had already built the system around drawing the sprites and added like 40 menus.

Lessons Learned from a Video Game

Using a 9-slice to make a bigger shape

For future projects, I would 100% just use 9-slices instead. Every single menu would just be drawn from the same 9-slice, and slots could just have the UI sprite drawn in their own draw cycle (as they all have a draw cycle anyway for slot highlights and item drawing). 

It would have removed 60+ sprites that each have a bunch of frames for outline stuff (as mentioned above) - so I'd definitely recommend looking at using it!

Lessons Learned from a Video Game

:')

I could have also done the same thing with some of the progress bars, all of the buttons, and a bunch of other UI elements. At this point, though, I'm not gonna risk breaking a bunch of stuff that works and runs fine - sometimes you just gotta live with this stuff!

Localisation files

Put all your hardcoded text and speech into files from the beginning of your project.

"Oh but I can do that later o--" Just do it. For the love of God, please.

Otherwise, at some point down the line, you'll need to check every single script in your entire game for any hardcoded text and move it into a file so that localised text can be dynamically used instead. By that point, your game will be a unholy behemoth with hundreds of scripts and it will be an absolute nightmare.

No I'm totally not speaking from experience - why do you ask?

Sequence Builder

This was an interesting case of, "hey there's this cool new thing I'd love to try out but I literally have no time because I need to finish this game by yesterday".

In the game, we have these books that show little GIFs to the player to help with specific gameplay mechanics and act a visual learning alternative instead of reading.

undefined

gif of a gif, nice

When I added the books in the game, I thought that Sequences sounded like the perfect thing to use. I'd be able to make the little scenes and just render the one I wanted when the book was open.

However, Sequences were pretty new and there really wasn't that many good resources for them. I had about 50 or so GIFs to create - I really didn't have time to learn a whole thing first by trial and error when I just needed to get stuff done. I would love to have learned them as I've seen people do some really cool things with them, but sometimes you just don't have the luxury.

What I ended up doing is just drawing out the GIFs frame by frame!

undefined

This might sound nuts, but in the GameMaker Image Editor, it's actually pretty easy thanks to the layers.

I could just draw out one scene, clone the frames, and move the elements bit by bit to create the GIF. The downside is that I think two entire texture pages of the game are dedicated to GIFs, which Sequences would have cut out completely!

Upgrading

This one's more of a warning - don't update your build or IDE mid-project or towards a deadline.

Although the build might be "stable", there are still a bunch of things that can go wrong (welcome to software dev). Although the GameMaker team do their best, it's literally impossible to come across everything in the beta testing.

You should always be aware that there might be an issue in the new version that may cause a problem with your current workflow and only update if you have time to handle that issue or revert back a version. 

An example is one of the IDE updates broke the Image Editor, which I rely on pretty heavily (and that day I just happened to need to upload 60+ bee sprites). Another version slowed down the IDE on Mac, so I had to restart it.

In both cases, I just reverted back an older version, so it's not the end of the world, but it's something to keep in mind.

Random Stats

If people are interested on some stats, here's some numbers:

  • 1700+ hours spent in-project (since 14th Jan 2021)
  • 600+ scripts, covering ~ 44k lines of code (quantity != quality tho ofc)
  • 500+ sprites with all the extra frames as mentioned above
  • 50+ objects, 13 of which are controller objects
  • 7 tile sets and 8 tile layers
  • 1 room (lol)

Questions

These were all the main things I thought about as I was building the game. Hopefully some of it was useful or interesting to read!

If anyone has any specific questions or wants to know how I did something (or didn't do something), reach out to me on Twitter - I'm happy to answer any questions! :D

If you're interested in the project in general and want to follow along, you can catch the game over on Twitter, and we also have a Guilded for chat/devlogs/forums/cute pics of bees.

Totally shameless promo too but you can also wishlist APICO in Steam if ya like! There's also a demo on Steam you can download if you want to give APICO a go.

Thanks for reading!

~ Ell


Written by Ross Manthorp

Ross Manthorp handles all things community at the GameMaker team. When he’s not pulling the strings from behind the scenes he’s enjoying Nintendo games, indie games, and getting emotional over cartoons and comics.