跳到正文
DEV Community· Shun Moriya·· 4 小時前AI 評分33

Mana 腳本設計筆記:遊戲 AI 有多少應由執行環境處理?(第二部分)

Script Design Notes — How Much of Game AI Should the Runtime Handle? (Part 2)

AI 導讀

Mana 將遊戲 AI 動作的中斷與續行交由 VM 管理,並按優先級保留執行狀態。Actor 恢復執行後是否仍應沿用原有行為,仍須由遊戲邏輯判斷;VM 不會替角色決定新情況下應採取的行動。Mana 由 VM 輪流推進各 Actor,實現協作式偽並行執行,而非為每個 Actor 建立專用執行緒。

正文

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.

Let Each Actor Own Its Progress

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.

What Mana Borrows from the Actor Model

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.

Concern Mana's approach
Organizing behavior Actors own state and named Actions
Interacting with another Actor Request an Action with request
Choosing which Action runs Use priorities to interrupt or defer execution
Preserving interrupted work Keep execution state for each priority level
Advancing Actors Let the VM run them in turn

The distinction matters: the priority and resumption rules below describe Mana, rather than properties guaranteed by the Actor Model in general.

A Hit Reaction in the Middle of a Patrol

Consider an enemy with two Actions:

  • patrol, its ordinary behavior, at priority 1.
  • damage, its hit reaction, at priority 5.

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");
    }
}

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.

The expected output is:

Enemy: Patrol A
Enemy: Ouch!
Enemy: Patrol B

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"

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.

Where State Machines Still Fit

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:

  • Represent situations such as alert or fighting through Actor variables or a state machine.
  • Let priorities and the VM handle temporary interruption and resumption.
  • Recheck the current situation when resumed behavior needs to respond to changes.

Moving execution management into the runtime reduces some bookkeeping. The game still needs rules for choosing behavior.

Concurrent Behavior Without a Thread per Actor

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

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.

Simple Actions Can Still Produce Complicated Interactions

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

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:

  • Which Action is currently executing.
  • Which Actions are suspended.
  • Which requests are pending.
  • Which requests were rejected.

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.

The Boundary I Am Trying to Draw

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.

來源:DEV Community · dev.to