Lumbermill’s conveyor belt system has gone through many iterations, and I’ve finally arrived at a version that I’m happy with and does everything the game needs well. I’ve taken features from previous versions, improved on others, and in some cases borrowed aspects from designs such as that of Factorio’s conveyor belt system. Passing that forward, I wanted to write up an explanation for Lumbermill’s system – as it’s hard to find any good resources about this particular game design / programming challenge on the internet as of writing.
There are a few main things that Lumbermill’s conveyor belt system must fulfil:
- Items must move continuously as long as there is space (and power).
- Items must stop and queue up without colliding when there is no space – and preferably without using colliders.
- Items must require minimal processor time to move.
- Machines may have multiple inputs and outputs.
- Machines must be modular and easily programmed.
I’ve divided this blog post up into the different parts of the overall system, which all work together to fulfil those goals.
Chains
In the Lumbermill factory system, relations between machines (including conveyors) are defined as either “Parents” or “Children”. Parent machines pass items to child machines. A chain is defined as a continuous, connected series of machines or conveyor belts, always with a single start point and a single end point. So as long as there aren’t any machines that have multiple inputs / outputs, the game will recognise that as a chain.
Connectors
A connector is created in the backend representation of the factory whenever a machine is constructed with multiple inputs or outputs. These machines are never part of a chain, and act as either an end terminal or a start terminal to any chains entering or exiting the machine.
When an item reaches the end of it’s current chain, it will check for an end terminal connector and be transferred to the connector – which will then decide where the item goes next, using “Transforms” (see the Movement section for more detailed info).
Item Queues
When an item is dispensed onto a chain, typically by a storage building, the item is added to the back of the “item queue” for that chain. This is a List data structure that keeps track of all the items currently on a chain. Despite the name, this isn’t a Queue data structure – as it needs support for removing and inserting items mid-queue, in the case of conveyor belts being deleted while carrying items.
Item Movement / Transforms
While in a physics-based system (and of course in the real world), items would be moved by the conveyors themselves – in the case of Lumbermill, being an Isometric 2D game – it solves a lot of potential problems by making items move themselves. The other key part to all of this is that items keep track of their distance from the start of the chain. This in turn, can be used to compare positions between adjacent items in the Item Queue, and prevent collisions when the distance becomes too small.
Transforms
When an Item is dispensed onto a conveyor belt, it is given a set of “Transforms”, these are C# objects that carry out a transformation on an item when executed. These can range from moving the item to a position, to selling and destroying the item. When an item has an incomplete Transform Queue, it will carry out the transforms one-by-one, before requesting a new transform queue from the machine it currently occupies when no more transforms are left to complete.
Each machine has its own “Transform[] Progress(Item item)” method, which returns a list of transforms to the given item. In the case of a conveyor belt, the conveyor will return a single “Move” transform (to move the item to its child), or a “Wait” transform if it has no child. I’m sure you guessed correctly – the Move transform is what facilitates item movement.
Movement
On the first frame an Item has a move transform as the current transform, it will set a target position for the item. Every frame, the item moves using linear interpolation (Lerp). Each frame the item also uses some simple maths to calculate it’s current distance along the chain (see below), and then the distance to the next item. The move transform will pause if the distance to the next item becomes too small, and resume when it has returned to a reasonable value again. This continues until the item has either reached it’s target position, has been destroyed, or the target has been moved or destroyed – in which case, the Move transform will return failure and a new Transform Queue will be requested.
Calculating Distance
There are a few key variables involved here to ensure these calculations can be carried out as quickly as possible, as every item must calculate it’s position each frame (which using this method, has negligible performance effect from initial testing – though there are always further optimisations that can be made).
Firstly, when adding or removing machines to a chain – each machine is assigned an index representing it’s position in the chain, where the first machine is 0 and the last machine is chain.length-1. These indices are updated whenever a machine is added, removed, or a chain created.
Secondly, items keep track of which machine they are currently on – this is done by updating the Item’s “currentFactoryObject” variable every time a Move transform completes, to the Move transform’s target Factory Object (machine).
With those variables in mind, the value of “currentFactoryObject.chainIndex” can be multiplied by the length of a single conveyor to generate the Item’s basic position. This isn’t quite accurate enough yet, as a lot of the time multiple objects will be on the same conveyor and thus have the same basic position. To calculate the precise position, we can then subtract the item’s position from the current factory object’s position. The magnitude of the resulting vector gives the Item’s distance from the current factory object. This can be added to the Item’s basic position to give an accurate position. Each item has a variable to keep track of the Item directly in front, which can be polled for its own chain distance. “itemInfront.distance – currentItem.distance” will give the distance between the items, which in turn can be used by the Move transform to establish whether to stop or not.
Removing / Adding Conveyors
The final piece in the puzzle is correctly handling changes in the layout of a chain when there are items on it. When deleting a conveyor, the game will first split the chain into two separate chains – A and B. Chain A is the original chain, with everything before the break removed. Chain B is the new chain, comprising of all the machines before the break.
The game will then split the item queue. The first step in that, is to run through Chain A’s item queue, and check for items whose “currentFactoryObject” variable is the destroyed machine. It will destroy those items completely – which in Lumbermill’s case means hiding and caching them to be recycled when new items are created (Unity’s Instantaite method for creating new objects can get expensive quickly, so it’s faster to reuse objects were possible in cases such as this). It will then remove all the items up to that point from A’s queue. All of the remaining, non-destroyed items before the break are then added to chain B’s item queue. The change in Chain for these Items will trigger each Item to refresh its Transform Queue to ensure they don’t attempt to continue moving to their original target.
Final Thoughts
I’m really happy with where Lumbermill’s conveyor belt system is now. It supports machines with any number of inputs / outputs, items move smoothly and queue up correctly. Items can also be manipulated easily using pre-built transforms which makes programming machines much easier than it had been in the past. Hopefully this blog post has helped someone build their own conveyor system, as from personal experience – rewriting and designing this whole thing has been a long road!
If you’ve stumbled across this blog from somewhere other than YouTube, I make devlogs cataloguing Lumbermill’s devlopment. You can check out the channel here: youtube.com/bwdev
Ben