Skip to content

Concepts ja

github-actions[bot] edited this page Sep 30, 2026 · 4 revisions

Concepts

🌐 English · 日本語

目次

Mana の考え方

Tutorial では、Mana のコードを実際に書きながら使い方を学びました。

このセクションでは一段深く進み、Mana がなぜ Actor / Action / Request という形になっているのか、そして複数の Action がどのように協調して実行されるのかを説明します。

構文を調べたい場合は Language Reference、実際にコードを書きながら学びたい場合は Tutorial を参照してください。

読む順番

  1. Actor-oriented という考え方
  2. Actor と Action の役割
  3. Request と Priority
  4. Mana の実行モデル
  5. Module
  6. Phantom
  7. Namespace
  8. Compiler と VM

ここまで読むと、Mana の主要な実行概念と、ソースコードから VM 実行までの全体像を把握できます。

次は Language Reference で、各構文と機能を正確に調べられる形へ進みます。

Actor-oriented という考え方

Mana は、ゲーム内にある複数の処理を Actor という独立した実行単位に分けて考える ためのスクリプト言語です。

NPC、扉、イベント進行、敵AI、演出制御などを一つの巨大な処理にまとめるのではなく、それぞれに役割を持たせ、必要なときに Action を Request して協調させます。

何を解決したいのか

ゲームでは、多くの処理が同時進行しているように見えます。

例えば、

  • NPC が会話する
  • 敵がプレイヤーを追いかける
  • 扉が開く
  • イベント管理が進行状態を覚える
  • 演出が始まる

といった処理があります。

これらを一つの長い処理へ集めると、ある処理の都合が別の処理へ影響しやすくなり、状態管理も複雑になります。

Mana では、役割ごとに Actor を分けます。

EventController
 ├─ Guide
 ├─ Gate
 └─ Guard

Actor は、それぞれ自分の状態と Action を持ちます。

Actor はキャラクターだけではない

Actor という名前から、ゲームキャラクターだけを表すように見えるかもしれません。

しかし Mana の Actor は、独立して Action を実行する基本的な実行単位です。

例えば次のようなものを Actor にできます。

  • NPC
  • 敵
  • 扉やスイッチなどのギミック
  • イベント進行管理
  • シーン制御
  • 会話管理
  • 演出管理

重要なのは、見た目のあるオブジェクトかどうかではなく、独立した責務と行動を持たせたいかです。

Actor 同士は Request で協調する

Actor が別の Actor に何かをしてほしい場合、Mana では Action を直接関数呼び出しするのではなく Request します。

request(3, Guard->move());

この形にすることで、依頼する側は Guard の内部処理を細かく知る必要がありません。

EventController
      │
      │ Request
      ▼
    Guard
      │
      └─ move Action

「誰が何をするか」を Actor と Action に分け、「いつ実行するか」を Request と Priority で調整するのが Mana の中心的な考え方です。

共有された時間の中で独立して動く

Mana VM は複数の Actor を順に進めます。

そのため、OS のスレッドを Actor ごとに作るわけではありません。各 Actor が別々の実行状態を持ち、VM の進行の中で少しずつ動くことで、ゲームから見ると複数の処理が並行して進んでいるように扱えます。

このドキュメントでは、この性質を 協調的な疑似並列実行 として説明します。

実際のCPU並列処理やマルチスレッド処理とは区別してください。

一般的な Actor Model との関係

Mana は Actor を中心に設計されていますが、学術的な Actor Model をそのまま実装した言語ではありません。

Mana 独自の仕組みとして、例えば次があります。

  • Action
  • Request
  • Priority
  • Action の割り込みと再開
  • グローバル変数
  • Mana VM による協調実行

そのため、Mana の actor は「一般的な Actor Model の actor と完全に同じもの」と考えるより、ゲームの処理を独立した実行主体へ分けるための Mana 独自の Actor と考える方が正確です。

Actor に分ける基準

何でも Actor に分ければよいわけではありません。

Actor に向いているのは、例えば次のような処理です。

  • 独立した状態を持つ
  • 外部から行動を依頼される
  • 他の処理とは別のタイミングで動く
  • Priority による割り込みを扱いたい
  • ゲーム上の責務として名前を付けやすい

一方、単純な計算や共通処理は Function の方が自然です。

Actor / Action
    ゲーム上の「誰が何をするか」

Function
    Action の中で使う計算や共通処理

Actor-oriented に考える利点

Actor ごとに責務を分けると、ゲームの処理を次のように捉えやすくなります。

NPC は talk する
Door は open する
Guard は move する
EventController はそれらを Request する

