4. Abstract as F


Progress Summary

As mentioned in the previous post - which I probably haven't written yet - I aimed to achieve the below in this week's sprint.

  • Implement a generic solution for floor pad and door interaction.
    • Floor pads should require a configurable minimum weight to be placed on them before they can be pressed.
    • Floor pad functionality should be easily extendable to variations - e.g. one-way, toggle, hold, and timed floor pads are in the pipeline.
    • Floor pads should interact with doors in a generic way so that other interactions - e.g. lights, moving platforms etc. - can easily be linked to a floor pad without requiring changes to the floor pad code.
    • This should integrate with the timeline rewind and replay features.
    • The new features should be covered with unit tests as much as possible by way of the HumbleObject design pattern.

The only thing I did not achieve was the timeline integration, which is really annoying because it spoils an otherwise perfect run. If only I could go back in time and remove it from the previous post.

I did spend significantly less time on this project this weekend though. I think I did about three and a half hours on Saturday and Sunday each, which you will know is not a bad thing if you've read my Psuedo-Productivity entry. I think I've spent more time on the dev logs than on actual development.

The next section is quite a long one that goes into some high-level detail (what an oxymoron) on how my floor pads and doors interact. If you don't really care about the section I put the most effort into, then you should just skip to the end, Dad.

Floor Pad and Door Interaction

Initial Naïve Implementation

An initial implementation of the interactions between the player, floor pads, and doors would have a direct communication between parties. The floor pad, for example, would store a list of the doors that it operates, and flow would look like the below

  1. The player collides with something.
  2. Player checks whether that something was in fact a floor pad.
    1. If it was indeed a floor pad, the player attempts to press it.
    2. Otherwise, nothing happens.
  3. The player queries the floor pad to see if it can be pressed.
    1. A floor pad can't be pressed if it is already pressed.
    2. A floor pad can't be pressed if there is not enough weight on it.
  4. If the floor pad can be pressed, the player presses it.
  5. The floor pad iterates through the list of doors and operates them.
    1. The door is opened if it is closed.
    2. The door is closed if it is open.

I've produced a diagram of this process below.

Naïve Floor Pad and Door Implementation

An initial, naïve implementation of the interaction between the player, floor pads, and doors.
Diagram Key

Stadium - A terminating point in the process.

Blue Rounded Rectangle - A C# class. An object in the game that I have implemented in code

Diamond - A decision made by the preceding object

Grey Rounded Rectangle - Indicates distinct areas of the code base. Code with a logical relationship should be in the same or similar areas. The player, floor pad, and door aren't really related - they just interact with each other.

Rhomboid - An interaction or dependency between classes. The direction of the arrows indicate which way the dependency goes. In the above, the player code is aware of and consumes the floor pad's code, and the floor pad is aware of and consumes the door's code. A rhomboid overlapping two grey rectangles indicates that the interaction creates a dependency between two significantly distinct areas of the code base/game.

Arrows - Indicate the flow of control/dependency. The do not represent the flow of execution. In this diagram, they are always pointing downwards, which is the same direction as the flow of control. In later diagrams, some arrows point upwards. The flow of control is always down the page.

The Issues

This is pretty much what my solution looked like when I initially cobbled it together. It gets the job done: the door is opened or closed as expected, and I am able to get on my merry way. So why would I come back to it a fortnight later and spend another six hours on it?

The main problem with this approach is that it is neither flexible nor extensible. Suppose I want a floor pad to operate something that isn't a door. I would then have to make changes to the floor pad code so that it could accept this new item. If I did not plan to change my approach, I would have to keep going back to the floor pad code every time I wanted to link it to something new. Thinking ahead, I can imagine a floor pad  operating a moving platform, and I shouldn't have to change the floor pad to do this.

The direct dependencies between the distinct areas of the code base are also a problem. If the player consumes the floor pad's code, then I will have to check that this interaction still works every time I update the floor pad. Similarly if I change the way the doors work, I will have to check the updated version is still compatible with the floor pad. This domino effect is effectively illustrated by the arrows.

A Revised Implementation

The revised implementation solves the above by:

Abstraction Meaning

I will briefly explain what I mean by an abstraction with an example for any readers from a non-developer background. In the previous implementation, the floor pad knew that is working with a door because the interaction is direct - there is no intermediary between the two. As discussed above, this hinders extensibility because I need to add code to the floor pad object every time I want it to interact with something new.

The C# language allows us to obscure object behaviour from their consumers. In the scenario above, the floor pad is consuming the doors code, but it doesn't really need to know that it is working with a door. In fact, the floor pad could get away with only knowing that it is working with something that opens and closes. This is effectively an abstraction of the door, and it allows the floor pad to operate with a window, hatch, or mouth in the same way it interacts with a door.

The door is hidden behind an interface, much like the ones and zeros in a computer are hidden being a user interface.

Revised Floor Pad Implementation


Diagram Key

