5. Over The Hump


Intro

It's been a while, but I am back baby. An irrelevant, two-line paragraph probably doesn't justify a standalone heading, but this commentary means that it will just yeah there it is reach three lines by the end (joke doesn't work on all devices).

Recap

In the previous development log entry, I wrote that I wanted to do the following in the next sprint.

  • 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 advised that the above list was in order of priority, and I also mentioned that:

  • I had plans on the weekend of the 2nd of April, so I was likely going to achieve less; and
  • I was wondering whether to work pause and main menus - that is, generic game stuff.

Review

Since the last entry,  I have achieved the items in the first two bullet points above. Not great. I have started on the pause menu and the main menu, however these are not complete. I said I had plans, okay. I should probably keep quiet about the fact that I have actually squeezed in two sprints since we last spoke since this would bring further shame to the measly progress made. I can explain, please don't leave me.

Am I overusing italics? How do you feel about being addressed personally? I think I may be at risk of giving off a Perks-of-Being-a-Wallflower vibe.

Die Rechfertigung

On Saturday the 2nd of April, I was out most of the day. I went up to Leicester to see my mam, brother, his gf, and my dog. I left at 1100 and was back by maybe 1900. I think I might have done a little work in the morning, but I doubt it. I'm also not sure that I did any work when I got back because I was absolutely cream-crackered (knackered (tired)).

I definitely did some work on Sunday morning, but I left again 1430 and played Baba is You with a friend, went for food, and then knocked back a whole pint at the pub. Got back at like 2300 and I had to cycle to work the next day. That Monday was the first day that all staff members would physically be in the office - previously there was a rota where only half of the office would be in at once [insert joke feigning ignorance of COVID]. I cycled four miles into the office just to find out that the internet had gone down in the whole office because there wasn't enough bandwidth to support a full house, so they sent us all home and we've been working from home for the past fortnight whilst it gets fixed. I was pretty grumpy cycling the four miles back home.

Going off on tangents aside, I did manage to integrate the floor pad and door functionality with the timeline. At this point in time, the solution worked, but it was messy and required a clean up. There were edge cases in my solution where the code was misleading - that is, the name of the procedures didn't align with what they actually did. I like my code to document itself, and a meaningful naming system is one of the ways I achieve that. Additionally, the allocation of responsibility within my code and the encapsulation of logic were all over the place.

Above the Law

I decided to break my no-games-on-weeknights rule - explained in the Hyper-Focus section of this post - and work on the refactor during the week of 04/05-08/05. It was definitely worth it.

Firstly, the refactor was more of a complete re-write, and it therefore took considerably longer than anticipated. I started on Monday evening thinking that I would be able to knock it out in a few hours that evening. How wrong I was.

My Hilbert's Programme

The initial problem I was trying to solve was absurdity within my code. The issue was in the OneWayFloorPadReleaseCommand class. This class is an implementation of a timeline command, and timeline commands are how I record, undo, and re-do in-game actions. The one-way floor pad release command is for recording the release of a one-way floor pad. A one-way floor pad is a floor pad that can only be pressed. Once it has been pressed, it cannot then be release. The complexity comes from the fact that my game features time travel - a one-way floor pad can be released when the initial press is being undone whilst time is being rewound. Timeline commands have two methods: CanExecute and Execute. Before the integration of floor pads into the timeline, the CanExecute method of a one-way floor pad release command always returned false - they can't be released after all. This meant, however, that the pressing of a one-way floor pad would not be undone as part of the rewind process, which is incorrect. After the integration, this implementation of the CanExecute method had to return true which was completely inconsistent with the definition of a one-way floor pad release command that was in my head. This was absurd.

When discussing this with my friend whilst playing Baba is You, I thought of the below mantra.

A system may be absurd provided that it is logical within itself.

In the context of our conversation, this meant the below

the rules your code defines make absolutely no sense whatsoever, but if sound logical reasoning based on those rules leads to inconsistent results, you have a bad system.

Having a one-way floor pad that can be release in itself is not bad. That being said, combining it with the rest of my game's architecture - that is, the naming conventions, the timeline system, the floor pad behaviour design, and their interactions with each other -  meant that it was absurd, and not absurd in a good way.

Encapsulation within Commands

My first attempt to solve this absurdity was to encapsulate all of the logic pertinent to a behaviour within the timeline commands themselves. Up to now, the floor pad was responsible for determining whether it could be pressed, and the timeline command just tapped into that logic. By moving it into the command, I could make sure that the inverse of a one-way floor pad's press command was something that could actually be undone.

This was a mistake. The first indication of this came when undoing and redoing actions during the rewind process triggered linked interactions outside of the time loop. For example, a door that was opened by a floor pad press would also be opened when that press was undone during the rewind. If you know anything about time travel, you know that the door should be closed by the inverse of the floor pad press during the rewind.=

I fixed this issue, but then I found that actions were being recorded during the replay phase of the time loop. After pressing a floor pad, triggering a rewind, undoing the floor pad press, and triggering a replay, the floor pad would be pressed as it was originally. The defect was that this replay press would get recorded onto the timeline and immediately replayed again. WRONG. I fixed this too.

Finally, I realised that after going through a full rewind and replay cycle, interactions with floor pads featured in the cycle would be broken going forwards. I will spare you the details, but the issue was the weight registered on each floor pad after the cycle. It was negative, which makes no sense.

The Second Re-Work

At this point, I can't remember the details of my second attempt. All I know is that it took ages and didn't work. The first re-work took ages too. At this point, I felt like I had lost momentum. I was stuck between a figurative rock and an equally figurative hard place. The figurative rock was the copious amount of time I was investing into this refactor. The figurative hard place was the little meaningful progress that was being returned by that time investment.  This is not a good place to me.

The Third Re-Work

I came away tired and frustrated. My eyes were sore, and I didn't know what to do. I went to bed. I thought long and hard about my system. What makes sense? Where should the responsibilities lie? I drew diagrams with words in boxes and many arrows pointing from one box to another - sometimes with squiggly lines between them.

I had it.

I closed my eyes, put a stick between my teeth, and I healed myself Far Cry 2 style.

I went back in.

It worked.

My final approach achieved the results and it made sense (for the most part). The compromise I had to make was a minor deviation from the abstractions I have in place, but I am okay with that. It's significantly better than an procedure doing the opposite of what it says it will do.

Bonus Round

I was done but I wasn't. The solution above had cleaned up my original mess, but it had made a new mess. Fortunately, this new mess was not a logical one. It was a best-coding-practices mess, and it was fairly easy - albeit time-consuming - one to resolve. I completed this on the morning of 09/05. Essentially, my game object code - or MonoBehaviours - were responsible for instantiating some of their own dependencies. This partly stems from restrictions imposed by the Unity Engine. The solution here is dependency injection, which is common practice in general software development. It is made difficult in Unity game development however because the internals of the engine control the instantiation of game objects. For readers from a non-developer background, instantiation means that creation of an instance of a class. Dependency injection is the pattern of supplying code with the things that it needs rather than that code being responsible for getting them (in addition to doing the things it was made for). This is in keeping with the Single Responsibility Principle, which is one of five principles that guide good code design. 

There is a dependency injection framework for Unity, but after having researched it, it sounds overly complicated for what I need. It's also quite a complicated framework with a steep learning curve, and I read about projects being held up or blocked because of complexity introduced by this framework. Additionally, it seems to be trying to work against Unity's component-based model.

The approach I followed was as follows. I encapsulated the construction of dependencies into builder classes. There would be an abstract implementation of each builder, which would allow me to easily add new ones later, and the later implementations would still seamlessly integrate with my existing systems. The builders were implemented as ScriptableObjects - types of Unity object that actually can be injected into MonoBehaviours - which allowed me to move the creation of dependencies away from my MonoBehaviours but still be able to inject the dependency at run time. The benefits go on, but this post is getting too long, so I won't list any more. It is safe to say that I am very happy with this pattern, and I will be using it more going forwards.

A Disappointing Ending

The pause and main menu are coming along nicely. Momentum has been re-gained. Not really sure what I want to achieve in my next sprint. All my unit tests have been invalidated. I'm tired and I want to go to bed.

Get Time Travel Prototype

Leave a comment

Log in with itch.io to leave a comment.