コードを「命令の長い列」として見るのではなく、複数の実行主体が互いに依頼しながらゲームを進める構造として捉えられることが Mana の特徴です。

ここまでで覚えておきたいこと

  • Mana は Actor を中心に処理を分ける
  • Actor はキャラクターに限定されない
  • Actor は状態と Action を持つ
  • Actor 同士は Request で協調する
  • Mana の Actor は学術的 Actor Model と完全に同一ではない
  • VM が複数 Actor を協調的に進める

次に読む

次は、Actor が持つ Action が通常の Function と何が違うのかを詳しく見ていきます。

Actor と Action の役割

Actor と Action の役割

Mana では、Actor と Action を分けて考えます。

Actor は 状態を持つ実行主体、Action は その Actor が実行できる行動です。

actor Guard
{
    bool mAlert;

    action watch()
    {
    }

    action move()
    {
    }
}

この例では Guard が Actor、watch と move が Action です。

Actor は状態を保持する

Actor のメンバー変数は、その Actor が持つ状態を表します。その Actor に属する Action から変数名を直接参照できます。

actor Guide
{
    int mTalkCount;

    action init()
    {
        mTalkCount = 0;
    }
}

Action が終わっても Actor 自体は残るため、次に別の Action が実行されたときも状態を参照できます。

この性質により、例えば次を表現できます。

  • 会話した回数
  • 扉が開いているか
  • NPC が警戒中か
  • イベントがどこまで進んだか

Action は Actor の外部から依頼できる行動

Action は Request の対象になります。

request(3, Guard->move());

ここで Guard->move() は Action への参照です。

Action を Actor の中へ置くことで、「その行動は誰の責任か」がコード上でも明確になります。

Guard
 ├─ watch
 ├─ move
 └─ talk

Function との違い

Action と Function は、どちらも処理をまとめられますが役割が異なります。

Function は現在の処理から普通に呼び出し、終了すると呼び出し元へ戻ります。

int clampHp(int hp)
{
    if (hp < 0)
        return 0;
    return hp;
}

一方 Action は Actor の実行モデルに参加し、Request と Priority の影響を受けます。

request(5, Enemy->damage());

大まかには次のように分けられます。

Function Action
計算や共通処理 Actor の行動
通常の呼び出し Request で依頼
呼び出し元へ戻る Priority によって割り込み・保留される
Actor 外の処理にも使える Actor に属する

「HPを計算する」は Function、「敵がダメージを受ける」は Action、というように考えると整理しやすくなります。

Action は一つずつしか存在しないのか

一つの Actor は複数の Action 定義を持てます。

さらに実行時には、Priority の異なる Request が同じ Actor に届くことがあります。

例えば、

priority 1 : patrol
priority 5 : damage

という状態になれば、damage が patrol に割り込みます。

damage が終わった後、保存されていた patrol の実行位置へ戻って再開できます。

このため Action は単なる「Actor のメンバー関数」ではなく、中断と再開を含む Actor の実行状態の単位でもあります。

同じ Actor の状態を Action 間で共有する

Action は同じ Actor のメンバー変数へアクセスできます。

actor Door
{
    bool mOpened;

    action init()
    {
        mOpened = false;
    }

    action open()
    {
        if (mOpened)
            return;

        mOpened = true;
        print("Door opened\n");
    }
}

このように、Actor の状態と Action を近くに置くことで、「状態を持つもの」と「その状態を変える行動」を一つのまとまりとして記述できます。

init と main

通常の Actor では、VM がプログラムをロードした後に init と main を自動的に Request します。

init は初期状態を設定する用途、main は Actor の基本動作を始める用途として使えます。

actor NPC
{
    int mState;

    action init()
    {
        mState = 0;
    }

    action main()
    {
        print("NPC started\n");
    }
}

ただし、すべての Actor に必ず両方を書く必要はありません。必要な Action だけを定義できます。

Action の責務を大きくしすぎない

一つの Action にゲーム全体の処理を詰め込むと、Actor に分けた利点が薄れます。

例えば、

EventController.main
    NPCの会話
    Doorのアニメーション
    Guardの移動
    SE再生
    状態保存

を全部直接行うより、

EventController
    ├─ Guide->talk() を Request
    ├─ Gate->open() を Request
    └─ Guard->move() を Request

と責務を分ける方が Mana の設計に合っています。

Actor / Action を設計するときの考え方

まずゲーム上の責務を言葉にしてみます。

誰が?        何をする?
Guide         talk
Gate          open
Guard         move
Event         start

この「誰が」を Actor、「何をする」を Action にすると、自然な構造になりやすくなります。

