Cultivation Meets Automation.
Factory Farming

I have written in the past about how rewarding coding under constraint can be, especially when writing code targeting neat little embedded devices. With You Cannot Go Back! for Pebble, the limitation was the watch’s 64k of RAM. But for the Playdate, the port of this game was trivial for the hardware to run. It wasn’t even breaking a sweat - but that was to be expected. We go from a Cortex-M3 to a Cortex-M7, and a far less extreme form-factor than a watch.
So, the next question was then - how to stress the Playdate?
Fire Up The Factories
Ever since Factorio took off, I have had a bit of an addiction to the factory simulation genre. And coding an efficient enough factory simulation in C which could run on embedded hardware looked to be an interesting challenge.
I decided to attempt a Factorio meets Stardew mash-up. The depleting resource patches in Factorio were never a mechanic I especially liked, though I understand their purpose in that game. Having crops instead nicely gets around this issue as they can grow produce repeatedly on a cycle, I also liked the idea that the player is tasked in deciding what to grow, and where. I then needed factory recipes, but as my inputs were to be freshly grown fruits and vegetables - these could be literal food recipes! I decided early on that each factory would present its input requirements as a list of ingredients, like you would see on the side of a packet of potato chips, for example. To keep within a corporate and capitalistic theming - the goods being produced would then become progressively hyper-refined and unhealthy (and profitable) as the game progressed.
I didn’t have a physical Playdate at this point, and wouldn’t for the vast majority of development. The game was almost entirely developed using the Playdate simulator and the x86_64 build specific for the simulator.
Initial Steps
You Cannot Go Back! was released mid May, the very same month I started to prototype Untitled Factory Game. Early prototype work consisted of learning the Playday’s sprite system, investigating animating sprites, and exploring the baking of large background image sprites as a more efficient way of drawing the game world than lots of individual tiles.
I had already settled on an toroidal wrapping world at this point (go off the top of the world and arrive at the bottom; go off the left and arrive at the right), and some basic code to handle wrapping is visible in this early recording - though note that the visuals of the conveyor belts did not wrap correctly here, this will only get fixed a while later on.
In Alpha 1 (May 18) everything, on-screen or off-screen, would be updated every frame. And while this was fine in the simulator, the on-device performance was reported to start to fall quiet quickly as the number of sprites ramped up.

Chunking It Up
An efficient simulator is depended on good data structures, and Alpha 2 was all about the back-end. The structure I designed for this game was focused on how to manage the game data for both factory rendering and simulation. I decided to go with a tile & chunk system, I knew that there would not be enough CPU to simulate the full factory at the game’s target 32 FPS (frames per second), but by dividing the world into chunks I aimed to simulate the chunks immediately around the player at 16 UPS (updates per second), and the ones further away at a more manageable 2 UPS.
Before we go further, let’s then define some terms which will be used frequently in the discussion:
- Tile: The smallest element of the game world, a 16x16 pixel square.
- Chunk: A rectangular sub-region of a game world, which manages a set of 12x7 tiles.
- Plot: A set of 12x12 chunks which make up a game world.
- Building: An entity which lives in on a tile, and which takes actions in the game world.
- Cargo: An entity which can be created or destroyed by a building, and which lives on a tile - but is also allowed to move between tiles.
In the following image, chunk boundaries are in blue, the tile’s background decorations are in green, buildings are shown in red and all cargo is drawn in yellow.

A key consideration when choosing 16x16 for the tile size was to be able see a good amount of the factory at any one time, as just watching all the cargo zipping around is very satisfying in itself. With 8x8 tiles you can draw a huge area of land on-screen, but graphics legibility is a major challenge with such few pixels to work with. Here’s a 2x screen dimensions snapshot of the final game which has been scaled down by 50% using a high-quality image resizing algorithm, and then dithered back to 1-bit fidelity. You can still make out the conveyor belts, but good luck telling what’s on them! Or what the factories are producing… This zoom level would have also completely blown the CPU budget for concurrent cargo sprite renders as well.