The new symbols are defined as follows.

Speech Bubble - An event channel used for communicating between different objects. This removes the direct dependencies between objects. The event channels intermediaries - middle-men, if you will - between two areas.

Horizontal Dotted Line - This represents an abstraction layer, and I will explain what each one is abstracting below.

The Justification

With the above implementation, the Player object is no longer interacting with the floor pad. Instead, we have a dedicated floor pad interactor whose job it is to monitor collisions with floor pads. The floor pad interactor is still attached to the player as a component - it is just separate to the rest of the player logic. This means that I can also attach a floor pad interactor to other objects in my game if I want them to be able to interact with floor pads. For example, I can put it on a moveable box or an NPC, and I wouldn't have to update the floor pad interactor's code. Sweet.

The floor pad interactor interacts with the floor pad (obviously), but there is an abstraction layer between the two parties. As far as the interactor is concerned, it is interacting with something that can be pressed and released. This allows the interactor to work with different implementations of the floor pad. If I add floor pads that only work when the player has collected a certain collectible rather than requiring a minimum weight, the floor pad interactor will work with the new floor pad straight out the box. Saucy.

The floor pad evaluates whether it can be pressed as before, but it now broadcasts a notification on an event channel when it is pressed. The channel it broadcasts on is floor-pad specific - it's titled FloorPadPressEventChannel1, and there is a channel 2 and 3. The channel notifies the relevant objects that the floor pad has been pressed rather than the floor pad itself.

Note how there are two event channel symbols in the above diagram. The first one represents the floor-pad specific channel, and the second one represents an abstraction of that same channel. The channel is presented to the door in a generic way - the door doesn't know that the event channel is specifically for floor pad presses. It just knows that it is a channel that sends notifications. The generic channel is in an area of the code base separate to the floor pad and the door, which is why it is is a distinct grey box in the diagram. The door is no longer restricted to only floor pad notifications - any channel could tell the door to open or close - and this is achieved by that abstraction layer. Now, I could, for example, add in a type of door that just opens when the player comes near, and I wouldn't have to change the door's code. I would just need to set up a channel that notifies the door when the player is near to it.

Note how the arrows go upwards from the door to the generic channel. This shows that the door is dependent on the channel rather than the channel being dependent on the door. New door features have no impact on the channel.

Revised Door Implementation


After a notification is sent along the channel, the interactions on the door's side are similar to the floor pad's. The door interactor is the only thing listening to the channel - the actual inner workings of the door don't know about the channel. This is captured in the direction of the arrows, which flow upwards to the event channel and downwards to the door.  The interactor is a middle-man. This is further illustrated in the below screenshot, where the door interactors are actually completely separate to the doors in the hierarchy on the left.

There are two floor pad channels in the scene above. The first door is linked to the red floor pad, and the second two are linked to the purple floor pad.
Thinking about it now though, I'm not sure why I have them as separate object. That will just mean I have to put two objects in the scene every time I want to add a door. I may re-visit this.

The door interactable serves as a middle-man between the door interactor and the actual door. This is to split out the responsibilities: one thing is solely responsible for interacting with the door (the interactor), and one thing is solely responsible is responding to those interactions (the door). This allows me to add in another abstraction later - the interactor only cares about being able to interact with something, and the door interactable translates this to an open or a close. The door actually does the opening and the closing. My implementation of this has allowed code re-use, but I appreciate this is not clear from the diagrams. I also won't be able to explain this without going in to a lot more detail, which I don't think is appropriate. You will just have to take my word that I have minimised the amount of additional code required to add another interactable object even if that interactable object is completely unrelated to doors.

Finally, there is an abstraction layer between the door interactable and the door itself. The door interactable code only needs to know that whatever it is working with can open or close. This allows me to change the type of door being interacted with. Perhaps I will add a door that requires the player to have collected a key before it can be opened. This approach means that I will just need to add a new type of door, and the interaction logic further up the chain does not need to change. It additionally allows me to use the Humble Object pattern to write unit tests for the code, but I will save that for the next entry.

Next Steps

I have a few plans this coming weekend, so I will try to be more reserved with my goals. The below is in order of priority.

  • Integrate the floor pad and door interaction logic with the timeline.
  • Create some levels with the new door and floor pad features.
  • Write another sweet dev log about my unit testing (automated code tests).
  • Restrict player movement during the rewind.
  • Move the clone activation into the timeline area of the code base. It is currently in the player area of the code base, which doesn't make sense considering this area has no control over whether the rewind is approved or not - it can only request a rewind.

I'm aware there are still a few items above, but a few of them should be quick wins. That being said, I am wondering whether I should quickly knock up a main menu and pause menu so that I can actually deploy a build to this site. I can really do so if you are unable to quite the game without terminating the programme in Task Manager, and I think it would be cool for readers to be able to play it as it is being developed.

We shall see.

Get Time Travel Prototype

Leave a comment

Log in with itch.io to leave a comment.