ここまでで覚えておきたいこと

  • Actor は状態を持つ実行主体
  • Action は Actor が実行できる行動
  • Action は Request の対象になる
  • Function は通常処理、Action は Actor の実行モデルに参加する
  • Action は Priority により中断・再開されることがある
  • Actor のメンバー変数は複数 Action から共有できる

次に読む

Actor に Action を実行してもらう仕組みが Request です。次は Priority と組み合わせた動作を整理します。

Request と Priority

Request と Priority

Mana では、Actor に Action を実行してもらうときに Request を使います。

request(3, Guard->move());

この式は、Guard Actor に対して move Action を Priority 3 で依頼します。

Request は関数呼び出しではない

通常の関数呼び出しは、呼び出した処理が終わるまでその場で待ちます。

Request はそれとは異なり、対象 Actor の実行状態へ Action を登録します。

EventController
      │
      │ request(3, Guard->move())
      ▼
    Guard
      │
      └─ priority 3 : move

通常の request は、Request を発行した側が対象 Action の終了を待ちません。

対象 Action の開始や終了を待つ必要がある場合は awaitStart や await を使います。

Priority は Action の重要度を表す

Mana では数値が大きいほど Priority が高くなります。

例えば Guard が Priority 1 の patrol を実行中に、Priority 5 の damage が Request されたとします。

priority 5 : damage   ← 実行
priority 1 : patrol   ← 中断

高い Priority の Action が先に実行され、元の Action は途中の状態を保持したまま中断されます。

damage が終了すると、保存されていた patrol の位置へ戻って再開できます。

低い Priority の Request

現在実行中の Action より低い Priority の Request は、すぐには実行されません。

現在
priority 5 : battle

新しいRequest
priority 2 : talk

この場合 talk は Priority 2 の実行候補として保持され、Priority 5 の処理が終わった後に実行できる状態になります。

つまり Priority は単なる並べ替えの数値ではなく、Actor の中でどの Action を今実行し、どの Action を待たせるかを決める仕組みです。

同じ Priority は一つだけ

現在の Mana 実装では、一つの Actor の同じ Priority に複数の Request を保持できません。

すでに Priority 3 の Request が登録されている状態で、別の Priority 3 の Action を Request すると、その新しい Request は受け付けられません。

Guard
priority 3 : talk

request(3, Guard->move())
        ↓
同じPriorityが使用中なので受け付けられない

このため Priority は、「重要度」であると同時に Actor 内の実行スロットのような役割も持っています。

Priority を細かく数値化しすぎるより、ゲーム側で意味のある段階を決めて使う方が管理しやすくなります。

例えば、

1 : 通常行動
3 : 会話・イベント
5 : ダメージ反応
8 : 強制演出

のように用途を決められます。

数値そのものより、プロジェクト内での意味を揃えることが重要です。

Request が受け付けられない場合

Request は常に成功するとは限りません。

現在の実装では、例えば次のような場合に Request は受け付けられません。

  • 使用できない最低Priority以下を指定した
  • Actor が停止している
  • Actor が Request を拒否している
  • 同じ Priority がすでに使われている
  • 指定した Action が存在しない

通常の Mana スクリプトでは Request の成否を直接戻り値として受け取る構文ではありませんが、実行モデルを理解するうえでは「Request は依頼であり、必ず新しい Action が開始されるとは限らない」と覚えておくとよいでしょう。

sender

Request を受けた Action では、誰がその Request を送ったかを sender から参照できます。

actor Guard
{
    action talk()
    {
        if (sender == Guide)
        {
            print("Guide requested talk\n");
        }
    }
}

これにより、同じ Action でも Request 元に応じて動作を変えられます。

Request と待機

用途によって、Request 系の命令を使い分けます。

構文 呼び出し側の動作
request 依頼したら先へ進む
awaitStart 指定PriorityのActionが実行可能な段階まで待つ
await 指定PriorityのActionが完了するまで待つ
join 対象ActorのPriorityが指定値以下になるまで待つ

request は Actor 同士を疎結合に連携させたい場合、await はイベントの順番を明確にしたい場合に向いています。ただし、await 系は要求が受理されなければ待たずに進みます。受理後も対象 Actor の Priority を条件に待つため、正確な解除条件は Request リファレンスで確認してください。

Priority は Actor ごとに管理される

Priority はゲーム全体で一つの実行順位表を作るものではありません。

それぞれの Actor が、自分に届いた Request と現在の Priority を管理します。

Guide
priority 3 : talk

Guard
priority 5 : damage
priority 1 : patrol

Gate
priority 2 : open

