Since I was mostly the programmer, I’d like to look at some of the systems that make the game engine work. But first…
Inheritance?
For me, what made this game possible was an email exchange with Michael Schmidt at Unity, in which he cleared up something critical. Since Unity regards every script as a different C# type, how can you subclass a script into different variations? More specifically, how can one script interact with another when it doesn’t know if it’s the class, one of the subclasses, or one of the subclass’s subclasses?
The key is that Unity will treat a subclass script as if it were any class up its chain of inheritance. When I subclass ActualThing into Upgrade, and Upgrade into Sensor, other scripts can reference a Sensor script as if it were an Upgrade script or an ActualThing script. So these lines are equivalent:
float currentMass = someGameObject.GetComponent<ActualThing>().mass;
float currentMass = someGameObject.GetComponent<Upgrade>().mass;
float currentMass = someGameObject.GetComponent<Sensor>().mass;
A Sensor script can then override, for instance, ActualThing’s takeDamage() function, and respond to it differently than, say, a rock would:
//in ActualThing:
public virtual void takeDamage(float damageAmount){
//reduce hp
//in Sensor:
public override void takeDamage(float damageAmount){
//reduce hp
//reduce sight distance
This is how basic object inheritence patterns can be implemented in Unity.
Systems
The Grid: The first programming challenge was an infinite, non-repeating grid. Based on my previous thinking on large pseudorandom world generation, I worked out a system that places BigTiles (containing a 10×10 grid of normal game Tiles and other content on top of them) from a list of available BigTiles. A given x and y will always generate the same BigTile, allowing the game to dispose of BigTiles it no longer needs, but generate them again if needed. Each game picks a random x and y offset to the starting position–a pair of C# ints of value -2 billion to 2 billion. The x and y offsets are summed and used as another pseudorandom seed to further shuffle things around atop the BigTile. The list of available BigTiles changes based on the distance from the game’s starting point, allowing the game to gate more powerful and challenging content. Specific BigTiles can also be forced to appear, like the starting location.
Pathfinding: The bots use an A* pathfinding system. Niek worked out the initial code, and I adapted it to allow planning without executing the route (for AI reasoning) and to talk to the existing grid-based systems. This required a deep dive into Amit Patel’s A* Pages, a deep summation of pathfinding systems, which I highly recommend.
AI: The bots not being driven by the player have competing desires, to which they assign weights based on need and availability of a solution. The highest weight wins. They may reevaluate their options several times on the way to their goal, but the current “plan” has a sunk-cost-fallacy bonus attached, to reduce indecisiveness. The AI can query the Pathfinder (a separate script) for the “cost” of a path; it will also store the steps needed for pathfinding, to avoid a processing-heavy path recalculation for the action it ultimately decides to take.
Mystery Boxes: Remember how every BigTile has its own pseudorandom seed number? MysteryBoxes are a system that uses these to “randomly” shuffle things around on BigTiles when they’re generated–plants, upgrades, whatever you’d like. The plant growing on top of a toppled monument? That’s not scripted. One limitation is that if something on a BigTile gets destroyed, it’ll reappear if the player ever goes far enough away and comes back. A special subclass, the CPUBox, will generate a new CPU (the base of a new bot) if your party is smaller than the allowed size, or a random Upgrade if not. Like other gated content, the maximum number of party members increases with distance from your starting point.