task[3]

lim jia sheng,
0344034.

BDCM
.Games Development
::task[3]






task[3]: Game prototype

todo:

  • Create prototype of game idea

process:

So, where do we start?

Succide in Unity, n.d

Figure 1.1.1, Succide in Unity, n.d

Organising pre-existing components

Going off of the tutorials, I already had a few components. Whilst scattered, they acted as the foundation where I'd build on.

Overview

  • GamdTut.Core
    • Animation
      • Aniwaitor — helper that adds animation events programmatically to enable awaiting animations
    • Behavioural
      • CollectableBehaviour — wrapper to declare if the current object has been collected
      • DamagableBehaviour — wrapper for damageable objects, managing animation, projectile collision, & health (yes, there's also a typo)
      • ProjectileBehaviour — declares the current object as causing damage to damageable objects
      • SelfDestructBehaviour
      • SelfDestructOnAnimationEndBehaviour
      • SelfDestructOnDieBehaviour
      • SelfDestructOnStartBehaviour
      • SpawnOnDestroyBehaviour
    • Character
      • AbstractMeasurable — a skeleton class holding a single count prop
      • CoinMeasurable — another skeleton class extending AbstractMeasurable
      • GunController — Manages bullets shot from a gun on the player object
      • PlayerController — Controls player movement
    • Things
      • CoinController — Manages a coin's lifecycle, animation, & collection
      • DoorController — Enables the door when the gem key is collected, & manages its opening
      • GemKeyController — Manages a gem key's animation & collection, & the current camera
      • GemKeyManager — Manages its gem key child's active state depending on CoinMeasurable
      • VirtualCameraManager — Abstracts away cameras in the scene into individual indices, so they can be changed easily

Already, there were a few sore spots that should be changed for a real game.

Phasing out the Manager suffix for components

A Manager is already nomenclature widely used to describe a class that congregates various functionalities under its own umbrella. This differs slightly from how I was using it in GamdTut, which is a component that manages its children. Going on google & finding a synonym for it, landed me with a perfect match — Comptroller, aligning with the pre-existing Controller convention.

VirtualCameraManagerVirtualCameraComptroller

Controller? Comptroller? Behaviour?

Now here comes another problem, how do I differentiate between these three random ahh words? I sorta had a feeling on what they should mean, but this was the time I drew a hard line around them:

  • Behaviour — Component with no side effects (eg. a component which only stores data, provides events, registers event handlers etc., but does not modify the state of the GameObject itself)
  • Controller — Component with side effects (eg. a component that modifies the position, velocity, animation of the GameObject itself)
  • Comptroller — Controller component that manages the children underneath it (eg. a component that disables all its children on an arbitrary condition)

Transforming existing components

There were a few components from the tutorial that I felt needed a little bit more love in order for them to fit the final game.

Modularising PlayerController

There were definitely things to tweak & add. The first being simply to implement support for an animation state machine. However, whilst doing that in the base PlayerController class, it became awfully apparent that I should modularise it. Thus, I renamed PlayerController into PlayerMovementController, added a PlayerAnimationController, & used the existing PlayerController class as a wrapper for all the modules (via the ever helpful RequireComponentAttribute!).

Interaction

From the tutorial, I already had GunController, which read from the input directly & spawned bullet objects. However this would pose to be annoying if, & when, I'd want to read or change the input somewhere else. Thus I took the fire reading logic & abstracted the input management into PlayerFireController. Using events, I also added a fire rate.

Measurables

The original measurable class was bare & barren, so there I went fleshing it out. Before that, I knew that a measurable should function more or less like a writable store. Thus, after implementing Store, the new Measurable would derive from it. A measurable would need to ambiently reduce its value, so I knew this would've needed to be done through ticks. Whilst implementing withering in a Tick() method, I got the idea of just making the whole class an enumerator. This would allow me to start a Measurable as a coroutine, & not worry about manually ticking it.

After migrating Tick() to MoveNext() & adding a few more methods, it suddenly struck me how annoying manually tracking, adding, & subtracting from the current wither rate would be. This made me come up with a, probably extremely overengineered, solution of a transformer/modifier-based system to modify the value of a Measurable per tick. Using the built in C# event Action? as a reference, I vomited out MeasurableTransformer, which would enable me to just use the += & -= operators to add delegates that would run as """pipes""" through the data, transforming it & return the result, every tick.

Implementing the rest of the game

Analogy of this section, n.d

Figure 1.1.2, Analogy of this section, n.d

Level brancher

The first thing I thought to implement in this game was where the player would land to make their decision. I thought this would be a relatively straightforward thing to implement, & I was relatively right... Only relatively. The only tricky part was architecting the required modules for interaction, triggering, & arbitrary reveals to be modular — IntersectableBehaviour, InteractableBehaviour, & InteractableHintController — to form a setup where a hint would appear & interaction would be enabled once the player steps close enough to an object. After that, duplicating that 3 more times basically built the level.

Mood, Wealth, & Health

The backend for these were a breeze, big thanks to the Measurables & MeasurableTransformers that were implemented earlier. However, showing them was a little weird. This was where I found the difference between scale & size of a RectTransform. Whilst peculiar on a plain square object, is immediately obvious when any texture or curvature is applied, with scale distorting significantly. Thankfully it was a quick fix, but it made me scratch the inside of my skull for long enough that I felt compelled to put the short story here.

Procedurally generated fun

Hitting keyboard keys by jumping & interacting was one of my main inspirations for the game idea. As the anchor for the other ideas, & because it would consist of a lot of modules that would be reused for other levels (random spawner, responsive colliders, triggerable projectiles), I made it first.

Starting with the spawner, midway through a rough implementation, I realised I was rewriting a lot of the same code found in Measurable. So, before the spawner, I extracted the enumerator & ticking logic into Ticker & used that as a basis to trigger spawns at a fixed rate. This would eventually end up as a grid spawner. Whilst close, a keyboard isn't a strict grid, consisting of a variety of different sized keys. Evolving from that, & failing to find a way to implement custom editor instantiation functionality, I just used anchors as the spawner's children, of which the spawner would use their size & position to spawn.

Whilst even, even more close, it was here where I remembered box colliders have dimensions independent from their RectTransforms, enabling the user at this point to fall through some of the keys. A quick component to "maximise" the BoxCollider2D to the RectTransform would finally then, finish a keyboard. Using all these components, I reapplied them to the fun & pharmacy scenes, to complete a large chunk of the minigames.

All that in place, it... still felt really crappy to play. Everything felt, lifeless. I needed some physics, & the most satisfying effect I could think of was for the keys to be sorta elastic & bouncy. However, I had no idea how to implement it, even after searching online. After trying & trying many code snippets copied & pasted, eventually I kinda, accidentally created the effect. Adding onto that, the code made perfect sense too. Well, yeah, now I had fun AND satisfying minigames.

Routing

Now I had scattered levels, but no way to really swap in between them whilst persisting data. Looking through the Unity docs however, showed that it was possible to load multiple scenes at once. This gave me the idea to implement something sorta similar to web SPA's, where they would have an entrypoint page, & a hash router to switch between components nested inside of it. Eventually, this would look like a "main" scene, which contains the player & the measurables, routing between the game branches, which contains a player beacon to position the player in the level.

Awaiting animations

Whilst patching & fixing bugs, I found myself readjusting a lot of timings to destroy an object, so it happens right when the animation ends. This feels extremely icky, because the data is there, there must be some way to trigger an event or check for completion or something. Eventually, I stumbled across AnimationEvents, which were events that could be registered & triggered when a specific frame of an animation was reached. Creating a component which would register those events & expose them as regular C# events, along with a Await method would improve DX significantly, & even enable the infinitely useful SelfDestructOnAnimationEndBehaviour.

Asyncification

Callbacks; delegates, Callback hell; Unity's API.

Similarly to JavaScript's slow recovery away from callbacks, much of Unity's asynchronous programming relies on events & delegates. Similarly again to JavaScript, C#'s async/await programming style is a godsend to readability. However, converting the old ways of asynchronous programming to async/await is ugly, involving TaskCompletionSource, assignments, & even redeclaring the function signature. Abstracting it all away, again, similar to JavaScript's countless promisify helpers, eases the transition & interop between old & new. With all that said, this is exactly the Asyncify class, powering Aniwaitor.Await() & AsyncOperation.ToTask().

Bopping intro

I wanted something reminiscent of the old 8/16-bit demo scene, where the bopping animation would be extremely common on texts. Granted, they also had super cool brain melting effects, but I do not know how to write shader code, so that shall be shelved. Going in to implement it, I had in my mind that it sorta looked like the sprites moved along a sine wave, & with a few lines of code, plus a little messing about the frequency & amplitude, I did manage to recreate the effect somewhat using Mathf.sin(). Who knew math was at the core of many of man's spectacles?

Tutorials

For tutorials, there were a few prerequisites. The first being that Ticker's frequency had to have a way to be modified, in a way that would be global. The second being a way to emphasise some text over others, perhaps a smooth flicker effect. Then with both of those, a proper tutorial that would be dismissed after a click would be able to be made.

Augmenting Ticker

For this new functionality, there was already a sorta equivalent inside Unity — Time.timeScale. However, it wouldn't be the most direct way to augment Ticker to have it, since instances of Tickers aren't tied to any update mechanism of Unity by design, which meant they'd be ticking even if certain hooks are slowed down, like MonoBehaviour.FixedUpdate(). How I ended up implementing it was to modify the current handle-able tick counts per enumeration on Ticker according to Time.timeScale. This also means that it would properly discard ticks that would've otherwise be simply deferred & ran in batches after the deferral.

Smooth text flashing

Now, another problem with smooth transitions between 0 & 1. Whilst I could do some fancy tweening & what-not, I just couldn't shake the thought of how this could be just another Mathf.sin()-able problem. So, with a little copy & paste, as well as some swizzling of the code down to 1-dimension, plus targeting it to TMPro.TextMeshProUGUI.color.alpha, everything just basically worked.

The actual tutorial logic

The logic & spawning part was pretty simple, with the only tricky part being, how am I going to track if a user has dismissed the tutorial? Initially I went around pretty complicated, finding weak maps in C#, perhaps I'd have to clobber the router for a data prop, etc. At the end though GitHub Copilot actually made a suggestion that swept away everything I had in mind — gameObject.scene.buildIndex. With that, I could just store it in a static list on dismissal. Then if the scene was switched to, but the tutorial had already been marked as dismissed, I just destroy the tutorial in MonoBehaviour.Awake(), like nothing ever happened!

feedback:

  • 28/10/2022
    • Make sure the game is in 16:9, not free-aspect
    • Add hints & prompts to inform the player what to do & what's happening

final:

Figure 1.2.1, The final game, 4/12/2022

reflection:

This part of the combined assignment actually taught me a lot. It was surprisingly fun as well; can't ever get enough of C#! The cool creature comforts, new(), new {}, operator overloading, extension classes, mmm... Not only just the coding, being able to interact with & play the things written down; crafted, was extremely gratifying as well. Whilst I recognise it is not for everyone, & the struggles were definitely struggle-ful, overall I rate it.

The main & obvious thing learnt was coding. I mean, kinda hard not to, having to write in it. The extra engineering rabbit holes that sorta stray away from the main game though, was where I thought I learnt the most. As I needed to solve more niche problems, I had to explore more around how things worked & their limitations, including the language & the IL that powered it. Besides that, the modularity of all the components, & the usage of composition rather than inheritance is thoroughly swaying me away from OO in my own code. Whilst I was already super on the edge before this, such a modular pattern was extremely useful & superseded inheritance time & time again. We'll see if my OO module/class (educational context lol) next semester manages to sway me then.

At the end of the day, I learnt a lot that I'll definitely make use of again in the future. Who knows, maybe I'll make another game, maybe I won't. Maybe I'll start to use C# more, maybe I'll write my own language called D-flat. Spooky.

Comments