The Last Flame is an narrative-driven adventure game set in the arctic of Eryndor. Worked on AI, EventBus and cutscene management, developed reusable Unity Packages.
C#,
Unity,
Design Patterns,
Custom Unity Packages
A project where I truly felt at home in Unity. For this project I worked on AI, Cutscene Management and making an EventBus for modular cross system communication. For the Behaviour Trees and EventBus I eventually set up custom Unity Packages so the code can easily be reused in future projects.
Behaviour Tree
For AI I decided to continue the development of my Behaviour Tree from a previous project. Making improvements to overall structure, modularity and performance of the implementation. The Behaviour Tree as most implementations is built upon a graph structure and its control flow is evaluated left to right using a depth-first search. The tree’s flow can be altered using different predicates for the decorators and different sequencing nodes.
The tree’s structure is specified by passing the root node as an argument to the tree’s constructor. The tree then constructs a blackboard and sets the blackboard references in all of the child-nodes. The nodes communicate using the BlackBoard structure which stores all entries in a type-erased dictionary, this way all objects can be stored together in one dictionary for simplicity while still being accessible through a string as a key. One potential optimization here would have been to key entries based on their typeid rather than a string. This is quicker than hashing a string, which is essentially a non-determinate number of integers/chars which would lead to multiple hash-calls and hash-combines. Comparatively, hashing a System.Type is far quicker because it has a pre-cached hash given to it at compile-time based on its type. This however was ultimately not implemented as it would’ve made storing multiple entries of the same type impossible. Furthermore BlackBoard accesses never proved to be a significant bottleneck during profiling.
Human tracking and avoiding staying too close to the player.
For communication between different systems I opted to implement the EventBus pattern, which I’ve really come to greatly appreciate. EventBuses help alleviate many of the common problems of tight coupling between systems by rather than giving systems direct access to each other they leverage events to give and receive information and commands from other systems.
To subscribe to an Event any class can create an instance of an ArgumentEventHandler or a NoArgumentEventHandler. The type parameter dictates the type of the Event to listen to. Alongside the type parameter the constructor for the EventHandler also takes in a delegate to call when an event of type is published. Based on whether the class uses the Argument or NonArgumentEventHandler the subscribed delegate can either take the event as a parameter or choose to ignore the data of the event, only using it as a sort of “notification”
Events can be created by simply inheriting from the Event interface. The events themselves can contain any amount of arbitrary data, or none at all. Internally in the EventBus all EventHandlers are kept in a Dictionary that is keyed based on the EventHandlers event type and stores event handlers of that type in a HashSet to avoid storing multiple instances of the same EventListener.
One potential improvement to the EventHandler I have not yet had the time to implement would be to move it away from static memory and to break it into a “scope agnostic” reusable object instead. This would allow for the construction of multiple EventBuses rather than having one global one. This is good not only for code-reusability, as it would benefit other systems but also has some quite interesting gameplay implications as it could function in a smaller scope. Meaning an EventBus could be shared only among Enemies to for example indicate they should target another entity. Or a player specific EventBus in a multiplayer game so only events for that specific player take place.
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;namespaceringo.EventSystem{publicstaticclassEventBus{privatestaticDictionary<Type,HashSet<IEventHandler>>_handlers=new();[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]privatestaticvoidInitializeOnLoad(){_handlers=new();}publicstaticvoidSubscribe<T>(IEventHandlerhandler)whereT:IEvent{if(handler==null){Debug.LogWarning("[EventBus] Subscribed to null subscriber");return;}if(_handlers.ContainsKey(typeof(T))){_handlers[typeof(T)].Add(handler);return;}varnewHandlerList=newHashSet<IEventHandler>{handler};_handlers.Add(typeof(T),newHandlerList);}publicstaticvoidUnsubscribe<T>(IEventHandlerhandler)whereT:IEvent{if(!_handlers.ContainsKey(typeof(T))){Debug.LogWarning("[EventBus] Could not find event type to unsubscribe");return;}_handlers[typeof(T)].Remove(handler);}publicstaticvoidPublish<T>(T@event)whereT:IEvent{if(!_handlers.ContainsKey(@event.GetType()))return;// Saves a snapshot of handlers to prevent concurrent modificationvarhandlers=newHashSet<IEventHandler>(_handlers[@event.GetType()]);foreach(varhandlerinhandlers){handler.Handle(@event);}}}}
In order to be able to reuse these Systems for future projects I also decided to package them up as separate Unity Packages for the Behaviour Tree and Event Listener respectively. While this process was a bit unfamiliar to me, learning about how the Unity Package pipeline works and how to create reusable units was very interesting to me and when I got the hang of it I ended up creating packages for some more of my projects.
Reflection
The Last Flame has been an incredible learning opportunity for me, allowing me to truly spread my wings in Unity regarding systems design and applying clean code practices and design patterns. My biggest takeaway from this has definitely been the benefits and importance of modularly designing code in reusable parts rather than a monolith. Having set up the Unity Packages for Behaviour Trees and EventBus which I have used countless times in projects after this one for fast iteration it has been truly one of the best uses of my time and I am looking forward to keep on developing my different Unity Packages.