各 Actor は独立した実行状態を持ち、VM がそれらを順番に進めます。

そのため「Guard の Priority 5 が Guide の Priority 3 より先に実行される」という単純な全体順位ではありません。

Priority は 同じ Actor の中で Action をどう割り込み・保留するかを決めるものです。

Priority を使う理由

ゲームでは、現在の行動を中断してでも優先したい処理があります。

例えば、

歩く
  ↓
敵を見つける
  ↓
戦う
  ↓
ダメージを受ける
  ↓
戦闘へ戻る

これを大量の状態分岐だけで表現すると、元の処理へ戻る管理が複雑になります。

Mana では Priority ごとに実行状態を保持することで、割り込み前の Action へ戻れる構造を持っています。

ここまでで覚えておきたいこと

  • Request は Actor に Action の実行を依頼する
  • 通常の request は終了を待たない
  • Priority は大きいほど高い
  • 高い Priority は現在の Action に割り込める
  • 低い Priority は後で実行するために保持される
  • 同じ Actor の同じ Priority には複数 Request を保持できない
  • Priority は Actor ごとに管理される
  • Request 元は sender から参照できる

次に読む

次は、これらの Request と Priority が Mana VM の中でどのように実行されるのかを全体像として整理します。

Mana の実行モデル

Mana の実行モデル

Mana の実行モデルは、複数の Actor がそれぞれ独立した実行状態を持ち、Mana VM がそれらを協調的に進めることを基本にしています。

OS のスレッドを Actor ごとに作る方式ではありません。

ゲームから見ると複数の Actor が同時に動いているように扱えますが、VM は Actor を順に実行していきます。

全体像

flowchart LR
    VM["Mana VM"] --> A["Actor A"]
    VM --> B["Actor B"]
    VM --> C["Actor C"]

    A --> A1["Action / Priority state"]
    B --> B1["Action / Priority state"]
    C --> C1["Action / Priority state"]
Loading

各 Actor は、自分の Action、Priority、実行位置、スタックなどの状態を保持します。

VM はそれぞれの Actor を進めることで、複数の処理を協調させます。

VM が Actor を順に進める

Mana VM の Run() は、登録されている Actor を順に実行します。

概念的には次のように考えられます。

VM Tick
 ├─ Actor A を進める
 ├─ Actor B を進める
 ├─ Actor C を進める
 └─ 必要なら新しくRequestされたActorをさらに進める

これは Actor ごとのCPUスレッドを意味しません。

そのため、Mana の並行性は 協調的な疑似並列実行 です。

ゲームエンジン側から見れば一つの VM を一定間隔で進めればよく、Mana 側では複数 Actor の状態を独立して記述できます。

Actor は実行位置を持つ

Action が途中まで実行された状態で高い Priority の Action に割り込まれると、Mana は元の Action の実行位置を保持します。

priority 1 : patrol
    line A
    line B   ← ここまで実行

priority 5 : damage が割り込む

割り込み時には、元の Action の実行位置やスタック状態が保存されます。

その後 damage が終了すると、保存されていた状態へ戻ります。

priority 5 : damage
    終了
       ↓
priority 1 : patrol
    line C   ← 続きから再開

この仕組みが、Mana の Priority による割り込みの中心です。

Priority ごとに実行状態を保持する

Actor は一つの「現在のAction」だけを持つのではなく、Priority ごとの実行状態を保持できます。

例えば、

priority 5 : damage   ← 現在実行中
priority 3 : talk     ← 保留
priority 1 : patrol   ← 中断

という状態を持てます。

高い Priority の Action が終了すると、残っている中で実行可能な Priority へ戻ります。

このため、複雑なゲーム行動を一つの巨大な状態機械だけで表現せずに、Action と Priority の組み合わせへ分けられます。

Action の終了と再開

Action が最後まで到達するか return すると、その Action の Priority は解放されます。

その下に中断中の Action があれば、保存されていた実行位置へ復帰します。

flowchart TD
    A["priority 1: patrol"] -->|"priority 5 request"| B["priority 5: damage"]
    B -->|"damage ends"| C["priority 1: patrol resumes"]
Loading

一方、復帰できる Action が残っていなければ、その Actor は実行するものがない状態になります。

request は実行状態を追加する

request は単なるジャンプ命令ではありません。

request(5, Enemy->damage());

これは対象 Actor に新しい Priority の Action 実行状態を追加する操作です。

現在より高い Priority ならすぐに割り込み、低い Priority なら後で実行するために保持されます。同じ Priority の実行状態がすでに存在する場合、新しい Request は受理されません。

