It has been a while since my last update. This is mostly due to a design pattern overhaul and working on bigger systems of the game. In this sprint I have been working on:
With the increasing size and complexity of the project, ways of organising the code into sensible chunks become more and more valuable. I have been reading up on and trying out a number of software development design patterns. Unity especially offers a variety of ways in which specific functionalities can be implemented, and these are often vigorously advocated. Some advocate using Singletons and MonoBehaviours, whereas others will tell you to focus on using ScriptableObjects, and yet others will tell you to do everything with events. There seems to be a constant and unresolved debate regarding composition versus inheritance and if to implement any or a combination of acronyms such as MVC, MVP, DRY, KISS etc..
During my work, I have refactored my project to test out a number of different approaches, and the best approach I have found is to use Singletons and events to open communication channels between parts of the game and using a Model-View-Presenter approach for the UI. First, I am now using a multi-scene approach, where the first scene that loads contains all the managers and singletons. This is the only place singletons live and these get loaded before anything else happens in the game as to ensure there are no racing conditions.
These managers relay information throughout the app using C# events. This allows some decoupling of the code and allows me to build up modular parts that hook into the various events. An example would be the Location Provider Manager: this provides the user’s location information to the rest of the game. Every time the user’s location changes (either Lat/Lng or H3Index), this manager emits a LocationChanged event. Now, all systems that need to know about changing locations (e.g. Maps, Quests etc.) can listen for this event and act accordingly. In other words, the managers can be seen as global event channels with custom manager-specific logic.
For the UI, I have strictly implemented an MVP approach. This allows me to decouple the data layer (Model) from the visualisation (View) and the logic (Presenter). The presenter is in charge of talking to the model and the view and telling each one what to do. The presenter listens for events from the model and the view and acts accordingly. This way, the model and the view never directly “talk” to each other, meaning the visualisation and data are nicely separated. The Presenter, View and Model are all components that live on a GameObject. Thus, when we access the Model or View from the presenter, we want to make sure we have a reference to them.
This approach does introduce a bit of overhead for very simple UI screens but is well worth it for more complex pages with a bunch of custom logic.
Spatial Features and Landcover System
I had to abandon the previous idea of saving everything in specific H3 tiles. The previous approach did segment the world into hexagons and each hexagon contained all information needed. However, it started to become messy with increasing amounts of information that needed to be stored. I then decided to scrap that idea and came up with a new approach. Now, everything is still linked to H3 indices, but more loosely. For example, information with a spatial component gets stored with its respective H3 index at a given resolution instead of being stored as child data of a given H3 tile. When the player then moves around the game world, I can query for information in the player’s vicinity using H3 indices and only download and display relevant information.
Along with the new spatial approach, I also implemented a new land cover system. The idea is, that to procedurally generate the game world and interesting content in the future, I will need some landcover information. The problem is, that land cover information is needed at quite a small scale to be meaningful, and generating this on the fly can be computationally expensive. I implemented the following pipeline:
For each H3 region at a specific scale, get all the H3 child areas (smaller scale H3 indices) and for each of these, perform:
- Check to see if there is land cover information on the device. If yes continue with step 4, if no continue with step 2
- Figure out which Slippy Map tile the H3 tile is in. If the Slippy Map tile is saved on the device, continue with step 4, if no continue with step 3
- Download the respective tile from MapBox
- Save the MapBox tile image on the users device
- Get the centroid coordinates of the H3 hex and figure out the Slippy Map image pixel colour at that location (if the coordinates fall on an edge pixel, offset to get rid of edge effects)
- Convert pixel colour to landcover class
- Save the information as a JSON on the user’s device.
Global Modular Questing System
One of the biggest systems I have worked on is the development of the questing system. This system is needed to allow me to create content for the game. Specifically, this is how I will introduce crowdsourcing and educational tasks alongside more traditional game quests. This system was highly complex to develop seeing the needed modularity and the various types of quests needed. The system is split into three main categories: quest points, quests and quest steps. In addition, the system I developed also integrates a quest log, a quest archive and ties everything together with a quest manager. The individual quest points, quests and quest steps are defined as templates to be as modular as possible. These templates are then used to create the individual objects. This is important seeing that we potentially want multiple quest points to start the same quest or multiple quests to include the same quest step etc…
Quest points are defined locations in the game where quests can be accepted from. I have implemented quest points to host various options, including the visual representation of the quest point, the distance that a player can interact with a quest point, the available quests at a specific quest point and so on. In future iterations, these quest points will also be procedurally generated, to offer an entertaining experience to all.
Quests are a collection of tasks that form a mission in the game. Quests can be anything, from an educational journey, over crowdsourcing, to helping an NPC. There are various types of quests in terms of repetition. For example, some quests can be done once and only once globally, others can be repeated in different locations and yet others can be done once a day.
Finally, we have the quest steps. These are the individual steps or tasks that make up a quest. Quest steps can include anything from “Go to a specific location”, over “upload a description” to “collect 5 gems”. Quest steps can have specific requirements (e.g. only available in particular locations at specific times) and can be implemented to form a story (e.g. first complete step 1, then continue to step 2 and so on). Combining quest steps allows us to implement just about any quest in the game.
To tie all the quest relevant functionalities together, I implemented the quest manager that acts as an mediating layer between the various components. The quest manager listens for and emits specific events, which trigger various functions. The various states of the quest and the individual quest steps is constantly saved and persisted to the backend firestore database. This means that a user can close the app and when they open the app, the quests will be in the same state.
Extra: Quest System Demo
As a special extra, I have recorded a quick video showing how the quest system works. I think this might make it more clear what this system actually looks and feels like.