この記事はSiv3D Advent Calendar 2025の5日目の記事です。
きっかけ
とある繋がりの方々と「アンナのお菓子な大冒険」というゲームをチーム制作したのがきっかけでした。
(「アンナのお菓子な大冒険」はふりーむにて無料配信中です。ぜひ遊んでみてください。)
また、このゲームのメインプログラマ兼リーダーを務めた方がSiv3D Advent Calendar 2025の11日目に記事を投稿するみたいなので、気になる方はそちらの記事を楽しみにしてください。
このゲームでは以下のようなスクリプトを使ってモーションを作成しました(このスクリプトは別のメンバーの方が作った独自のスクリプトです)。
#Floating //モーション名
Move,body,0,-20,1
RotateTo,lFeather,-15,1
RotateTo,rFeather,15,1
RotateTo,rarm,-10,1
RotateTo,larm,10,1
文法はシンプルで「命令,対象,引数...」といった感じでキャラクターのパーツに対して動きを指定します。コメントアウトや変数の機能もありました。
このシステムが気に入ったので、個人制作に取り入れてみたいと思い、スクリプトを自作することを決めました。
最終的に、次のようなスクリプトが誕生しました。
Motion(Object obj)
{
ps=DS(LoadFile("asset/motion/sara/Pause.txt"))
t1=0.3
t2=0.2
maruPouse = Pause(obj, GetChild(ps, "Maru"), t1)
@PauseExtraRotation(maruPouse,
"ubody",
1 )
>>
@Pause(obj,GetChild(ps,"UpRight"),t2)
}
Pythonっぽさのある、奇妙なスクリプトです。
私はこのスクリプトをAScriptと呼んでいるので、以降このスクリプトのことをAScriptと表記します。
今回の記事では「なぜこのようなスクリプトを作ったのか」「どのように作ったのか」という2点を書いていきたいと思います。
スクリプトを自作した理由
前述したとおり、チーム制作がきっかけではありますが、私がスクリプトを欲したのは、イベントシーン作りたかったというのが一番の理由です。
ドラクエⅥのムドー城突入とか、マリオストーリーのノコブロス戦とか、あのようなイベントシーンにはずっと憧れています。
モーション、セリフ、カメラワーク、エフェクト、効果音、そういった演出を実行画面を見ながらホットリロードで調整できたら、超楽しくイベントシーンが作れると思い、スクリプトを導入しようと決心しました。
でもそれなら、luaやらAngelScriptやら既存のものを導入すれば済む話ですが、なぜスクリプト自作したかというと、面倒くさい環境構築とか勉強をしたくないという惰性が主な理由でした。
・・・そんな身も蓋もない話をここでするべきではないと思うので、次の章ではスクリプトを自作することのメリットについても話していきます。
AScriptの特徴
スクリプト自作の大きなメリットは、自分の制作環境に特化できることだと思います。
私の制作環境にはアクションシステムと呼んでいるプログラムがあって、AScriptはアクションを組み立てるのに特化しています(AScript の "A" は Action の "A")。
アクションシステムは一次元のステートマシンみたいなものです。
シーン管理からキャラクターの処理、モーションなど、私の制作環境では大体のものをアクションシステムで動かしています。
どのように特化しているのかというと、例えば関数や変数、演算といった基本的な機能のほかに、以下のような特殊なものが用意されています。
Func()
{
@ Wait("aaa", 1)
@ Wait("bbb", 1)
>>
@ Wait("ccc", 1)
{ @Wait(1) >> @Wait("ddd", 1) }
}
-
アッド文 @
@の後ろのアクションを関数の戻り値に追加する。 -
ドリフト文 >>
前のアクションと次のアクションの区切り -
複合アクション文 {}
複数アクションを一つにまとめて戻り値に追加する
イベントシーンを作る際は、キャラクターを動かすアクションやカメラを動かすアクション、効果音を鳴らすアクションなどをスクリプト上で組み合わせて作っていくイメージです。
また、特殊な文法以外にも、自分好みのルールを作れるというのは、自作スクリプトの特権です。
例えば、私のスクリプトの変数がすべて参照という扱いになっています。
a = 1 // aは1の参照
b = a // bは1の参照
b = 2 // b の参照先に 2 を代入
print(a) // 2 が出力される
b -> 3 // bは 3 の参照
c = clone(b) // cは 3 の参照 (bの参照先とは別物、明示的な複製)
それに伴い参照演算子 "->" や複製関数 "clone" などを作ることになりました。
上記のソースコードだと "=" が参照と値の更新両方を担っていてちょっと気持ち悪いですが、ユーザーが意識的に "->" と "=" を使い分ければ解決すると思います。
まぁスクリプトですし、いい加減な書き方も許容できた方がいいかなと。
どう作ったか
詳しい内部の処理は説明しきれませんが、どのようにスクリプトを作ったか、要点を絞って順に説明していきます。
要点は以下の3つです。
1. 構文の定義
2. 構文解析(構文木の構築)
3. C++との連携
1. 構文の定義
構文は次のような形で、ソースコード上で定義しています。
//識別子
static const Char ch{ alp or c('_') };
static const Pattern Identifier{ ch + many(ch or num) };
//整数
static const Pattern Integer{ (1 <= many(num)) };
//小数点数
static const Pattern Decimal{ Integer + c('.') + (1 <= many(num)) };
//有理数
static const Pattern Rational{ Decimal / "dec" or Integer / "int" };
//文字列
static const Pattern StringLiteral{ c('\"') + many(wild and not c('\"')) / "contents" + c('\"') };
//bool
static const Pattern Boolian{ s("true") or s("false") };
//値
static const Pattern Value{
StringLiteral / "str" or
Boolian / "bool" or
Rational / "rat" or
Identifier / "ide"
};
注目していただきたい点は2つあります。
1つは構文パターンが階層的に定義されている点です。
これに従って、のちの構文解析では構文木を作成します。