16x16 was the best compromise between graphical fidelity and the desire to see a large amount of the factory on-screen at any one time. So that’s 25 tiles on the horizontal and 15 on the vertical, given the Playdate’s 400x240 screen. I could have chosen a chunk size of 5x5, with five horizontal and three vertical chunks filling a screen, and with 25 tiles per chunk. But I considered this chunk size to be too small (you’ll see why in a bit), though if I could just remove one tile from the vertical and one from the horizontal, I could then have a chunk size of twelve tiles on the horizontal and seven on the vertical. Now only four chunks are needed to perfectly fill the screen, better! I also had to figure out what to do with the two black bars which are needed to cover up the remaining sixteen pixels, but this turned out to be an ideal place to locate the game’s persistent User Interface elements.

In the above image the Playdate’s screen dimensions are shown in red, chunk boundaries are in yellow and the black bars which got repurposed for the UI are in blue. We see that there are tiles from at most nine chunks visible on screen at any one time - the chunk the player is standing in, and the eight surrounding chunks.
I also wanted for the player to be able to zoom in and get a closer look at the detailed goings on, and if the game is rendering at 2x scale then we can see that there are at most four chunks visible on screen - the chunk the player is in, one chunk above or below, one left or right, and one chunk from a corner. All dependent on which quadrant of the chunk the player is standing in (North-West, North-East, South-East or South-West). In the image below the quadrant boundaries are shown with green dashed lines.

We also see here why the black bars are a necessity, as given where the player is standing here towards the southerly edge of the North-West quadrant, there would be an empty area at the bottom of the screen if it were not for the black bars masking this portion of the screen.
With the chunk size defined, the game can then pre-compute at start a list of pointers from each chunk to:
- The surrounding eight chunks .
- All other chunks except for the surrounding eight chunks.
These two lists are used when zoomed out.
We also need:
- The W, NW, N chunks, and all other chunks except these.
- The N, NE, E chunks, and all other chunks except these.
- The E, SE, S chunks, and all other chunks except these.
- The S, SW, W chunks, and all other chunks except these.
Where one of these four pairs of lists is used when zoomed in.
These list are pre-computed for very fast lookup as they are used as the base of the simulation. The lists of surrounding chunks are the “near” chunks, these are passed to the rendering pipeline and are simulated at a high-fidelity of sixteen UPS, the lists of all of the other chunks are the “far” chunks, these are simulated at a lower-fidelity of two UPS.
Thinking about the list of “all other chunks except for the surrounding 8 chunks”, we see now why chunks of 5x5 tiles would have been a bad idea. The final game is 12x12 = 144 chunks, each of 12x7 tiles, and each chunk needs a pointer (4 bytes) to every other chunk (for either the “near” or “far” list) for the zoomed-out simulation, plus equivalent lists for each quadrant of the zoomed-in simulation, so that’s 144 chunks, multiplied by 143 pointers, multiplied by 5 lists, that’s 412 KB of RAM used to hold all of this. An about manageable amount of fast look-up data for a device with 16 MB of RAM.
Had it been a similar game area but with 29x17 chunks, each of 5x5 tiles, then the lookup table would have ballooned to 4.9 MB, definitely getting on the less reasonable side of things.
Running On Chunks
Each chunk owns a 192x112 pixel bitmap which graphically represents the 12x7 tiles which fall under the chunk. A large number of individual sprites is expensive to render (we only want to deal with cargo on conveyors this way) and so as much of the world as possible is baked into this background bitmap. Conveyor belts (with the exception of animated conveyors), crops, factories, harvesters, soil, dampness, obstructions, paths, signs, etc., all of these are rendered once and baked into the corresponding background bitmap.
Each background bitmap gets re-drawn anytime something on it changes, and for some changes with wider effect such as placing a well (which dampens a 15x15 area) or placing a factory on a chunk boundary, multiple chunks need to re-draw their background image. In this example, four chunks need to be re-drawn.

