Lessons Learned From Finishing a Game in GMS2


Lessons Learned From Finishing a Game in GMS2

It's the upcoming game everyone is buzzing about. APICO is a laid-back beekeeping sim game which has already captured the attention of over 9000 players! The game has recently been finished and is due for launch in 2022. Ell (One half of the game's development team, TNgineers) has written a technical post-mortem on finishing a game in GameMaker Studio 2. 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! I recently just finished the Gold Master build for our game, APICO, which is built in GameMaker Studio 2 and is due to release early 2022! 5 months ago I posted here to talk about how we moved the game from the original engine (custom JavaScript) to GMS2 and some of the things I learnt on the way.

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

I've split things into different sections but there's no specific order here as I just jotted things down as I built the game. Feel free to ask me any specifics of how we approached something!

Disclaimer: Although I have been a software dev for many years I am not an expert with GMS2 by any sort - I only started using GMS 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 are 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 GMS 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).

undefined

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 + 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 and then the last few months needed to get stuff done and so 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 GMS 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 fine but coming from using a lot of JavaScript it was a bit weird to me (and 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 pretty much replaced every ds_* in the entire game with them.

undefined

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

I would definitely recommend for anyone to use 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 do not have to worry about memory issues due to forgetting to destroy your ds_* when you are finished with them (which you appreciate more as your script count grows), and I think it's nice to be able to use some of the "normal" conventions you are used to from other languages for accessors (like arr[0] instead of list[| 0])

For saving and loading JSON files please know 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 + arrays from the start everywhere.

Async Files

Speaking of saving and loading files, I would also 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 vague 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 as you have to handle things a lot differently so it's worth doing from the get go.

undefined

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. Instead what I did 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.

undefined

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 GMS 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 so 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 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.

undefined

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 are dealing with about 10,000 instances that get deactivated, and then activated as you move around the world.

undefined

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 as we have a few extra bits of functionality that flowers do 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! A good example of use was at one point we had a lot of lag from the light rendering.

At 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 what I did is 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.

undefined

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.

There were quite a few cases of this and it's definitely something to think about as 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 you're just utilising the fact that you can now target that object using with().

Step/Draw Events & Caching

I mentioned it in our original post but I think it's important enough to say again - 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 being run (like say our Beehives slowly ticking down lifespan and looking for flowers) I'd recommend instead 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 up things a lot more in your game if you have a lot going on.

undefined

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 say asset_get_index() every frame to get a sprite if that sprite 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 is 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.

I would say that 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 basically overworld objects you can click on to see a menu. This is like one of the main parts of the game, we're basically just a big ol' bee themed inventory management game haha!

undefined

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. Instead what I did is 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, say 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 then it wasn't called, but if it found one then it would run the logic there (good example of caching here, 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)

undefined

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, like 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 see 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 something left as a future 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 GMS. 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.

undefined

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 GMS2 with basic http_requests(). It also let us have an approval process for mods which is important given the content rating for the game being for kids and how easily mods are downloaded - as you probably hear consoles can be brutal and we're taking no chances.

undefined

downloading and loading mods in-game

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

Outline Shader

I always find it interesting when games have some straight up weird choices and you always wonder what legacy reasons the game still has it in for!

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 lil 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 as you couldn't do anything cheaply to make outlines on canvases. When I moved the game over I still kept this system as I didn't know any better on a way to do it!

undefined

yeah...

Towards the end of the game I had 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 GMS2 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...

undefined

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!

undefined

:')

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 it's one of those things where we're too far gone now and I'm not gunna risk breaking a bunch of stuff that works and runs fine when the game is soon to be submitted for console cert - 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 will 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 and by that point you 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, what makes 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 and 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 DoneTM. 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 GMS 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 2 entire texture pages of the game are dedicated to GIFs, which Sequences would have cut out completely!

Upgrading

So more of a warning one really here that I'm sure people know but it's worth saying - 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 YoYo team do their best it is 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 for after about half an hour so it ended up needing to be restarted.

In both these cases I just reverted back a version so it's not the end of the world, but just something to keep in mind as when you're doing builds for say console cert you don't want to be changing versions all the time!

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 at least 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 - 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 a demo on Steam/GJ/Itch and we're hoping to release early Q1 2022 on PC + Consoles <3

Thanks for reading!

~ Ell