Skip to main content

Designing and coding the SwitchBlocks

This post is a continuation of a previous blog entry. Our MVP featured clickable, animated switches, but they were not "wired together." This meant there was no way to create a win state or any meaningful interaction! Without interaction, there is no game.
Cool XOR gate, but what now?

I thought it might be overwhelming to have every single Switch object report its Ready status via an event to the GameManager. Even from the perspective of the cockpit theme, it makes more sense to compartmentalize vehicle functions into different control blocks. In our design document, we brainstormed some of these compartmental functions:
Start the reactor (necessary)
Concept: Getting powered up
Execution:
Result: It is necessary to start the reactor before the ship can do anything at all.
This is likely the first step a player will always have to perform in order to proceed with the other challenges

Defeat the warning alarm (optional)
Concept: “car alarm”
Execution:
Result: By defeating the alarm, the player removes an annoying, distracting siren sound (think low health beep in zelda).
Reduce global timer rate of decay (gives player more time to complete other tasks)

Safety self check (optional)
Concept: NASA “launch readiness”
Execution:
Result: By performing a safety self check, the player will know whether all start-up criteria have been properly implemented.
If the player has forgotten a step, it will alert them prior to launch failure

Calculate nav data / coordinates (required)
Concept: getting ready for the jump to lightspeed
Execution:
Result: It is necessary to tell the navigation computer where to go with the stolen ship.
Without properly calibrating the jump to lightspeed, the ship cannot take off.

Remove ground tether (required)
Concept: getting ready to take off
Execution:
Result: It is necessary to decouple the vehicle from the ground / docking station.
Without this step, the ship cannot take off

Life support systems for travel (required)
Concept: making cabin habitable for player while in space
Execution:
Result: It is necessary to pressurize or otherwise secure the cabin for flight in space.
Without this step, the ship can take off, but the player will perish in space

So these functions could be separate arrangements of Switch objects into what I called a SwitchBlock. It made sense that a block could represent the Reactor, for example, and that the GameManager could keep track of each block's function as well as status. That meant if the Reactor was OFF, the GameManager could ensure that the rest of the cockpit was also OFF. Likewise, if all the blocks were completed, the level could progress.

OK, this seemed logical enough. Any switches on the level would belong to a block, and each block would listen to the status update events of its respective switches. This had the side benefit of making the "ears" of the GameManager plugged to the noise of each switch, and only had to listen to more momentous changes in whole switch block statuses. But first things first...

It became immediately apparent that simply grouping switches didn't matter unless the switches had some way to interact with each other. I decided that their relationship should be handled by the switch block, but I didn't know how this would work. I certainly did not want to have to hard-code each level's ship Reactor switch block by hand to accommodate the (potentially) unique way this particular ship powered up. In other words, I needed to find a way to create links between switches without those switches needing to know anything about the links.

So I created a new class called SwitchRelationship. Each SwitchBlock has an array of Switch objects that comprise the block, as well as a list of SwitchRelationships that it can coordinate.


But how is the block coordinating these relationships? It's determined by the properties of the SwitchRelationship. The behavior of the primary switch is always directed at the dependent. The behavior itself is defined as an enum "RelationshipType" within the SwitchRelationship itself. It seems wrong to me, but it works in practice: when the Primary becomes Ready (for example), it uses the type enum to call a coroutine with the same name.




The switch block doesn't immediately know what relationship(s) the switch belongs to, so we need to go through the whole list and call each coroutine:

Cool. So now they're linked together, but I had to create some co-routines that actually made for useful interactions. The list above is the list as of today, but when I started out, switches could only LOCK or UNLOCK other switches. So this meant I needed to expand the Switch class to have a lock/unlock status, and an additional Event that invoked whenever LockStatus changed, as well as a conditional within Clicked() to disregard player clicks (and make a unique sound) on locked switches.

At this point, I stopped working on switch code because much of the level design can now be done without touching the scripts any more!

Comments