In terms of data management, a building manager and cargo manger handle (respectively) the memory management and lazy initialisation of building and cargo structs with Playdate sprite objects (cargo need sprites as they move about the world, the larger 3x3 tile buildings need a sprite to provide a physics engine collider).
Each tile has a building pointer and a cargo pointer either of which may be null, or may point into these struct arrays. And hence there can be at most one building on a given tile, and at most one piece of cargo resting on or in transit through a tile.
Each tile will also register its building and any cargo up one level with its parent chunk, and any cargo which transition between chunks needs to both de-register from the previous chunk and register to the new chunk.
Every second of gameplay then comprises of 32 frames, where this breaks down further:
- On every frame the scene is rendered.
- On every even frame (2, 4, 6…) the player’s current chunk and all nearby chunks are simulated with a tick-size of 1.
- On frame 1 and 17 all far chunks are simulated with a tick-size of 8.
- On frame 3 and 19 the list of visible sprites in all nearby chunks is refreshed with the sprite system.
The refresh of the list of sprites with the sprite system also happens explicitly when the player crosses a chunk boundary, but we perform these extra periodic refreshes too in order to make sure that we start to render cargo which have been recently conveyed into one of the nearby chunks.
Alpha 2 was tagged on June 2nd.
Core Gameplay
With the core underlying framework in place I could start to iterate on extending these base structures and coding in the most critical gameplay mechanisms: the ability to plant crops, let them grow, harvest them, and to move the harvested produce around on conveyors.
The 7x7 area harvester building was added, along with splitter and filter conveyor belt variants. Lakes and rivers were added to the world generation, and the ability to sell cargo was implemented. A basic dev UI was added to the right hand bar, allowing for buildings and some test-cargo to be placed in the world. The bottom UI bar started to be populated with performance info, as well as details of the current tile.
Another huge feature for Alpha 3 was I/O. There’s not much interesting to write about here - but a massive amount of work was needed to implement and fully debug the ability to persist the world in a JSON format, and to restore from this. Any struct instances which own a transient pointer (e.g. a tile owning a pointer to a piece of cargo) save the index into the cargo struct array, such that the pointer can be restored once the array has been repopulated on load.
I made use of the new I/O features to bundle a world for easier on-device testing, at 16x16 chunks the simulator claimed 12.6 of 16 MB RAM was used, however it crashed on device. Alpha 4 reduced the world size to 8x8 chunks, but this felt cramped. The final released game goes half way in-between these two with 12x12 chunks. Alpha 4 was released June 19th.
This was the point that Untitled Factory Game became Factory Farming. The name came to me when I was half way hiking up the Jura mountains and perfectly fit the slightly dystopian and highly capitalistic theming of game, while at the same time being a direct reference to the key gameplay concepts.

The First Feature Expansion
The types of buildings available in the game was the next thing to expand. Explicit building sub-categorisations were created for: crops, conveyors, harvesters, factories and utility buildings. At least one instance type was coded in for each of these, the vitamin factory was the first factory sub-type, and the well was the first utility sub-type. The underground conveyor belt was added at this stage, this is an absolutely vital component for any reasonable-sized complex, and its omission up to this point was hampering my testing.
The shop was added, and along with it another important-yet-tedious coding endeavour to write the proper UI framework which would replace the simple development UI used up to now. New UI was added for the shop, the player’s inventory, to sell items, and for the building placement, destruction and item pickup modes.
Alpha 6 (5 was skipped due to a bug) came out on July 6th, another thing which was finally fixed at this point was the visuals of the world wrapping.

Wrapping The World
The basic mechanics of a finite - yet borderless - toroidal world like the one in Factory Farming have been evident since Alpha 1. The linking of the edges of the word is rather easy, the function which takes \((x, y)\) coordinates and returns a pointer to a Tile struct performs a modulus operation on the input coordinates to get the integer remainder when the coordinate is divided by the world’s size in \(x\) or \(y\). Add in some code to instantly teleport the player if they try to cross the world boundary and, from a mechanical standpoint, everything is done.
But then to handle the graphical representation of the world requires some further tricks, here we see a single plot with the top & bottom chunks highlighted in blue, the left & right chunks in green, and the corner chunks in red. Everything in the striped region is outside of the playing area.

The game keeps track of which quadrant of the plot the player is in and triggers an update every time they cross between quadrants. In this example the player is assumed to be in the North-West quadrant, outlined by a yellow box. We see that chunks from the bottom and right edges have been offset such that the world appears to be without seams visually as well as mechanically.

