Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Architecting Data-Driven Event Systems

Hello! Welcome to the next step in building our JRPG engine.

In our last lesson, we built a SceneManager that lets us transition between different game states, like moving from the map to a menu. This is a critical piece of the puzzle, but our scenes themselves are still static. A JRPG world is defined by its stories, characters, and interactions. Today, we'll design the system that makes all of that possible.

This lesson directly addresses the learning outcome: Design the architecture for a data-driven event system, including event triggers, commands, and an interpreter. We will focus entirely on the architectural concepts, laying a robust foundation before we write a single line of implementation code. This "design first" approach is crucial for building complex, scalable systems and aligns perfectly with your goal of understanding how game engines truly work under the hood.

1. The Philosophy: Why "Data-Driven"?

Before we design the "how," let's establish the "why." Why is a data-driven event system the industry standard for RPGs? The alternative is to hard-code every interaction. Imagine writing JavaScript like this for every NPC:

// Hard-coded approach - DO NOT DO THIS!
if (playerInteractsWith.id === 'npc_king') {
    if (!game.flags.has('quest_started')) {
        showDialogue("Welcome, hero! Will you accept my quest?");
    } else {
        showDialogue("Have you defeated the dragon yet?");
    }
} else if (playerInteractsWith.id === 'npc_guard') {
    showDialogue("Sorry, the castle is closed.");
}

This quickly becomes an unmanageable mess. A data-driven approach separates the logic (the event interpreter) from the content (the events themselves). Instead of code, an event becomes a piece of data, often a JSON object, that describes what should happen.

Data Driven (Scripts aren't Data) #rpg

To understand the importance of this separation, let's hear from a veteran RPG developer. Mark Darrah, former Executive Producer for the Dragon Age series, has a strong take on this. This video explains why treating scripts and events as data is fundamental to building large-scale RPGs.

Please watch the first three sections, from the beginning until 05:23. Pay close attention to his distinction between 'scripting' as a form of hard-coding and 'data' as structured, reusable content.

As Darrah argues, a data-driven system gives us:

  • Reusability: The same Show Text logic can be used for thousands of different lines of dialogue.
  • Accessibility: Game designers who aren't programmers can create and modify complex scenes by editing data files (like in RPG Maker's event editor).
  • Maintainability: If you want to change how all dialogue boxes look, you change it in one place—the renderer—not in hundreds of hard-coded event scripts.
  • Consistency: Templated data ensures that similar game elements behave in predictable ways for the player.

Our goal for this lesson is to design an architecture where a complex cutscene can be defined purely as a JSON object, with no custom JavaScript required.

2. Anatomy of a JRPG Event

Let's dissect a classic JRPG interaction: opening a treasure chest.

  1. The player walks up to the chest and presses the action button.
  2. A sound effect plays.
  3. Text appears: "You found a Potion!"
  4. A "Potion" is added to the player's inventory.
  5. The chest's appearance changes to look open.
  6. If the player interacts with it again, the text says: "It's empty."

This simple sequence reveals the core components we need to design:

  • Event Triggers: What initiates the event? (e.g., pressing a button on a specific tile, stepping on a trigger tile, an event that runs automatically when the map loads).
  • Event Commands: The individual steps of the event sequence (Play Sound, Show Text, Add Item, Change Self Switch). These are the building blocks of our events.
  • Event Interpreter: A system that reads a list of commands and executes them one by one, waiting for each to finish before starting the next.
  • Game State (Flags/Switches): A way to track progress, so the game knows the chest has already been opened.

3. The Core Architectural Patterns

Your background in software development will be very useful here, as our event system will be built upon well-established design patterns.

Event Commands as Objects: The Command Pattern

Each action in our event sequence—Show Text, Move Character, Give Item—can be represented as an object. This is a classic application of the Command Pattern.

Notes on Game Programming Patterns by Robert Nystrom

These notes on Robert Nystrom's book 'Game Programming Patterns' provide a concise summary of the Command Pattern and its use cases in games.

Read the section 'Chapter 2: The Command Pattern'. Note how it decouples the requester of an action from the object that performs the action.

By encapsulating each request as an object, we can create a simple list of commands for our interpreter to process. A ShowText command object might look like this:

{
  type: "SHOW_TEXT",
  text: "Hello, world!",
  portrait: "hero_face.png"
}

The EventInterpreter doesn't need to know the details of how to show text. It just sees type: "SHOW_TEXT" and delegates the work to a specialized handler for that command type. This makes the system incredibly extensible: to add a new ScreenShake command, you simply create a handler for it; the core interpreter logic doesn't change.

The Interpreter as a Virtual Machine: The Bytecode Pattern

If an event is a list of command objects, what does that make our interpreter? In essence, it's a virtual machine (VM), and the list of commands is its bytecode.

This might sound complex, but it's a powerful analogy. You're not building a full CPU-level VM, but a very simple, domain-specific one. Its only job is to execute the "instructions" (our command objects) that define your game's behavior.

Notes on Game Programming Patterns by Robert Nystrom

The same notes on 'Game Programming Patterns' also cover this concept.

Now read 'Chapter 11: The Bytecode Pattern'. The example of a stack machine is more low-level than we need (we can use JSON instead of raw bytes), but the core idea is the same: encode behavior as data that can be easily shipped, modified, and interpreted.

This is precisely how RPG Maker works. Its visual event editor is just a user-friendly way of creating a list of bytecode instructions that its game engine interprets at runtime.

The Big Picture: Event-Driven Architecture

When we put all these pieces together—sources, triggers, commands, interpreters—we are building an Event-Driven Architecture (EDA).

Event-Driven Architecture - System Design

This article from GeeksForGeeks provides a formal overview of EDA and its components. This will help us put standard names to the pieces we're designing.

