-
-
Notifications
You must be signed in to change notification settings - Fork 4
Concepts ja
🌐 English · 日本語
- Mana の考え方
- Actor-oriented という考え方
- Actor と Action の役割
- Request と Priority
- Mana の実行モデル
- Module
- Phantom
- Namespace
- Compiler と VM
Tutorial では、Mana のコードを実際に書きながら使い方を学びました。
このセクションでは一段深く進み、Mana がなぜ Actor / Action / Request という形になっているのか、そして複数の Action がどのように協調して実行されるのかを説明します。
構文を調べたい場合は Language Reference、実際にコードを書きながら学びたい場合は Tutorial を参照してください。
- Actor-oriented という考え方
- Actor と Action の役割
- Request と Priority
- Mana の実行モデル
- Module
- Phantom
- Namespace
- Compiler と VM
ここまで読むと、Mana の主要な実行概念と、ソースコードから VM 実行までの全体像を把握できます。
次は Language Reference で、各構文と機能を正確に調べられる形へ進みます。
Mana は、ゲーム内にある複数の処理を Actor という独立した実行単位に分けて考える ためのスクリプト言語です。
NPC、扉、イベント進行、敵AI、演出制御などを一つの巨大な処理にまとめるのではなく、それぞれに役割を持たせ、必要なときに Action を Request して協調させます。
ゲームでは、多くの処理が同時進行しているように見えます。
例えば、
- NPC が会話する
- 敵がプレイヤーを追いかける
- 扉が開く
- イベント管理が進行状態を覚える
- 演出が始まる
といった処理があります。
これらを一つの長い処理へ集めると、ある処理の都合が別の処理へ影響しやすくなり、状態管理も複雑になります。
Mana では、役割ごとに Actor を分けます。
EventController
├─ Guide
├─ Gate
└─ Guard
Actor は、それぞれ自分の状態と Action を持ちます。
Actor という名前から、ゲームキャラクターだけを表すように見えるかもしれません。
しかし Mana の Actor は、独立して Action を実行する基本的な実行単位です。
例えば次のようなものを Actor にできます。
- NPC
- 敵
- 扉やスイッチなどのギミック
- イベント進行管理
- シーン制御
- 会話管理
- 演出管理
重要なのは、見た目のあるオブジェクトかどうかではなく、独立した責務と行動を持たせたいかです。
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並列処理やマルチスレッド処理とは区別してください。
Mana は Actor を中心に設計されていますが、学術的な Actor Model をそのまま実装した言語ではありません。
Mana 独自の仕組みとして、例えば次があります。
- Action
- Request
- Priority
- Action の割り込みと再開
- グローバル変数
- Mana VM による協調実行
そのため、Mana の actor は「一般的な Actor Model の actor と完全に同じもの」と考えるより、ゲームの処理を独立した実行主体へ分けるための Mana 独自の Actor と考える方が正確です。
何でも Actor に分ければよいわけではありません。
Actor に向いているのは、例えば次のような処理です。
- 独立した状態を持つ
- 外部から行動を依頼される
- 他の処理とは別のタイミングで動く
- Priority による割り込みを扱いたい
- ゲーム上の責務として名前を付けやすい
一方、単純な計算や共通処理は Function の方が自然です。
Actor / Action
ゲーム上の「誰が何をするか」
Function
Action の中で使う計算や共通処理
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 と何が違うのかを詳しく見ていきます。
Mana では、Actor と Action を分けて考えます。
Actor は 状態を持つ実行主体、Action は その Actor が実行できる行動です。
actor Guard
{
bool mAlert;
action watch()
{
}
action move()
{
}
}
この例では Guard が Actor、watch と move が Action です。
Actor のメンバー変数は、その Actor が持つ状態を表します。その Actor に属する Action から変数名を直接参照できます。
actor Guide
{
int mTalkCount;
action init()
{
mTalkCount = 0;
}
}
Action が終わっても Actor 自体は残るため、次に別の Action が実行されたときも状態を参照できます。
この性質により、例えば次を表現できます。
- 会話した回数
- 扉が開いているか
- NPC が警戒中か
- イベントがどこまで進んだか
Action は Request の対象になります。
request(3, Guard->move());
ここで Guard->move() は Action への参照です。
Action を Actor の中へ置くことで、「その行動は誰の責任か」がコード上でも明確になります。
Guard
├─ watch
├─ move
└─ talk
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、というように考えると整理しやすくなります。
一つの Actor は複数の Action 定義を持てます。
さらに実行時には、Priority の異なる Request が同じ Actor に届くことがあります。
例えば、
priority 1 : patrol
priority 5 : damage
という状態になれば、damage が patrol に割り込みます。
damage が終わった後、保存されていた patrol の実行位置へ戻って再開できます。
このため Action は単なる「Actor のメンバー関数」ではなく、中断と再開を含む Actor の実行状態の単位でもあります。
Action は同じ Actor のメンバー変数へアクセスできます。
actor Door
{
bool mOpened;
action init()
{
mOpened = false;
}
action open()
{
if (mOpened)
return;
mOpened = true;
print("Door opened\n");
}
}
このように、Actor の状態と Action を近くに置くことで、「状態を持つもの」と「その状態を変える行動」を一つのまとまりとして記述できます。
通常の 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 にゲーム全体の処理を詰め込むと、Actor に分けた利点が薄れます。
例えば、
EventController.main
NPCの会話
Doorのアニメーション
Guardの移動
SE再生
状態保存
を全部直接行うより、
EventController
├─ Guide->talk() を Request
├─ Gate->open() を Request
└─ Guard->move() を Request
と責務を分ける方が Mana の設計に合っています。
まずゲーム上の責務を言葉にしてみます。
誰が? 何をする?
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 と組み合わせた動作を整理します。
Mana では、Actor に Action を実行してもらうときに Request を使います。
request(3, Guard->move());
この式は、Guard Actor に対して move Action を Priority 3 で依頼します。
通常の関数呼び出しは、呼び出した処理が終わるまでその場で待ちます。
Request はそれとは異なり、対象 Actor の実行状態へ Action を登録します。
EventController
│
│ request(3, Guard->move())
▼
Guard
│
└─ priority 3 : move
通常の request は、Request を発行した側が対象 Action の終了を待ちません。
対象 Action の開始や終了を待つ必要がある場合は awaitStart や await を使います。
Mana では数値が大きいほど Priority が高くなります。
例えば Guard が Priority 1 の patrol を実行中に、Priority 5 の damage が Request されたとします。
priority 5 : damage ← 実行
priority 1 : patrol ← 中断
高い Priority の Action が先に実行され、元の Action は途中の状態を保持したまま中断されます。
damage が終了すると、保存されていた patrol の位置へ戻って再開できます。
現在実行中の Action より低い Priority の Request は、すぐには実行されません。
現在
priority 5 : battle
新しいRequest
priority 2 : talk
この場合 talk は Priority 2 の実行候補として保持され、Priority 5 の処理が終わった後に実行できる状態になります。
つまり Priority は単なる並べ替えの数値ではなく、Actor の中でどの Action を今実行し、どの Action を待たせるかを決める仕組みです。
現在の 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 は受け付けられません。
- 使用できない最低Priority以下を指定した
- Actor が停止している
- Actor が Request を拒否している
- 同じ Priority がすでに使われている
- 指定した Action が存在しない
通常の Mana スクリプトでは Request の成否を直接戻り値として受け取る構文ではありませんが、実行モデルを理解するうえでは「Request は依頼であり、必ず新しい Action が開始されるとは限らない」と覚えておくとよいでしょう。
Request を受けた Action では、誰がその Request を送ったかを sender から参照できます。
actor Guard
{
action talk()
{
if (sender == Guide)
{
print("Guide requested talk\n");
}
}
}
これにより、同じ Action でも Request 元に応じて動作を変えられます。
用途によって、Request 系の命令を使い分けます。
| 構文 | 呼び出し側の動作 |
|---|---|
request |
依頼したら先へ進む |
awaitStart |
指定PriorityのActionが実行可能な段階まで待つ |
await |
指定PriorityのActionが完了するまで待つ |
join |
対象ActorのPriorityが指定値以下になるまで待つ |
request は Actor 同士を疎結合に連携させたい場合、await はイベントの順番を明確にしたい場合に向いています。ただし、await 系は要求が受理されなければ待たずに進みます。受理後も対象 Actor の Priority を条件に待つため、正確な解除条件は Request リファレンスで確認してください。
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 をどう割り込み・保留するかを決めるものです。
ゲームでは、現在の行動を中断してでも優先したい処理があります。
例えば、
歩く
↓
敵を見つける
↓
戦う
↓
ダメージを受ける
↓
戦闘へ戻る
これを大量の状態分岐だけで表現すると、元の処理へ戻る管理が複雑になります。
Mana では Priority ごとに実行状態を保持することで、割り込み前の Action へ戻れる構造を持っています。
- Request は Actor に Action の実行を依頼する
- 通常の
requestは終了を待たない - Priority は大きいほど高い
- 高い Priority は現在の Action に割り込める
- 低い Priority は後で実行するために保持される
- 同じ Actor の同じ Priority には複数 Request を保持できない
- Priority は Actor ごとに管理される
- Request 元は
senderから参照できる
次は、これらの Request と Priority が Mana VM の中でどのように実行されるのかを全体像として整理します。
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"]
各 Actor は、自分の Action、Priority、実行位置、スタックなどの状態を保持します。
VM はそれぞれの Actor を進めることで、複数の処理を協調させます。
Mana VM の Run() は、登録されている Actor を順に実行します。
概念的には次のように考えられます。
VM Tick
├─ Actor A を進める
├─ Actor B を進める
├─ Actor C を進める
└─ 必要なら新しくRequestされたActorをさらに進める
これは Actor ごとのCPUスレッドを意味しません。
そのため、Mana の並行性は 協調的な疑似並列実行 です。
ゲームエンジン側から見れば一つの VM を一定間隔で進めればよく、Mana 側では複数 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 による割り込みの中心です。
Actor は一つの「現在のAction」だけを持つのではなく、Priority ごとの実行状態を保持できます。
例えば、
priority 5 : damage ← 現在実行中
priority 3 : talk ← 保留
priority 1 : patrol ← 中断
という状態を持てます。
高い Priority の Action が終了すると、残っている中で実行可能な Priority へ戻ります。
このため、複雑なゲーム行動を一つの巨大な状態機械だけで表現せずに、Action と Priority の組み合わせへ分けられます。
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"]
一方、復帰できる Action が残っていなければ、その Actor は実行するものがない状態になります。
request は単なるジャンプ命令ではありません。
request(5, Enemy->damage());
これは対象 Actor に新しい Priority の Action 実行状態を追加する操作です。
現在より高い Priority ならすぐに割り込み、低い Priority なら後で実行するために保持されます。同じ Priority の実行状態がすでに存在する場合、新しい Request は受理されません。
この点が通常の Function 呼び出しとの大きな違いです。
awaitStart、await、join では、呼び出し側 Actor が条件を満たすまで同じ命令を再評価する形で待機します。
つまり「VM 全体を止めて待つ」のではありません。
EventController : Guard の終了待ち
Guard : move を実行中
Guide : 別のActionを進行可能
ある Actor が待っていても、他の Actor は進められます。
この性質が、イベント進行や複数キャラクターの同期に向いています。
yield() は現在の Action の実行をその時点で一度譲ります。
長い処理を一度に進め切らず、次の VM の進行へ処理を渡したい場合に使います。
action update()
{
// 何らかの処理
yield();
// 次の進行で続きを行う
}
細かなスケジューリング規則は Language Reference で扱いますが、Concepts では「Actor が自分から実行権を譲る仕組み」と考えてください。
通常は現在の Action が終了すると一段ずつ元の Action へ戻ります。
rollback を使うと、指定した Priority より上の実行状態をまとめて破棄し、より低い Priority の状態へ戻せます。
これは例えば、
- 行動をキャンセルする
- 一連の割り込み状態をまとめて終了する
- 強制的に基本行動へ戻す
といった制御に使えます。
正確な境界条件や構文は Language Reference で扱います。
refuse() は、Actor が新しい Request を受け付けるかどうかを制御します。comply() で受付を再開できます。
lock は少し性質が異なります。現行コンパイラは lock ブロックの前後で同期実行状態を切り替える命令を生成し、VM は現在の Priority の Synchronized フラグを ON / OFF します。
ただし、現行の Actor::Request はこのフラグを Request の受付判定や Priority の割り込み判定には直接使用していません。
そのため、現在の lock を mutex や「絶対に割り込まれない atomic 区間」と同じものとして理解しないでください。正確な現行挙動は 実行制御リファレンス で扱います。
プログラムをロードすると、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 の特徴は、単に「複数の 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 は、複数の Actor で再利用したい Action やメンバー定義をまとめるための仕組みです。
Mana では、Actor ごとに似た Action を何度も書くより、共通部分を Module として定義し、必要な Actor から extend して利用できます。
module CommonActions
{
action greet()
{
print("Hello\n");
}
}
actor Villager
{
extend CommonActions;
}
この例では、Villager が CommonActions の定義を取り込みます。
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 にまとめられます。
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 | |
|---|---|---|
| 実行主体になる | はい | いいえ |
| VM 起動時に Actor として生成される | はい | いいえ |
| Action を定義できる | はい | はい |
extend される側になる |
通常は使わない | はい |
| 主な目的 | 独立した実行単位 | 共通定義の再利用 |
複数の Actor が同じ意味の振る舞いを持つときに、Module は有効です。
一方、単にコードが少し似ているだけなら、必ずしも Module に分ける必要はありません。
「この振る舞いは複数の Actor に共通する一つの役割か」という観点で判断すると整理しやすくなります。
Actor 自身と Module の両方に同じ名前の Action やメンバーを定義すると、コンパイラのシンボル解決規則の対象になります。
同名定義による上書きや優先順位を前提に設計せず、再利用する役割ごとに衝突しない名前へ分けることを推奨します。
現行仕様として保証する構文と制約は Module リファレンス を参照してください。
- Module は Actor に共通機能を与える再利用単位
- Module 自体は実行主体ではない
- Actor から
extend ModuleName;で利用する - クラス継承より、Actor に加える「部品」と考えると理解しやすい
- namespace /
usingと組み合わせて整理できる
次は、Actor の定義を実行時に生成するための Phantom を説明します。
phantom は、実行時に必要なタイミングで Actor を生成するための Actor 定義のテンプレート です。
構文は Actor とよく似ています。
phantom EnemyTemplate
{
action appear()
{
print("Enemy appeared\n");
}
}
ただし、actor と phantom では VM にロードされたときの扱いが異なります。
通常の actor は、Program Image を VM にロードしたときにインスタンス化され、VM の Actor 一覧へ登録されます。
actor Guide
{
action main()
{
}
}
Guide はプログラムの常駐する実行主体として扱われます。
phantom は VM 起動時には Actor インスタンスを作りません。
VM は Phantom の定義情報を保持し、C++ 側から必要になったときに生成できます。
概念的には次の流れです。
Mana source
|
| phantom EnemyTemplate
v
Program Image
|
v
Mana VM
|
| 定義だけ保持
|
| C++: CreateActorFromPhantom(...)
v
実行時 Actor
現行 VM には、Phantom から Actor を生成するための API があります。
auto enemy = vm->CreateActorFromPhantom("EnemyTemplate", "Enemy01");第1引数は Phantom の定義名、第2引数は生成する Actor の名前です。
これにより、同じ定義から複数の実行時 Actor を作るような用途へ発展させられます。
例えば、ゲーム中に必要になったタイミングで生成されるものが考えられます。
- 敵キャラクター
- 一時的なイベント Actor
- 動的に配置されるギミック
- スポーンされる NPC
起動時から常に存在する必要がない実行主体を、あらかじめ Mana 側で定義しておく場合に適しています。
| Actor | Phantom | |
|---|---|---|
| 定義に Action を持てる | はい | はい |
| VM ロード時に自動生成 | はい | いいえ |
| 通常の起動時実行対象 | はい | いいえ |
| C++ から必要時に生成 | 必須ではない | 主な用途 |
| 主な目的 | 常駐する実行主体 | 動的生成用テンプレート |
Phantom は定義しただけではインスタンスが存在しないため、通常の Actor のようにロード直後から init / main が実行されるわけではありません。
生成後の詳細なライフサイクルや C++ API の使い方は Integration で扱います。
現行仕様では、Mana スクリプトから phantom を直接インスタンス化するための構文は定義されていません。
Phantom の生成は C++ 側の VM API が担当します。
この点は、Phantom が Mana とゲームエンジン側の境界に近い機能であることを示しています。
Module と Phantom はどちらも Actor そのものとは異なりますが、目的は大きく違います。
Module
-> 既存 Actor に共通定義を再利用する
Phantom
-> 新しい Actor を実行時に生成するための定義
Module は「機能の部品」、Phantom は「生成用テンプレート」と考えると区別しやすくなります。
- Phantom は Actor 定義のテンプレート
- VM ロード時には自動でインスタンス化されない
- C++ の
CreateActorFromPhantom()から生成できる - 動的な敵、NPC、ギミックなどに利用できる
- 現行仕様ではスクリプトから直接生成する構文はない
次は、大規模な Mana プログラムで名前を整理する Namespace の考え方を整理します。
namespace は、Actor、Module、struct などの名前を論理的に整理し、同じ短い名前が衝突するのを避けるための仕組みです。
Tutorial では書き方を学びました。ここでは、Mana における 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 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 はコードを短くできますが、複数の Namespace に同名シンボルがあると、名前が曖昧になることがあります。
その場合は完全修飾名で意図を明示します。
request(10, Game::Town::Guard->talk());
短さよりも、どの Actor を参照しているかが明確であることを優先してください。
Namespace 自体が VM 上で実行されたり、Actor のような状態を持ったりするわけではありません。
Namespace はあくまで コンパイル時の名前整理と名前解決の仕組み です。
この点は Actor や Module との重要な違いです。
- Namespace は名前を論理的に整理する
- ファイル分割とは別の仕組み
-
::は Namespace の修飾に使う -
->は Actor の Action 参照に使う -
usingは名前探索を補助する - 名前解決はセマンティック解析で行われるため前方参照が可能
- 曖昧な場合は完全修飾名を書く
次は、Mana のソースがどのように実行されるかを 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 は、ソースを読み、構文と意味を解析し、VM が実行できる形式へ変換します。
概念的には次の流れです。
ソースを読む
↓
構文を解析する
↓
名前や型を解析する
↓
エラーを検出する
↓
Program Image を生成する
Mana が namespace や前方参照を扱えるのも、単純に上から1行ずつ実行するのではなく、コンパイル段階でソース全体を解析するためです。
Program Image は、Compiler と VM の間をつなぐコンパイル済みデータです。
Actor、Action、命令、定数など、VM がプログラムを実行するために必要な情報を保持します。
Source Code
↓ Compiler
Program Image
↓ VM
Runtime State
ソースコードと、実行中の Actor の状態は別のものです。
Mana VM は Program Image をロードし、Actor と Action を実行します。
主な役割は次の通りです。
- Actor の管理
- Action の実行
- Request の処理
- Priority による割り込みと再開
- VM 命令の実行
- C++ 側との連携
Tutorial で使った request や await も、この VM の実行モデルによって動作します。
通常の actor は Program Image のロード時に VM 上へ生成されます。
その後、初期化処理を行い、Actor の init、続いて main が起動されます。
一方、phantom は定義情報として保持されますが、ロード時には Actor インスタンスを生成しません。
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
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 は 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 の構文と各言語機能を正確に調べられる形へ整理します。
このマニュアルは shun126/Mana の documents/wiki/ から自動生成しています。Wiki を直接編集しても次の公開で上書きされるため、修正はリポジトリへの Pull Request でお願いします。
This manual is generated from documents/wiki/ in shun126/Mana. Edits made on the Wiki itself are overwritten on the next publish, so please send changes as pull requests to the repository.