These border chunks are given appropriate an \(x\) and \(y\) offset variables depending on the quadrant the player is in. Then every time a sprite issues a moveTo command, this offset is added on to the sprite’s world coordinate. This way cargo can still be seen to be moving around on conveyor belts in this region of pixels which is technically outside of the play area. Buildings and their colliders are not shifted, as it is impossible for the player to walk into this area, as soon the player crosses the boundary they get teleported to the other side (and hence they change quadrant, and trigger an update to recompute all the border region offsets).
Expanding Horizons
Limiting the plot to 12x12 chunks (which is 144x84 tiles, 12,096 tiles total) was primarily due to the Playdate’s 16 MB of RAM, but I would find out later that 144x84 tiles is also a sensible choice given the Playdate’s 180 MHz ARM Cortex-M7 CPU, and how large of a factory it is possible to simulate before things start to slow down.
But I also knew that this wasn’t large enough for the scope of the game I was designing, there simply was insufficient room to build all of the multitude of factories I was planning on including in the game.
The solution was to add more plots, eight in total. A new plots manager lets the player move between owned plots, and to buy new ones.There is only enough RAM to have a single plot loaded into the Playdate’s memory, but it can also remember in the player’s JSON save file some key statistics about the other plots which are not currently loaded.
The game remembers the total sales value of all cargo sold in the other plots, this is averaged over the last two minutes that the plot was loaded. Hence the player can still earn income from selling items in plots which are not loaded (at least, in a statistical sense).
I also wanted the player to be able to move cargo between plots and to use this as a mechanic to drive gameplay choices, so exports and imports managers were added too. The exports manager remembers the average rate that each type of cargo was exported per plot, and the imports manager can then make use of this time-averaged stream of cargo by allowing for up to four import streams to be made in each plot (one for each cardinal direction).
For example, if 2 water/second were being exported from plot A, and 6 water/second were exported from plot B. Then in plot C the player could import 8 water/second from the North side of the import manager, or 4 water/second from both the North and East sides of the import manager.
You may also notice a first polish-pass in the jump to Alpha 7, including new player sprite & physics-based controls, a first pass over the game art and the inclusion of some juice (screen shake, though dialled way too high here!).

That wasn’t nearly all however, the game got its title screen, three save slots and save management. The saving (and later loading) was made asynchronous so as not to crash the Playdate by virtue of not returning from the game’s update function for 10 seconds. The wetness mechanic was tied into the crops, and they started to get their water and soil bonuses or penalties. Many additions were made to the game UI such as adding the inspect mode and in-game menu. The mechanisms to allow for game progression and the ability to unlock buildings went in - along with the UI on the pause screen to communicate the player’s next goal. And a tutorial and associated UI was added to help guide the player in the creating of their first factory.

Finally, a bunch of utility items were coded in. This was mostly because these were fun to work on and I was procrastinating designing the game’s core progression tree. A number of these utility items are visible in the animation below. The Retirement Cottage was added at this stage as a mechanism to end the game (a small homage to the excellent Space Trader for Palm OS, 2002 - here you would end the game by purchasing a small moon and retiring there).

Alpha 7 was released on August 14th.
Game Design & Progression Systems
Factory simulation is a genre where engaging content can be cheap to produce. Just as an author can write simply “an then an epic space battle commenced”, causing downstream issues to the director of the film adaptation, I can just as easily write [code to the effect of] “and then the player must create 280 catering kits, with each kit comprising a TV-dinner, desert, energy drink and suitable packaging” - instantly creating a multi-hour gameplay problem for the player to solve.
I wanted Factory Farming to have streamlined gameplay (at least, relative to other comparable entries in the factory simulation genre), given that it will be running on a tiny device and played with just two buttons + D-pad. To follow this principle and keep the gameplay loop focused, an early decision I made was to not add the extra complexity of tech trees or some equivalent of Factorio’s science packs. Instead the game progression is built wholly around selling cargo, something you’ll be doing anyway in order to earn the money needed to expand the factory.
While there is not an explicit tech tree with options presented to the player, there is still the game’s overall progression graph which is constructed based on the inputs and outputs of the game’s factories. This is currently split into seven different tiers, one for each of the six soil types - plus an extra epilogue tier. The average number of inputs required per factory increases in higher tiers, resulting in a monster five-input factory at the end-game (there are only eight accessible tiles around any factory, and one is reserved to output the finished products, so having to feed five inputs into at most seven tiles is quite the challenge).
The work to construct the tech tree, and its encoding in the game along with the progression-path used to unlock all the buildings on the way made the game playable and defined the release of Beta 1!