この点が通常の Function 呼び出しとの大きな違いです。

待機する側も Actor である

awaitStart、await、join では、呼び出し側 Actor が条件を満たすまで同じ命令を再評価する形で待機します。

つまり「VM 全体を止めて待つ」のではありません。

EventController : Guard の終了待ち
Guard           : move を実行中
Guide           : 別のActionを進行可能

ある Actor が待っていても、他の Actor は進められます。

この性質が、イベント進行や複数キャラクターの同期に向いています。

yield の役割

yield() は現在の Action の実行をその時点で一度譲ります。

長い処理を一度に進め切らず、次の VM の進行へ処理を渡したい場合に使います。

action update()
{
    // 何らかの処理
    yield();

    // 次の進行で続きを行う
}

細かなスケジューリング規則は Language Reference で扱いますが、Concepts では「Actor が自分から実行権を譲る仕組み」と考えてください。

rollback は Priority を巻き戻す

通常は現在の Action が終了すると一段ずつ元の Action へ戻ります。

rollback を使うと、指定した Priority より上の実行状態をまとめて破棄し、より低い Priority の状態へ戻せます。

これは例えば、

  • 行動をキャンセルする
  • 一連の割り込み状態をまとめて終了する
  • 強制的に基本行動へ戻す

といった制御に使えます。

正確な境界条件や構文は Language Reference で扱います。

refuse と lock

refuse() は、Actor が新しい Request を受け付けるかどうかを制御します。comply() で受付を再開できます。

lock は少し性質が異なります。現行コンパイラは lock ブロックの前後で同期実行状態を切り替える命令を生成し、VM は現在の Priority の Synchronized フラグを ON / OFF します。

ただし、現行の Actor::Request はこのフラグを Request の受付判定や Priority の割り込み判定には直接使用していません。

そのため、現在の lock を mutex や「絶対に割り込まれない atomic 区間」と同じものとして理解しないでください。正確な現行挙動は 実行制御リファレンス で扱います。

起動時の init と main

プログラムをロードすると、VM は通常の Actor を生成し、初期化用の処理を行った後、各 Actor の init と main を Request します。

概念的には次の流れです。

Program Image をロード
    ↓
Actor を生成
    ↓
グローバル初期化
    ↓
各 Actor の main を Priority 0 で予約
    ↓
各 Actor の init を最高優先度(2147483647)で Request
    ↓
通常の VM 実行

これにより、Actor は起動時の初期化と通常動作を Action として記述できます。ただし、全 Actor の init が完了してから全 Actor の main が始まる、という一括の待ち合わせはありません。各 Actor は自分の init が終了すると、予約済みの Action を優先度順に実行します。初期化済みであることが必要な連携は、明示的に順序を設計してください。

Mana の実行モデルを一言で表すと

Mana の特徴は、単に「複数の Actor がある」ことではありません。

重要なのは、

Actor
  ├─ 自分の状態を持つ
  ├─ Action を持つ
  ├─ Request を受ける
  ├─ Priority ごとの実行状態を持つ
  └─ 割り込み・待機・再開を行う

という構造を、VM が協調的に進めることです。

このモデルによって、ゲームの「歩く」「話す」「攻撃する」「ダメージを受ける」「イベントを待つ」といった処理を、互いに独立した Action として組み合わせられます。

ここまでで覚えておきたいこと

  • Mana VM は複数 Actor を順番に進める
  • Actor ごとに独立した実行状態を持つ
  • Mana の並行性はOSスレッドによる並列実行ではない
  • Priority ごとに Action の状態を保持できる
  • 高Priorityの Action は低Priorityの Action に割り込める
  • 同じPriorityの新しいRequestは、そのPriorityが既に存在すると受理されない
  • Action 終了後は中断していた Action を再開できる
  • await 系の待機は VM 全体を止めない
  • refuse は新しいRequestの受付を制御する
  • lock は現行実装では同期状態フラグを切り替えるが、Requestの割り込み判定を直接抑止するものではない

次に読む

ここまでで、Mana の中心となる Actor / Action / Request / Priority / VM 実行モデルの全体像を説明しました。

次は Module、Phantom、Namespace といった、より大きなスクリプト構成を支える概念を扱います。

Module

module は、複数の Actor で再利用したい Action やメンバー定義をまとめるための仕組みです。

Mana では、Actor ごとに似た Action を何度も書くより、共通部分を Module として定義し、必要な Actor から extend して利用できます。

module CommonActions
{
    action greet()
    {
        print("Hello\n");
    }
}

actor Villager
{
    extend CommonActions;
}

