Generator operations training simulator


First, a demo

The premise

The SmartSpatial project requires a training simulator for their fresh hires. These employees must know how to deal with the day to day tantrums of electronics. Primarily they worked in the power production department for a data center. The system representing it must accomodate tantrums.

The initial demo would be with a generator; It would simulate different causes of it failing like fuel problems, intake problems, electricity etc. It would also provide proper stimulus (think "Davey here can tell the problem of a V8 from how it breathes") to cultivate instincts among the new hires.

I took one look at the requirements and started salivating at how outlandish they were and the possibility of implementing them. Not in my wildest daydreams would I have been given such a nice toy to build. So I got to work.

Implementation

I chose to use C++ for this project, and chose to use UINTERFACE() for modularity. Through artist protest, I would later have to go back on this decision.

Level management

Firstly, SmartSpatial uses the popular but infamous ArchVizExplorer template. But just like Darco, I can use levels to separate the functionality. Fairly easily but the client's delight made it memorable.

Resources

The resource system is the backbone of the entire simulation. The abstract design is as follows:

  • What is a resource: A resource is anything in finite capacity that can be consumed and provided. For example, electricity, fuel, air etc. The consumption rate can be modified, as well as different parameters of the resource.
  • Why is a resource: To drive an operation. Certain actors are Sinks and can have a relevant Source. Through this relationship, a Sink may change its behavior depending on parameters of the source. For example, a generator may shut down if there's no power to its breaker, or emit black smoke when fuel is not being provided at the right rate.
  • Where is a resource: Resources are found in Sources. For example, electricity is found in this mystical plug in the wall. These sources can also change their parameters based on how much resources are left, or what is the rate of consumption of these resources. For example, a generator may start droning if there's too much load put on it.
  • How is a resource (transferred): The Sinks and Sources can be compounded, where a sink can also be a source. For example, a breaker is an electric Sink that is connected to a wall plug; but it is also an electric source and provides the generator with mains electricity; it can monitor the power taken by a generator, or voltage provided by the wall outlet and trip to protect the merchandise.

In practice, each resource and its corresponding sink/source combo was implemented as its own UINTERFACE() in C++. For example, IPowerSink and IPowerSource for electricity, that consume a corresponding FPower struct.

We could have used Templating, but sadly Unreal Engine disallows it and it does not play well in the editor. Though this would have fit nicely by OOP, it would've incurred the cost of maintaining UObjects and their lifetimes.

Had I a bigger team, I would have explored these avenues but timew as a wasting.

The noise system

Noise is a big part of working with generators, as such we needed a noise system to prevent the operators from taking their real life headphones off and winging it.

I created a noise system that compounds noise created by multiple sources and creates a Noise Field.

This noise field can be sampled, so we can calculate the Decibels at any point. I made the noise system with exponential curves in mind, but it never came to that and we sailed along with linear curves.

The operator constantly samples this sound field to determine a Noise Level. The operator has a Noise Limit above which things get messy.

The noise field can be sampled for a gradient in any direction. For example, if the operator wants to walk forward, we can sample the noise at their currect location, sample the noise at the location they would be at (considering the input vector, the movement speed and the frame time). If the operator is above the noise limit and wants to move in a direction where the noise is greater, it is disallowed with a popup to inform the operator.

The inventory system

An operator can pick stuff up (spare fuses, breakers, tools) and put them in their inventory. Before Pickup Picking them up is as simple as going Before Pickup

The tools system

The tools hook into the mechanics and provide different functionalities. For example, a fuel canister may be picked up and used at the fuel inlet of a fuel tank to fill it up. The usage is defined by hitboxes, which are primitives belonging to the same actor, along with a relevant tool target component which is set to find all hitboxes with the relevant tag. For example, in the fuel can's case, the Generator has a FuelCanTarget Actor Component, which looks for primitives with the "SSP_FUEL_CAN_TARGET" tags, and reacts to the fuel can accordingly.

Tools are equipped and held to be used, but there are also tools that can be used passively. For example, the Noise Protection can be worn without having to take it off to use the gas can.

Examples of tools:

  • Diesel and Gas Can
  • Continuity meter
  • Voltmeter
  • Dipstick (to determine what kind of fuel is in the tank, and how much there is) et cetera.

These Passive Tools slot into different locations. So for example, Noise protection cannot be worn at the same time as a headlamp. However later these advanced passive tools were scrapped because it was too complicated to get across to the operator.

The callout system

The operator needs to be notified of the result of certain actions. Certain things like "The generator sounds fine and whatnot".

Luckily I had the perfect inspiration for this. Ever player The Witcher 2? Witcher 2 Combat Log (Courtesy of GameUiDatabase.com) Towards the bottom-right side, the game details all damage dealt to/by the player as well as certain hints like "The mechanism has unlocked". I yoinked this system to create little callouts that inform the operator of tidbits about whatever they're working on. These are triggered on things like inspection, replacement etc. Callout before fuse fixed Callout after fuse fixed And by that I mean individual components of each thing, like here the operator is notified on the state of each fuel filter. Callout on clean filter Callout on clogged filter

And much more if you'd like to listen to the rant.

Takeaways

Use actor components

UINTERFACE is good in that ANY kind of entity can implement them. It doesn't have to be an Actor, it can be a widget or a UObject (it actually has to be a UObject). BUT they get clunky very quickly.

  • For one, nesting an interface gets weird. For example, a generator is an IPowerSource but it relies on the Mains Switch, which must be a different actor in the editor. It gets cumbersome to manage and modify this kind of linking.
  • Interfaces cannot have sibling contracts. C++ does not let you extend interfaces, which means I cannot, for example, ensure that an electric switch absolutely MUST be a power sink as well as power source.
  • There is no way to signal up in Interfaces. I cannot get a reference to a Delegate from a UINTERFACE. Before you think about having a function that returns a delegate that is set by the implementer, please say that sentence again inside your head.
  • There is a weird bug going around (for just me on the forums apparently), at least in the football sim, where the implementations in Blueprints simply vanish on each reload.

Just use actor components where possible. They play really well with AngelScript, and for certain things like widgets you can always rely on UINTERFACE.

Make more diagrams

Midway through this project, I started using Excalidraw within Obsidian to share things with clients. The effect was immediately noticable and drastically positive.

From then on, I've used Mermaid and Excalidraw to communicate anything technical.