Of course, not shaking things up at all as the game progresses is also bad, complexity is gradually added in a few ways which I tried to keep tied to the core mechanics.
Utility unlocks can make the player re-assess how they are building their factories: unlocking wells gives the option of creating wet and damp soil in regions away from water; obstruction removers can be used to streamline existing areas of the factory; smart use of landfill can make aquatic farms much more efficient, and can even be used to dry out whole areas.
The different plots serve multiple mechanical and gameplay purposes. Additional plots fundamentally exist to allow the industrial complex as a whole to grow significantly larger than the Playdate would be able to simulate simultaneously. But in addition, the game’s complexity is also allowed to increase over time by initially hiding away the additional plots, and the extra complications which arise from this partitioning make themselves apparent as the gameplay progresses and the cargo exporting and importing mechanics become more important.
The limitations imposed by the plots system force the player to make strategic planning decisions:
- There are a maximum of three of the six soil types available on any one plot. Limiting what may be grown effectively and mined on any one plot.
- Up to four different types of cargo may be imported into any one plot, this helps to alleviate these restrictions but imposes its own optimisation problem in the choosing which cargo is most efficient to import.
- The bog plot is strewn with rivers, complicating factory design.
- The desert plot has no natural sources of water, and water must instead be imported if it is to be used here.
- The final two plots are bought not with money, but instead with late-game cargo items. This forces the player to have made it sufficiently far though the game’s progression system before they are able to access the end-game.
Music and sound effects were added for Beta 1, as well as end credits, a whole load of new art for all of the new cargo items, and flavour text for the new factories, a number of bugs were fixed and a bunch of quality-of-life gameplay, input & UI tweaks were made.

Beta 1 was made public on September 10th. The footage below is from my first full play through of the game, this was carried out with both infinite-money cheats and a cheat to massively reduce the number of cargo needed to unlock the next stage of the progression. Even with both of these active, it still took over six hours to complete the game.

A Problem With The Simulation
The next undertaking was a full no-cheats play through, this was done using Beta 3 (September 25th, containing a large collection of minor tweaks and fixes on top of Beta 1). I used this full play through to refine the sell price of raw cargo pieces (i.e the produce made by crops), progression requirements, and factorie’s value-added multipliers on their output cargo with respect to their inputs. I started to notice however, many hours in, that something wasn’t right…
Factories, harvesters and crops are pretty simple, they have a countdown timer and when it expires they do something. They manufacture a piece of cargo, attempt to harvest, or generate a new piece of harvestable cargo on their tile. They work basically the same with a tick-size of one vs. a tick-size of eight. But for conveyor belts, things are a bit more complicate…
A conveyor belt piece is a single 16x16 tile, and update-ticks are directly proportional to pixels. Therefore with sixteen ticks of size one for near tiles, or two ticks of size eight for far tiles, a cargo piece will move at the pace of one tile per second. At least this is true at the start of the game, later on once the player unlocks Conveyor Grease the speed of upgraded conveyors doubles - now a piece of cargo can cross a far tile in a single tick, this caused some direct issues and also highlights a wider problem…
There are some sophisticated algorithms to handle conveyors, the Factorio devs got creative and nicely documented their solutions in their blog. I might attempt something like this in the future, but given that I am writing this in raw C, and keeping always in mind that it is for a (relatively speaking) low powered CPU - I decided against adding complicated data structures to manage the conveyors. No, conveyors are instead treated like any other building during a tick update: for each chunk, iterate over the chunk’s buildings and call their update function.
Supposing then that we have a packed section of upgraded conveyor belts far away from the player, if we happen to iterate over the belts opposite to the motion of the cargo then everything works as expected - all items progress by exactly one tile.