この例では、Villager が CommonActions の定義を取り込みます。

Module の役割

Module は、実行主体そのものではありません。

actor のように VM 起動時にインスタンスが作られて自分で Action を実行するのではなく、Actor に共通機能を与えるための再利用単位です。

概念的には次のように考えられます。

module CommonActions
        |
        | extend
        v
actor Villager

module CommonActions
        |
        | extend
        v
actor Guard

一つの Module を複数の Actor から利用できます。

継承より「部品の再利用」と考える

extend という名前から、C++ や Java のクラス継承を想像するかもしれません。

しかし、Mana の Module はクラス階層を作るための仕組みとして考えるより、Actor に共通の定義を追加する部品として理解する方が適切です。

例えば、

  • 会話用 Action
  • 共通リアクション
  • 共通の待機処理
  • 複数種類の NPC が共有する振る舞い

などを Module にまとめられます。

namespace と組み合わせられる

Module は namespace 内にも定義できます。

namespace Game::NPC
{
    module Talkable
    {
        action talk()
        {
            print("Hello\n");
        }
    }
}

完全修飾名で利用できます。

actor Villager
{
    extend Game::NPC::Talkable;
}

using で名前空間を探索対象にした場合は、短い名前でも参照できます。

using Game::NPC;

actor Villager
{
    extend Talkable;
}

現行コンパイラでは、このような namespace / using を通した Module の名前解決も行われます。

Actor と Module の違い

Actor Module
実行主体になる はい いいえ
VM 起動時に Actor として生成される はい いいえ
Action を定義できる はい はい
extend される側になる 通常は使わない はい
主な目的 独立した実行単位 共通定義の再利用

Module を使う判断

複数の Actor が同じ意味の振る舞いを持つときに、Module は有効です。

一方、単にコードが少し似ているだけなら、必ずしも Module に分ける必要はありません。

「この振る舞いは複数の Actor に共通する一つの役割か」という観点で判断すると整理しやすくなります。

同名定義について

Actor 自身と Module の両方に同じ名前の Action やメンバーを定義すると、コンパイラのシンボル解決規則の対象になります。

同名定義による上書きや優先順位を前提に設計せず、再利用する役割ごとに衝突しない名前へ分けることを推奨します。

現行仕様として保証する構文と制約は Module リファレンス を参照してください。

まとめ

  • Module は Actor に共通機能を与える再利用単位
  • Module 自体は実行主体ではない
  • Actor から extend ModuleName; で利用する
  • クラス継承より、Actor に加える「部品」と考えると理解しやすい
  • namespace / using と組み合わせて整理できる

次は、Actor の定義を実行時に生成するための Phantom を説明します。

Phantom

phantom は、実行時に必要なタイミングで Actor を生成するための Actor 定義のテンプレート です。

構文は Actor とよく似ています。

phantom EnemyTemplate
{
    action appear()
    {
        print("Enemy appeared\n");
    }
}

ただし、actor と phantom では VM にロードされたときの扱いが異なります。

Actor は起動時に生成される

通常の actor は、Program Image を VM にロードしたときにインスタンス化され、VM の Actor 一覧へ登録されます。

actor Guide
{
    action main()
    {
    }
}

Guide はプログラムの常駐する実行主体として扱われます。

Phantom は起動時に生成されない

phantom は VM 起動時には Actor インスタンスを作りません。

VM は Phantom の定義情報を保持し、C++ 側から必要になったときに生成できます。

概念的には次の流れです。

Mana source
    |
    | phantom EnemyTemplate
    v
Program Image
    |
    v
Mana VM
    |
    | 定義だけ保持
    |
    | C++: CreateActorFromPhantom(...)
    v
実行時 Actor

C++ から生成する

現行 VM には、Phantom から Actor を生成するための API があります。

auto enemy = vm->CreateActorFromPhantom("EnemyTemplate", "Enemy01");

第1引数は Phantom の定義名、第2引数は生成する Actor の名前です。

これにより、同じ定義から複数の実行時 Actor を作るような用途へ発展させられます。

どのような場面で使うか

例えば、ゲーム中に必要になったタイミングで生成されるものが考えられます。

  • 敵キャラクター
  • 一時的なイベント Actor
  • 動的に配置されるギミック
  • スポーンされる NPC

起動時から常に存在する必要がない実行主体を、あらかじめ Mana 側で定義しておく場合に適しています。

Actor と Phantom の違い

Actor Phantom
定義に Action を持てる はい はい
VM ロード時に自動生成 はい いいえ
通常の起動時実行対象 はい いいえ
C++ から必要時に生成 必須ではない 主な用途
主な目的 常駐する実行主体 動的生成用テンプレート