2つめは構文パターンのタグ付けです。
構文に対して"/"をつけるとタグが付与されます。これが結構便利です。
例えばStringLiteralは次のように構文が定義されました。
c('\"') + many(wild and not c('\"')) / "contents" + c('\"')
文頭は " で始まり、複数の " 以外の文字が続き、文末は " で終わる、というような定義です(例えばこんなの → "もじれつ")。今気づいたんですが、文字列中に " を入れたい場合のエスケープ機能を実装してませんでした...必要になったら実装します。
実際に文字列として読み取られるのはダブルクオーテーションで囲まれた内側です。そこで上記のように、内側の構文に"contents"というタグをつけておくことで、文字列の内容が取り出しやすくなります。
また、タグは名づけ以外にも、例えば「構文木に含めない」というignoreタグや「自分を削除し、下の階層を展開する」というexpandタグのような、構文木の構築を指定するメタタグもあります。
ちなみにこのような構文の定義の仕方は@7shiさんの記事を参考にしました。
2. 構文解析(構文木の構築)
(雰囲気で構文木と記述しているだけなので、これを構文木と言って良いのかは知りません。)
定義された構文通りに、与えられた文章をばらして木を構築する段階です。
元の文章
print(1+2)
構文木
[expr]prin… -- [item]prin… -- [function]prin… +- )
|
|- [args]1+2 -- [arg]1+2 +- [expr]2 -- [item]2 -- [rat]2 -- [int]2
| |
| |- [operator]+ -- +
| |
| +- [item]1 -- [rat]1 -- [int]1
|
|- (
|
+- [name]print +- rint
|
+- p
...あまり語ることがないです。
実装は結構重かった気がします。
3. C++との連携
スクリプトに型や関数、値を登録することができます。
// 型の登録
state.registerType<ColorF>(U"Color");
state.registerType<RectF>(U"Rect");
state.registerType<Circle>(U"Circle");
state.registerType<Vec2>(U"Pair");
// 関数の登録
state.registerFunction(U"Print", [](const String& s) {editor::Output(s); });
state.registerFunction(U"Print", [](int s) {editor::Output(Format(s)); });
state.registerFunction(U"Print", [](double s) {editor::Output(Format(s)); });
state.registerFunction(U"Fmt", [](int i) {return Format(i); });
state.registerFunction(U"Fmt", [](double i) {return Format(i); });
state.registerFunction(U"Fmt", [](const Vec2& i) {return Format(i); });
//値の登録
state.registerValue(U"Rect",U"RECT",RectF{100});
stateというスクリプトのメンバ変数に色々登録している感じです。
lua script がこんな感じだった気がする...という想像上のlua scriptを参考にしました。
型登録の際にclone関数の処理を指定することもできます。
指定しなければデフォルトコピーになります。
おしまい
だいぶいい加減な説明だったかもしれませんが、体力的にも説明能力的にも、ここまでが限界でした。
というか今回の記事、あまりSiv3Dの話できなかったですね...スクリプトの内部の実装ではSiv3Dが用意しているデータ構造を使っていて、かなり助けられてます。
Siv3Dの話を期待して読んでくださった方にはあまり面白くない記事になってしまいました。本当に申し訳ございません...
似たようなことがやりたい、という方に少しでもこの記事が役に立ったら幸いです。
以上です、ありがとうございました。