Read the introduction, the 'Events in Event-Driven Architecture' section, and the 'Components of Event-Driven Architecture' section. As you read, try to map their terminology to our JRPG context.

Here's how the EDA components map to our JRPG:

  • Event Source: An NPC, a treasure chest, or a trigger tile on the map.
  • Event: The data object containing event details, including the list of commands.
  • Publisher/Dispatcher: The part of your game that detects a trigger (e.g., the input handler detects a button press facing an NPC) and sends the event to the interpreter.
  • Subscriber/Handler: Our EventInterpreter class. When an event is published, the interpreter "subscribes" to it, taking control of the game loop until the event is complete.

4. Designing the Data Structures

With the patterns in mind, let's design the JSON structure for an event. A good data design is the heart of a data-driven system. We'll design this to be part of our map data.

Imagine an NPC on the map. In our map's JSON file, that NPC could be defined like this:

{
  "id": "old_man_01",
  "x": 10,
  "y": 15,
  "sprite": "npc_sage.png",
  "event": {
    "trigger": "on_action",
    "pages": [
      {
        "conditions": { "switch": "main_quest_started", "value": false },
        "commands": [
          { "type": "show_text", "text": "A dark energy is growing in the east. You must stop it!" },
          { "type": "set_switch", "switchId": "main_quest_started", "value": true }
        ]
      },
      {
        "conditions": { "switch": "main_quest_started", "value": true },
        "commands": [
          { "type": "show_text", "text": "Be careful on your journey." }
        ]
      }
    ]
  }
}

Let's break this down:

  • trigger: Defines how the event starts. on_action means the player must press a button while facing the NPC. Other types could be on_player_touch or autorun.
  • pages: This is a key concept borrowed from RPG Maker. It's an array of possible event sequences. The interpreter will only run the first page whose conditions are met.
  • conditions: This determines if a page can run. Here, it checks the state of a global switch (a boolean flag). This is how we give the NPC different dialogue before and after the quest starts.
  • commands: This is our "bytecode"—an array of command objects that the interpreter will execute in sequence.
Test your understanding!

Using the data structure above, how would you write the JSON for a treasure chest that contains a 'Sword'? The first time you open it, it gives you the sword and turns on a switch called chest_001_opened. The second time, it just says "It's empty."

Show answer
{
  "id": "treasure_chest_001",
  "x": 20,
  "y": 5,
  "sprite": "chest.png",
  "event": {
    "trigger": "on_action",
    "pages": [
      {
        "conditions": { "switch": "chest_001_opened", "value": false },
        "commands": [
          { "type": "play_sound", "sfx": "treasure" },
          { "type": "show_text", "text": "You found a Sword!" },
          { "type": "add_item", "itemId": "sword", "quantity": 1 },
          { "type": "set_switch", "switchId": "chest_001_opened", "value": true }
        ]
      },
      {
        "conditions": { "switch": "chest_001_opened", "value": true },
        "commands": [
          { "type": "show_text", "text": "It's empty." }
        ]
      }
    ]
  }
}

Note: We would also need a command to change the chest's sprite to an open one, but that's a detail for later implementation.

5. The Interpreter's Logic Flow

Finally, let's outline the high-level logic of the EventInterpreter. This is not code, but a blueprint for the class we will build in the next lesson.

The EventInterpreter will be a stateful object. When not active, it does nothing. When an event is triggered:

  1. Activation: The interpreter receives the event object (from the NPC or chest).
  2. Page Evaluation: It iterates through the pages array and finds the first page where conditions are met.
  3. Initialization: It sets an internal pointer, let's call it commandIndex, to 0. It is now in an "active" state.
  4. Game Loop Takeover: While the interpreter is active, it blocks normal player input and movement. The main game loop will call eventInterpreter.update() instead of player.update().
  5. Execution Cycle: On each update call:
    a. It gets the current command: const command = page.commands[this.commandIndex];
    b. It starts executing the command (e.g., tells the UI to display a text box).
    c. Crucially, it must handle asynchronous commands. A ShowText command doesn't finish instantly. The interpreter must enter a "waiting" state until it receives a signal that the command is complete (e.g., the player pressed a button to close the text box). Similarly, a MoveCharacter command requires waiting until the character reaches their destination.
    d. Once the current command is complete, the interpreter increments commandIndex.
    e. If commandIndex is now past the end of the commands array, the event is over.
  6. Deactivation: The interpreter resets itself to an "inactive" state and returns control of the game loop to the player.

Conclusion

We have now designed a complete, robust architecture for a data-driven event system. By separating content from logic and leveraging powerful design patterns, we have a blueprint for the very heart of your JRPG's storytelling engine.

Key Takeaways:

  • Data-Driven Design is essential for creating scalable and maintainable RPGs. Events should be defined as data (JSON), not hard-coded logic.
  • The Command Pattern provides a clean way to represent individual event actions (Show Text, Move Character) as extensible objects.
  • The Event Interpreter acts as a simple Virtual Machine that executes a list of commands, which can be thought of as its Bytecode.
  • Our system fits within a formal Event-Driven Architecture, with triggers, sources, publishers, and subscribers.
  • A well-designed data structure using pages and conditions is key to creating stateful, dynamic events.
  • The interpreter's logic must handle asynchronous commands by waiting for them to complete before proceeding.

Preview of the next lesson:

With this comprehensive design in hand, we are ready to bring it to life. In our next lesson, "Implement an event interpreter that processes a sequence of commands," we will write the JavaScript classes for the EventInterpreter and a few core commands. You'll see how the architectural elegance of these patterns translates into clean, effective code.

Can't find a good explanation? Sign up and we'll make it for you

Sign up