init / main をどう考えるか

Phantom は定義しただけではインスタンスが存在しないため、通常の Actor のようにロード直後から init / main が実行されるわけではありません。

生成後の詳細なライフサイクルや C++ API の使い方は Integration で扱います。

スクリプトから直接生成する機能

現行仕様では、Mana スクリプトから phantom を直接インスタンス化するための構文は定義されていません。

Phantom の生成は C++ 側の VM API が担当します。

この点は、Phantom が Mana とゲームエンジン側の境界に近い機能であることを示しています。

Module との違い

Module と Phantom はどちらも Actor そのものとは異なりますが、目的は大きく違います。

Module
  -> 既存 Actor に共通定義を再利用する

Phantom
  -> 新しい Actor を実行時に生成するための定義

Module は「機能の部品」、Phantom は「生成用テンプレート」と考えると区別しやすくなります。

まとめ

  • Phantom は Actor 定義のテンプレート
  • VM ロード時には自動でインスタンス化されない
  • C++ の CreateActorFromPhantom() から生成できる
  • 動的な敵、NPC、ギミックなどに利用できる
  • 現行仕様ではスクリプトから直接生成する構文はない

次は、大規模な Mana プログラムで名前を整理する Namespace の考え方を整理します。

Namespace

namespace は、Actor、Module、struct などの名前を論理的に整理し、同じ短い名前が衝突するのを避けるための仕組みです。

Tutorial では書き方を学びました。ここでは、Mana における Namespace の役割を整理します。

ファイルと Namespace は別のもの

ファイル分割は、ソースコードを物理的に整理する方法です。

Namespace は、プログラム中の名前を論理的に整理する方法です。

ファイル
  -> どこにコードを書くか

namespace
  -> その名前がどの領域に属するか

例えば npc.mn に書かれているから自動的に NPC namespace に入るわけではありません。

名前に意味のある階層を与える

namespace Game::Town
{
    actor Guard
    {
    }
}

namespace Game::Dungeon
{
    actor Guard
    {
    }
}

どちらも短い名前は Guard ですが、完全な名前は異なります。

Game::Town::Guard
Game::Dungeon::Guard

このように、同じ役割名を別の文脈で安全に使えます。

:: は名前の所属を表す

Mana では :: を Namespace の修飾に使います。

request(10, Game::Town::Guard->talk());

一方、-> は Actor と Action の関係を表します。

Game::Town::Guard -> talk
^^^^^^^^^^^^^^^^^    ^^^^
Actor の名前         Action

この2つは役割が異なります。

using は名前探索を補助する

完全修飾名を毎回書く代わりに、using で探索対象を追加できます。

using Game::Town;

actor EventController
{
    action main()
    {
        request(10, Guard->talk());
    }
}

using Game::Town; は、未修飾名を解決するときに Game::Town も探索対象にします。

また、特定の Actor や Module を名前として取り込む使い方もできます。

using Game::Town::Guard;

現行実装では、using の対象として Namespace と Actor / Module が扱われます。

名前解決はパース後に行われる

Mana コンパイラはソースを読み込んだ後、セマンティック解析の段階で名前を解決します。

そのため、次のように using の後で Namespace を定義するコードも解決できます。

using Game::Town;

actor Controller
{
    action main()
    {
        request(1, Guard->talk());
    }
}

namespace Game::Town
{
    actor Guard
    {
        action talk()
        {
        }
    }
}

これは、単純に上から1行ずつ名前を確定しているわけではなく、コンパイル単位全体を見て意味を解析しているためです。

複数ファイルでも同じ名前空間を使える

Namespace は一つのファイルだけに閉じた仕組みではありません。

複数のソースから同じ論理的な Namespace を使うことで、大きなゲームを役割ごとに分割できます。

character.mn  -> Game::Character
npc.mn        -> Game::Character::NPC
enemy.mn      -> Game::Character::Enemy
event.mn      -> Game::Event

ファイル構成と Namespace 構成を似せると分かりやすくなる場合はありますが、両者が同一である必要はありません。

using を増やしすぎない

using はコードを短くできますが、複数の Namespace に同名シンボルがあると、名前が曖昧になることがあります。

その場合は完全修飾名で意図を明示します。

request(10, Game::Town::Guard->talk());

短さよりも、どの Actor を参照しているかが明確であることを優先してください。

Namespace は実行単位ではない

Namespace 自体が VM 上で実行されたり、Actor のような状態を持ったりするわけではありません。

Namespace はあくまで コンパイル時の名前整理と名前解決の仕組み です。

