Script Design Notes — How Much of Game AI Should the Runtime Handle? (Part 2)
In the first article , I wrote about why I introduced action requests in Mana, a scripting language I develop as a personal project. The idea was to let the runtime preserve an interrupted action and resume it later, instead of making each gameplay system manage that execution state itself. This ti

Posted on Oct 25 • Originally published at qiita.com
In the first article, I wrote about why I introduced action requests in Mana, a scripting language I develop as a personal project.
The idea was to let the runtime preserve an interrupted action and resume it later, instead of making each gameplay system manage that execution state itself.
This time, I want to look more closely at what that means for game AI.
By game AI, I mean the logic behind enemy and NPC behavior: patrolling, reacting to a hit, talking, and deciding what to do next.
There are two questions I want to separate: how an action continues executing, and whether continuing that action still makes sense.
A game often has several things happening at once.
A guard patrols while another NPC talks. A door opens while enemies keep moving. An event controller waits for one character to finish an action while the rest of the scene continues.
For these situations, I found it useful to think of each participant as owning its state and behavior, asking other participants to act when necessary.
In Mana, a participant is an Actor, and a named behavior belonging to it is an Action.
A guard might have patrol, talk, and damage Actions. An event controller can request a conversation without knowing which line of the patrol script the guard has reached.
The Actor performing an action owns the progress of that action.
The Actor Model helped shape this way of dividing responsibilities.
The Actor Model describes independent entities that coordinate through messages.
Mana borrows the idea of asking another participant to do something while leaving that participant responsible for its own execution.
However, Mana is not a conventional implementation of the Actor Model.
Its priority levels, action interruption, and resumption rules are mechanisms I added for gameplay scripting. Mana also supports global variables, so it does not enforce a design in which all state is isolated inside Actors.
The distinction matters: the priority and resumption rules below describe Mana, rather than properties guaranteed by the Actor Model in general.
In Mana, a larger number means a higher priority.
const int kPatrolPriority = 1; const int kDamagePriority = 5; actor Enemy { action main () { request ( kPatrolPriority, self -> patrol ()); } action patrol () { print ( "Enemy: Patrol A \n " ); request ( kDamagePriority, self -> damage ()); print ( "Enemy: Patrol B \n " ); } action damage () { print ( "Enemy: Ouch! \n " ); } } Enter fullscreen mode Exit fullscreen mode This is Mana code; I am using C++ syntax highlighting because the syntax is similar.
self refers to the current Actor. To keep the example focused on execution order, the enemy requests its own hit reaction during the patrol. In a game, a system detecting a hit could issue that request instead.
Enemy: Patrol A Enemy: Ouch! Enemy: Patrol B Enter fullscreen mode Exit fullscreen mode After patrol prints its first message, the higher-priority damage Action interrupts it. When damage finishes, patrol continues with the statement after the request.
Priority 1: patrol prints "Patrol A" | | Request damage at priority 5 v Priority 5: damage prints "Ouch!" | | damage finishes v Priority 1: patrol resumes and prints "Patrol B" Enter fullscreen mode Exit fullscreen mode This example only prints messages. It does not move a character or play an animation.
The important detail is that damage contains no code to return to patrol.
The hit reaction does not need to know whether it interrupted patrolling, talking, or something else. The VM preserves the interrupted execution state and resumes it afterward.
State machines remain useful for organizing game AI.
States such as patrolling, alert, and fighting make the character's current situation explicit. They provide a clear place to describe the conditions for switching behavior.
Temporarily interrupting a behavior and later continuing it requires another kind of information: how far that behavior has progressed.
Hierarchical state machines, state stacks, and coroutines can also represent this. Mana places the interruption and resumption mechanism in the VM, with execution state organized by priority.
That still leaves an important gameplay question unanswered.
Suppose the enemy spots the player while reacting to a hit. Once the reaction ends, should it continue patrolling? Perhaps it should start fighting instead.
Being able to resume a piece of code and deciding that its behavior is still appropriate are separate responsibilities.
The VM remembers where execution stopped. It does not, by doing so, determine what the character should do in a changed situation.
For my purposes, a useful division seems to be:
Moving execution management into the runtime reduces some bookkeeping. The game still needs rules for choosing behavior.
There is another aspect of this execution model worth clarifying.
The Actor Model does not require a dedicated thread for every Actor. Mana does not create one either.
The VM advances Actors in turn, allowing their behaviors to make progress over time:
VM update -> Advance Actor A -> Advance Actor B -> Advance Actor C Enter fullscreen mode Exit fullscreen mode Mana's documentation describes this as cooperative pseudo-parallel execution.
Scripts can also use yield() to give control back to the VM, spreading an action across multiple advances of the runtime.
I chose this approach because I wanted the execution model to fit into a game's update loop. The goal was to advance each participant's behavior alongside the game, rather than execute every Actor simultaneously on the CPU.
Execution order still matters, though. When a request is issued, and how far its target has already progressed, can affect behavior.
For an event that needs a specific sequence, yielding control is not enough to establish that another Actor's action has finished. That is where explicit waiting, such as the await described in the first article, becomes useful.
Letting the VM manage interruption can make individual Actions easier to read.
But understanding the Actor as a whole can become harder as more requests interact.
For example, an Actor might have this execution state:
Priority 5: damage is running Priority 3: talk is pending Priority 1: patrol is suspended Enter fullscreen mode Exit fullscreen mode When damage finishes, patrol does not immediately resume. The pending talk Action has a higher priority and runs first.
Each Action may be simple on its own, while the combination produces behavior that is difficult to infer from any one definition.
The one-request-per-priority restriction discussed in the first article also matters here. If damage already occupies a priority slot, another request at that priority cannot be queued behind it.
Assigning a priority to hit reactions therefore does not fully specify how repeated hits should behave. The game still needs to decide what those events mean and how to handle them within the available execution model.
This makes me think that better runtime visibility would be valuable. I would like to make it easier to inspect:
A language feature can make behavior easier to write while also making its interactions harder to observe. The tools for understanding that behavior deserve attention alongside the execution model.
Mana combines the idea of independent participants exchanging requests with gameplay-specific rules for priorities, interruption, and resumption.
In the patrol example, the VM takes responsibility for preserving interrupted execution and determining when it can continue. The game retains responsibility for deciding whether that continuation is appropriate.
I think that division can reduce the amount of execution bookkeeping in gameplay scripts. It also leaves real design work around changing situations, competing requests, and repeated events.
That is the boundary I am still exploring: how much the runtime should remember and schedule, and how much the game should decide.
To experiment with the execution rules, try the Request example in the Mana Playground. Lower the alarm priority below the conversation priority and watch how an interruption becomes deferred work.
The implementation is available in the Mana repository. Connecting this VM to a game's update loop is another part of the design I want to explore in this series.