Imagine having a bunch of machines in a room, each of them does a seperate function. All of them are required to function, and each of them has slightly different requirements. Each machine can have resources that another might need, and each one needs to be ticked but not all the time. The final complication would be the fact that not every machine can query the grid network at the same time since it is single threaded. It sounds daunting at first, especially if you needed to program something like this but the solution can be fairly straightforward if we take advantage of a popular programming pattern known as chain of responsibility.
The principle behind this design pattern is that each object in a chain has the ability to pass messages to the next machine it is connected to. Every machine you have would be connected together in a large chain, in code this would take the form of a linked list. We use this ability for each node in the network to know about every other node to pass messages around in the form of requests. Here is what the diagnostic panel for the grid network looks like in Space Trucker at the time of writing.
Some of the terms might be confusing so let's clear that up. Reconstruction is when the grid network checks the map for all compatible machines and builds up the chain of responsibility, this would be something that happens when the map is loaded initially but could also happen while the game is running (such as when a machine is destroyed or added to the network).
Provider regions are literally regions of space on a given level where machines can exist. The purpose for compartmentalizing this is to have different rooms which can be cut off from other rooms. Another example of needing this would be for oxygen distribution so we could fill the given volume with reserves from a machine to produces oxygen.
Node points are the actual machines on the grid network, each node point in a provider region is capable of passing around resources to other nodes in that provider and other providers also. The requests sent and received is for keeping track of how many times a given machine will ask to get something like electricity or water, and vice versa how many times it gets a request to deal with something a machine produces like fuel for reactor or a battery taking electricity that was generated.
An example scenario might be a electric turbine that is spinning. It produces energy to feed into the network, there is a polling time for this to occur and is all event based. When the machine triggers the want to pass electricity around the grid network it gets added to a queue of requests that get processed every time the game engine updates which is about 20 times a second.
The energy produced from the turbine goes into this queue and when passed around the chain of responsibility proceeds to ask every machine how it would like to proceed with the following resource letting the machine itself decide at the time if it would like that. In the case of our turbine it could add energy to a machine that needs it, but the system is designed to work on levels. The energy coming out of the turbine is in bursts of 512 "energies" per update, if the machine has something in the queue already it cannot make another request until that previous one has been dealt with.
So a computer on the bridge of our ship does not need this much energy, it can only take energies in increments of 32 per update so the turbine packet passes by the computer and onto the next machine and asks it if it can take the 512 energies it has and each thing keeps telling it no until it arrives at the ship's battery which is configured to allow packets up to 512 to be accepted into it. Once the turbine energy packet hits the ship's battery on the chain of responsibility it is then consumed by the battery and the grid network informs both the battery and the turbine of this operation.
A request is marked as being successful if something in the chain accepts what it was either requesting or providing. In the case of the turbine it was successful when it was accepted by the ship's battery, thus charging it up some towards its ceiling of about 10k energies. Failure, while sounding harsh is actually not that bad. It really means that the request just did not get accepted by anything on the network during the time it was passed around the network, this is done done for every single time the packet visits each machine but cumulatively where each packet is measured as a whole on whether it succeeded or not based on what it was trying to do such as provide energy to machines.
With the possibility for so many things happening at once it makes you realize that you need tools that exist in the real world... in your game world in order to properly measure what is happening. Especially if we would like to convey this information to the player of our game. Above is what is known as an annunciator panel, it shows an array of conditions based on pre-defined categories. In the example video above the switch was flipped to perform a light test which loops through and triggers each light individually for testing purposes only.
During normal conditions this is configured so each light on the panel has the ability to hook directly into a given machine using the grid network as a basis to quickly get pointers to the information where it exists in memory rather than having to search for it. During the reconstruction event a dictionary of machines in the grid network was created and then used as a reference for a method to get a machine by a given type.
This makes the operation of the each panel very fast, and not directly dependent on the machine but the existence of it in the grid network. This means that if the machine is deleted the game will not crash, instead the panel will just never update since it never gets the pointer to a current machine instance and therefore will not do anything. Using this style of design prevents jarring null reference exceptions in code that can halt development when in reality nothing is actually wrong or missing just a typo on the programmer's part or maybe the level designer had not placed the right machine down but none of these are problems using a system like this since you just notice the problem and then correct it. Sometimes the problem can be corrected without even having to close the game while it is running, which makes the life of working on a complex system like this much easier.
One of the final challenges in a system like this is the graphical display of data on a terminal. Not to say it is more difficult than displaying it on LED's and gauges, but rather it requires a different sort of finesse to move the data into a correct space the game engine can understand. When working with entities in the game world it is easy to refer to things by type, for example you can hook something by a type such as querying the grid network like "give me all the batteries" which would return array of all batteries in the network based on provider regions they reside in allowing for easy iteration and or specific checking based on the provider the machines resides in.
Since the GUI elements are not game entities it requires something else. The GUI system uses the concept of controls and a screen control manager which is based on 2D vector coordinates meaning we have only two variables an X and a Y direction to work with. There is sort of a Z plane, in the sense that one control can exist on top of another control but this is not configured via code but rather by hierarchy. Some special edge cases exist and allow for properties to make a given control 'always on top' or 'mouse cover' which means that when that control is above another control mouse clicks cannot fall through to the control below it so that the GUI operates in the way both developer and especially players will expect it to. The purpose of which is to stop accidental clicks, if you are interacting with an interface with some buttons on it, and another dialog pops up and covers them clicking where they exist in the space below that other control should not fire the event for clicking the button it will be ignored when it is covered by another control to prevent this unwanted behavior.
The solution to this problem is two-fold and requires a couple more well known design patterns like the one we used to build the grid network messaging system. For the GUI to represent the state of an entity in the game world we need to use a structural pattern known as a proxy. It will provide a surrogate or placeholder for another object to control or access it.
This system we will call the GUI proxy, with its purpose being to allow a machine to change controls on a given control without any direct bindings to them. The proxy class would contain methods like UpdateTextBox, UpdateProgressBar, UpdateEnabled, and UpdateVisible so that a control could be manipulated by proxy and this all this information can be serialized and passed along to the GUI when it asks to update. An update to an in-game GUI should occur during a render event and not several times between ticks to optimize the performance overall.
Finally, the last pattern we need is on the GUI side. We can use the behavior pattern known as the observer to talk to the GUI proxy and ask it questions about what state our controls should be in without directly binding to them at runtime. With this one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Using both of these patterns allows each computer workstation entity with a monitor and in-game 3D GUI to update only when something has changed according to the GUI proxy. Each control is hashed with it's name, position, and rotation in the game world to keep it unique and to stop cross-talk between other GUI's and the proxy itself. Using this unique hash key we query the GUI proxy for changes on startup and hooking a notification event that triggers when the data changes.
Now that we have this grid network system and the ability for it to talk to entities and GUI controls alike we can start to use it to build out the rest of the functionality on the ship. Each machine will have it's own requirements and design that needs to be followed but using this system we can accommodate them all and anything else we might come up with in the future.
Next post will talk about how you can leave a truly personal touch on your game.