この点は Actor や Module との重要な違いです。

まとめ

  • Namespace は名前を論理的に整理する
  • ファイル分割とは別の仕組み
  • :: は Namespace の修飾に使う
  • -> は Actor の Action 参照に使う
  • using は名前探索を補助する
  • 名前解決はセマンティック解析で行われるため前方参照が可能
  • 曖昧な場合は完全修飾名を書く

次は、Mana のソースがどのように実行されるかを Compiler と VM の関係から整理します。

Compiler と VM

Mana は、ソースコードをそのまま直接実行する方式ではありません。

Mana Compiler がソースコードを解析して Program Image を生成し、Mana VM がその Program Image を実行します。

Mana source (.mn)
        |
        v
Mana Compiler
        |
        v
Program Image
        |
        v
Mana VM
        |
        v
Actor / Action の実行

スクリプト言語でもコンパイルできる

スクリプト言語であっても、実行前にソースコードを別の形式へ変換することができます。

Mana でいうコンパイルは、CPU が直接実行するネイティブマシンコードを作ることではありません。

Mana VM 用の Program Image を作ることです。

Compiler の役割

Compiler は、ソースを読み、構文と意味を解析し、VM が実行できる形式へ変換します。

概念的には次の流れです。

ソースを読む
    ↓
構文を解析する
    ↓
名前や型を解析する
    ↓
エラーを検出する
    ↓
Program Image を生成する

Mana が namespace や前方参照を扱えるのも、単純に上から1行ずつ実行するのではなく、コンパイル段階でソース全体を解析するためです。

Program Image とは

Program Image は、Compiler と VM の間をつなぐコンパイル済みデータです。

Actor、Action、命令、定数など、VM がプログラムを実行するために必要な情報を保持します。

Source Code
    ↓ Compiler
Program Image
    ↓ VM
Runtime State

ソースコードと、実行中の Actor の状態は別のものです。

VM の役割

Mana VM は Program Image をロードし、Actor と Action を実行します。

主な役割は次の通りです。

  • Actor の管理
  • Action の実行
  • Request の処理
  • Priority による割り込みと再開
  • VM 命令の実行
  • C++ 側との連携

Tutorial で使った request や await も、この VM の実行モデルによって動作します。

Program Image をロードしたとき

通常の actor は Program Image のロード時に VM 上へ生成されます。

その後、初期化処理を行い、Actor の init、続いて main が起動されます。

一方、phantom は定義情報として保持されますが、ロード時には Actor インスタンスを生成しません。

CLI ではまとめて実行できる

mana source.mn

とすると、ソースをコンパイルしてそのまま VM で実行できます。

Program Image をファイルへ出力することもできます。

mana source.mn -o program.bin

コンパイル済みファイルは次のように実行できます。

mana --execute program.bin

内部の役割で見ると、次の違いです。

mana source.mn
    = Compile + Execute

mana source.mn -o program.bin
    = Compile

mana --execute program.bin
    = Execute

Compiler と VM は組み込み可能

Mana の Compiler と VM は、コマンドラインツールだけに閉じた仕組みではありません。

Compiler はライブラリとして、VM はランタイムとしてゲームやエディタへ組み込めます。

Editor / Game Engine
        |
        +--> Mana Compiler
        |        |
        |        v
        |   Program Image
        |        |
        +--> Mana VM

詳しい C++ API は Integration で扱います。

コンパイル時と実行時

Compiler と VM が分かれているため、問題が検出される段階も分かれます。

Compiler は、構文や名前解決、型など、実行前に判断できる問題を検出します。

VM は、実際に Actor と Action が動いているときにしか判断できない問題を扱います。

この分離により、可能な誤りは実行前に見つけつつ、ゲーム実行時の制御は VM に任せられます。

Program Image の互換性

Program Image は Mana VM が読み込む実行形式です。

現行 VM はロード時にシグネチャ、バージョン、ビット数などを確認します。

そのため Program Image は、対応する Mana VM と組み合わせて利用するコンパイル済み形式として扱います。

まとめ

  • Mana は Compiler と VM に分かれている
  • .mn ソースを Compiler が Program Image へ変換する
  • Program Image は Mana VM 用の実行形式
  • VM が Actor / Action / Request を実行する
  • Compiler は実行前の解析とエラー検出を担当する
  • CLI では Compile と Execute をまとめても分けても使える
  • Compiler と VM は C++ アプリケーションへ組み込める

これで Concepts の基本項目は完了です。

次は Language Reference で、Mana の構文と各言語機能を正確に調べられる形へ整理します。

Clone this wiki locally