Interactive in-game tutorials often interrupt game flow and override standard behavior in a way that can be difficult to implement cleanly.
For example, imagine we have an action game and we want a tutorial to play when the player first takes damage:
(When player first takes damage) [Pause Gameplay and zoom to player] Dialog: "You just took damage!" Dialog: "When you take damage, your health depletes:" [Highlight health bar, animate health dropping] Dialog: "Let's drink a potion to restore some health! Press I to open your inventory." (Wait for player to press I) [Show Inventory screen] Dialog: "Click a potion to drink it!" (Wait for player to click a potion) Dialog: "Using a potion consumes it and restores some health." [Remove one potion from inventory. Highlight health bar, animate health restoring]
A common way I see this implemented is by scattering conditionals throughout the code, e.g.:
public class Player : MonoBehavior {
//...
void OnCollisionEnter(Collision collision) {
var damageSource = collision.gameObject.GetComponent<IDamageSource>();
if (damageSource != null) {
HP -= damageSource.Damage;
var tutMan = TutorialManager.Instance;
if (tutMan.Active && tutMan.CurrentStep == TutorialStep.WaitingForDamage) {
tutMan.GoToStep(TutorialStep.PlayerTookDamage);
} else {
PlayPainSound();
}
}
}
}
This approach can end up very messy, as it creates coupling, pollutes the gameplay code with tutorial logic, and adds performance overhead for checking if a tutorial is currently active.
We can reduce coupling using events, but that doesn't as easily let us skip or modify default behavior:
void OnCollisionEnter(Collision collision) {
var damageSource = collision.gameObject.GetComponent<IDamageSource>();
if (damageSource != null) {
HP -= damageSource.Damage;
tookDamageEvent.Invoke(this, damageSource); //tutorial manager listens for this
//we can no longer skip this code:
PlayPainSound();
}
}
One potential alternative would be to create tutorial-specific components, such as TutorialPlayer
, that extend the normal class and override with tutorial-specific logic where needed. However, this would require us to maintain separate prefabs with the tutorial-specific components, or be able to dynamically swap the components at runtime.
Another potential alternative is to add temporary components that have a higher execution order and block the default behavior, e.g.:
[RequireComponent(typeof(Player))]
public class TutorialPlayerCollisionDetector : MonoBehavior {
void OnCollisionEnter(Collision collision) {
var damageSource = collision.gameObject.GetComponent<IDamageSource>();
if (damageSource != null) {
var tutMan = TutorialManager.Instance;
if (tutMan.Active && tutMan.CurrentStep == TutorialStep.WaitingForDamage) {
GetComponent<Player>().DisableCollisions();
tutMan.GoToStep(TutorialStep.PlayerTookDamage);
Destroy(this);
}
}
}
}
However, that approach can only interrupt Unity event functions such as OnEnable()
and OnCollisionEnter()
, not direct function calls.
Is there a standard pattern for implementing interactive tutorials that interrupt or replace normal game logic?