Building an Event-Driven Objective Tracking System in Unity
While developing an ambulance orientation application for paramedicine students, I was responsible for implementing an objective-tracking system to guide learners through various training scenarios. Each scenario required learners to locate specific pieces of equipment inside a virtual ambulance, with their progress displayed through an interactive checklist.
The objective tracker needed to support multiple scenarios, different difficulty modes, and the ability to restart or replay activities. Rather than tying the checklist directly to the gameplay logic, I used an event-driven architecture to keep the two systems independent.
Separating Gameplay from UI
The system was divided into two primary components: SeekAndFindManager and ChecklistSeekAndFind.
SeekAndFindManager handled the gameplay logic, including tracking the current scenario, validating equipment interactions, and managing progression through the activity. ChecklistSeekAndFind was responsible for displaying objectives and updating their visual states as learners completed them.
Communication between these components was handled through C# events. This allowed the gameplay manager to notify other systems when something happened without needing direct references to their implementations.
For example, the gameplay manager could dispatch events such as OnSeekAndFindSuccess, OnDisplayOutroModal, or OnDisplayAlert. Any system subscribed to those events could respond independently, whether that meant updating the interface, displaying a notification, or playing audio.
This separation made it easier to modify the gameplay and presentation systems without introducing unnecessary dependencies between them.
Defining Scenarios with ScriptableObjects
To make the objective system reusable, I used Unity's ScriptableObjects to define the equipment required for each scenario.
Each scenario was represented by a ScenarioSO, which contained a list of ScenarioEquipmentSO assets. These represented the individual pieces of equipment learners needed to locate.
Keeping scenario definitions separate from the gameplay code meant that the same objective-tracking logic could be used across scenarios without duplicating the underlying implementation.
When a scenario began, SeekAndFindManager.Initialize(...) established the active scenario and called PopulateEquipmentList() to prepare its required equipment.
This approach also supported different difficulty modes, allowing the application to adjust how objectives were presented without requiring an entirely separate gameplay system.
Updating the Checklist
When a learner interacted with a piece of equipment, HandleEquipmentClick(...) determined whether the interaction satisfied an objective.
Before marking an item as complete, the manager validated several conditions:
- The equipment had not already been found.
- The current gameplay mode permitted the interaction.
- The equipment name existed in the active scenario's equipment list.
Once the interaction passed validation, the corresponding checklist entry could be updated.
The checklist controller exposed a method called UpdateChecklist(string explorableName), which received the name of the completed equipment.
Rather than relying on a numeric identifier or a direct ScriptableObject reference, the checklist used the equipment name to locate its corresponding UI element.
Using Unity's UI Toolkit, the lookup was performed with a query against the checklist's visual hierarchy:
contentContainer.Query<VisualElement>("Container")
.Where(element => element.tooltip == explorableName);
After finding the matching element, the controller updated its appearance by changing its colour, displaying a checkmark, and adjusting its button state.
This worked well for the project's requirements, although it introduced a dependency on consistent naming between the scenario data and UI elements. If an equipment name changed without updating the corresponding interface element, the lookup could fail.
A dedicated identifier would be worth considering for a larger system, but name-based matching provided a straightforward solution for this implementation.
Managing State Across Scenarios
One of the more challenging aspects of the system was keeping the gameplay and UI states synchronized.
The application supported multiple scenarios, each with its own objectives. Learners could also restart activities, which meant that previously completed equipment and checklist entries needed to be returned to their initial states.
The difficulty wasn't caused by a single issue. It involved several interacting factors, including scenario transitions, event timing, and the checklist's name-based lookup.
For example, an equipment interaction could occur while the checklist was being rebuilt or its visibility was changing. If the UI wasn't ready to process the update, the displayed objectives could become inconsistent with the gameplay state.
To address this, I used a combination of explicit reset logic, state validation, and controlled gameplay transitions.
The HandleResetSeekAndFind() method cleared the relevant gameplay state, reset equipment interactions, and invoked the appropriate checklist reset method depending on the current mode.
The system also used AdvanceGameState() to move between defined phases, including initialization, conversation, active gameplay, and completion.
By controlling these transitions, the application could coordinate scenario setup and checklist updates through a predictable lifecycle.
Validation inside HandleEquipmentClick(...) provided another safeguard by preventing invalid or duplicate interactions from updating the objective state.
These measures were important because an event-driven architecture doesn't automatically guarantee that every subscriber is ready to process an event. The lifecycle of the systems receiving those events still needs to be considered.
Benefits of the Architecture
Separating gameplay, scenario data, and presentation provided several benefits throughout development.
Maintainability was one of the most immediate improvements. Gameplay logic remained in its respective managers, while checklist controllers handled the interface. Changes to the visual presentation didn't require restructuring the underlying objective logic.
Reusability came from the ScriptableObject-based scenario definitions. The same gameplay systems could support multiple scenarios and difficulty modes using different equipment configurations.
Extensibility was another advantage of the event-driven approach. Additional functionality could subscribe to existing events without modifying the systems that dispatched them. Audio playback, notifications, and completion screens could all respond independently to gameplay events.
Finally, reliability depended on the validation and lifecycle management surrounding those events. Coordinating initialization, resets, and state transitions helped keep the checklist consistent with the learner's actual progress.
What I Learned
Building this system reinforced the importance of considering state management when designing event-driven applications.
It's relatively straightforward to create an event and subscribe to it. The more difficult part is determining when those events should be dispatched, which systems should respond, and how their states remain consistent as the application changes.
The combination of C# events, ScriptableObjects, and explicit game-state management gave the objective tracker a structure that could support different scenarios without tightly coupling gameplay to the interface.
For future implementations, I'd also consider replacing the name-based UI lookup with stable objective identifiers. This would reduce the dependency on equipment names and make the system more resilient to changes in scenario data.
Ultimately, the value of the architecture came from more than simply using events. It came from defining clear responsibilities between systems and managing their interactions throughout the application's lifecycle.
- Jordan Partridge