But going the other way and, oh dear…. now only the final piece of cargo (the apple) moved, and it got caught up in the propagating update call and moved many tiles instead of just one tile!

Preventing a piece of cargo from moving multiple tiles in a single tick is not actually that hard, a number which increments on each frame can be used to keep track of which tiles have already received an update call in the current frame. But getting the carrot moving here is a bit trickier, the information about whether or not the carrot can move is not local to the carrot’s tile - in this example the carrot’s movement is depends on if the apple four tiles away can move, but by the same logic it could depend on some other tile which is arbitrarily far away.
There are likely many interesting solutions to this problem, I’ve tried out at least a couple myself. The one currently being used in the V1.0 release of Factory Farming is to employ the power of recursion. Any time that a conveyor wants to move a piece of cargo onto a blocked tile (i.e. a tile which itself has a piece of cargo registered) which also has a building, it calls that building’s update function in an effort to dislodge the cargo. The called tile may find its output is also blocked, and call the next tile, and so on, and so on. I use a global state to keep track of the depth of the recursion and currently limit this to 128 tiles deep to avoid any dangers of overflowing the stack (I still need to figure out how big this is on the Playdate). By monitoring the maximum depth of recursion achieved in some large factories, this seems like a sensible cap.
In the animation below, a tile update with a red border is one where (via the frame number) the tile realises that it has already update on the current tick and hence shouldn’t update a second time.

This then covers the majority of cases. One thing I had to prevent is any recursion starting from a far tile propagating onto near tiles (or vice versa), as this will cause the tile to update too many times and cause ugly skipping of items on visible conveyors.
Testing, Testing And More Testing
It took me just over 23 hours to complete my first full no-cheats play through, including balancing the game as I went.

My final factory ended up looking like this

I also setup an auto-screenshot-on-save script before embarking on plot six, here my goal was to try and create a full upgraded belt of the late-game Party Pack cargo from scratch, all in one plot. I just about succeeded! This plot then later became my hardware stress-test plot.

Following this, a series of four Release Candidates were minted on October 31st, November 3rd, 5th and 6th. These were used to get more feedback about the game’s performance on the Playdate hardware, and to fix the problem with the conveyor simulation described above.
Time To Get Serious
As great as the community were in providing feedback on how various aspects of the game ran on the hardware, it was becoming apparent to me that I needed a Playdate before I could launch.
I fired up eBay and ordered a second hand one for a little less than £300, it was the right call.

To go from Release Candidate 4 to Release 1.0 took me around one week from having the hardware in hand - without all the community testing along the way it would have taken much longer.
Some critical issues were only found and resolved at this late stage.
- A major optimisation of the sprites added to the display-list when the UI is open was needed, else the frame rate tanked.
- Additional optimisation was needed when deciding which chunk backgrounds to re-draw to minimise re-draws.
- Music will skip (to the next track) if the game update function takes a long time to return, around a quarter of a second or so. Some operations with more extensive re-draws of chunk backgrounds needed to pause the music.
- Some behaviour was different on-device vs. the simulator.
- This highlighted a signed vs. unsigned integer bug in the world-wrapping code.
- Floating point numbers were not being rendered with
snprintfon the hardware and I needed to manually implement something equivalent.
Performance
The Playdate can successfully simulate a plot with 6,300 buildings (all factories, conveyors, crops etc.) and 3,000 pieces of cargo at a playable frame rate! There is some slowdown from the simulation evident at a large factory size - but it is mostly only when the player is moving and the whole screen is updating.
The main hit to the frame rate comes from having a large number of cargo-sprites on-screen at any one time, and this too is made worse by the player moving.
A brief tour of the mega-factory shown in the time-lapse above can be found 7m into this gameplay video.
Release
Version 1.0 of Factory Farming was released on December 22nd for purchase on itch.io.
There are no definite plans at the moment for post-launch updates, but given I know that a current performance bottleneck is in sprite rendering in busy areas - this could be somewhere to look into in order to provide further performance boosts in the future.
Factory Farming is written in C, the source code is available on GitHub.