Implementing Delays in a Dialogue System for SMTH inc.
One of the main features of my game, SMTH inc., is the dialogue system, where players can interact with colleagues via a messaging interface. To enhance immersion, I quickly sought to implement a delay between the moment the player sends a message and the moment the character responds.
The three steps of the feature I want to implement
Version 1: Using setTimeout and Encountered Issues
The initial idea for implementing this feature was to use setTimeout, a JavaScript function that adds a delay before executing instructions. The advantage of setTimeout is its simplicity: it only takes two arguments:
- - The function to call after the delay.
- - The delay in milliseconds.
With this, I quickly implemented a functional chat system with delays !
_
However, I soon encountered a series of unexpected bugs:
- - When multiple characters responded to the player, some messages would disappear.
- - If the webpage was refreshed during the wait, the message would never arrive, leaving the player stuck.
Additionally, I wanted to enhance the feature further by adding an intermediate 'isTyping' state, where a small message like 'So-and-so is typing' would appear for a few seconds under the chat, similar to modern messaging systems. The functionality's design then evolved as follows:
The new feature schema with a new isTyping step
Revisiting the Issues and Challenges to Evolving the Feature
First Problem: Messages Disappearing
A fundamental problem with setTimeout is that it does not easily handle concurrency. The program's state and the values used during function execution are frozen at the start of execution and do not update if something changes during the waiting period.
With setTimeout, the state used for the update is the value when the function was first called, not when the callback is called after the delay
Imagine a situation where the player simultaneously responded to Emily and Lance and is waiting for their answers. The game's state used for both conversations is frozen by setTimeout when the function is first called.
- - Since Lance’s response time is shorter, the callback function updates this state at the end of its execution.
- - However, when Emily responds, the game state used is still the initial state and not the updated one with Lance’s response.
Second Problem: Messages Never Appear When setTimeout is called
JavaScript execution must continue smoothly until the delay expires for the callback function to be executed. However, in a web application, this is not guaranteed. At any moment, the user could refresh the page or simply leave it. In such cases, with my naive implementation, the player never receives the message and becomes stuck if the response is necessary to progress in the game.
Third Problem: Iterating for the New Feature
The new feature involving two successive states (isWaiting and isTyping) during the NPC’s message wait time risks exacerbating the issues mentioned above. If we keep the same design with setTimeout, it means nesting two setTimeout calls!
At this point, I decided these bugs were too significant to ignore and opted to completely redesign the feature.
Version 2: Using Timestamps
If I summarize the critical needs of the feature in terms of acceptance criteria:
- - The event management system must know the current, up-to-date game state at the moment it updates it.
- - In case of a page refresh or if the user leaves the site, the pending message must not be lost.
- - To maintain a simple design, the system should function iteratively rather than recursively, avoiding nested operations.
Based on these criteria, I developed a new design based on a timestamp system with a straightforward operation.
The New System
Each planned state change is added to an array with an associated timestamp.
- - A loop checks every second whether any of the timestamps have been exceeded.
- - If so, the change is applied, considering the current game state rather than the initial state.
The new event management system design with the storage of event in the local storage and a check every second
Let’s revisit the acceptance criteria:
Knowing the Current State at the Moment of the State Change
- At the time of the timestamp check, if the current date exceeds the timestamp, the program consults the current state to update it. If two events simultaneously have surpassed timestamps, the second event will take into account the state changes from the first. This is achieved by synchronously updating localStorage during the first check and before the second.
Protecting Against Page Refresh
- The queue of pending events is stored alongside the game state in localStorage. A page refresh or site exit does not clear localStorage, so the player’s progress is preserved without losing any information.
Maintaining a Simple Iterative Design
- This new system does not involve nested events. It doesn’t even need to know whether the messages belong to the same conversation or what the previous state was because that is outside its scope. It applies state changes agnostically, greatly simplifying the system.
Risks of the New System
- Rendering Management. Rigorous management of renders is necessary to avoid infinite render loops caused by frequent state-change checks.
- Slight Latency. The timestamp will never be perfectly respected, as the event will always trigger at the first check after the delay expires. In my case, this isn’t an issue but could be for other functionalities.
Conclusion
I’m very satisfied with this new system, which efficiently manages the game’s events. Its agnostic implementation allowed me to use the same system for dialogues, interface appearance changes, player score updates, and more. Today, I have a modular event management system for my entire game!