はじめまして!今月より正社員となりました 店長 です。
ゲームプログラマとしてはおよそ7年の経験がありますが、
ヘキサドライブの皆についていくために、
ますます力を入れて頑張っております

さて、そんな私が最近勉強したスクリプト言語
Squirrelについて書いていきます。
まずこの言語に着目した理由ですが、
ゲーム開発をしていくと、管理のため、ビルド時間の短縮のため、などの事情で、
色々なパラメータなどをソースコード外に置いて管理することが増えていきますが、
たまにソースコードの一部をまるごと外に置きたいことがあります。
例
・シューティングのボスの複雑な動き
・ゲームエンジンのUI拡張部分
・頻繁に調整が入るNPCの動き
大規模なゲーム開発になると、ビルド時間が開発時間を大幅に消耗していくこともあるため、
できるだけそういう時間を減らしたいと思っていました。
そこで、実行中に一部分だけを逐次コンパイルしやすい言語を使ってみようと思ったわけです。
特にSquirrelは
手続き型言語、オブジェクト指向言語、関数型言語、データ駆動
などの特性を併せ持つというゲームで利用しやすい面もあります。
サンプルとして作ったコンソールアプリのソースコードを掲載いたします。
C++で定義したクラスをSquirrelで使用する方法
Squirrelで定義したクラスをC++で使用する方法のサンプルです。
実行後Cキーで逐次Squirrelのファイルがコンパイルされ、実行されます。
#include "stdafx.h"
#include <sqrat.h> // squirrel.h もインクルードされる
#include <sqstdaux.h> // sqstd_seterrorhandlers
#include <stdio.h>
#include <stdarg.h>
#include <Windows.h>
// 文字列出力
static void printfunc(HSQUIRRELVM vm, const SQChar* format, ...)
{
va_list args;
va_start(args, format);
#ifdef SQUNICODE
vwprintf(format, args);
#else
vprintf(format, args);
#endif
va_end(args);
}
// 敵クラス
class Enemy
{
public:
void up(){ printf("%sが上へ移動\n", m_name); }
void down(){ printf("%sが下へ移動\n", m_name); }
void left(){ printf("%sが左へ移動\n", m_name); }
void right(){ printf("%sが右へ移動\n", m_name); }
void shot(){ printf("%sが弾を発射\n", m_name); }
void name(){ printf("名前 %s\n", m_name); }
public:
int m_hp;
char* m_name;
};
// 数値出力関数
void printNum(int n)
{
printf("num = %d\n", n);
}
// テーブルへのバインド
void bindMyTable(HSQUIRRELVM vm)
{
using namespace Sqrat;
// テーブルを作成
Table myTable(vm);
// Enemy オブジェクトを作成
Class<Enemy> enemy(vm);
// Enemy オブジェクトにバインド
// メンバ関数のバインド
enemy.Func(_SC("up"), &Enemy::up);
enemy.Func(_SC("down"), &Enemy::down);
enemy.Func(_SC("left"), &Enemy::left);
enemy.Func(_SC("right"), &Enemy::right);
enemy.Func(_SC("shot"), &Enemy::shot);
enemy.Func(_SC("name"), &Enemy::name);
// メンバ変数のバインド
enemy.Var(_SC("m_hp"), &Enemy::m_hp);
enemy.Var(_SC("m_name"), &Enemy::m_name);
// ルートテーブルに enemy を "Enemy" としてバインド
RootTable(vm).Bind(_SC("Enemy"), enemy);
RootTable(vm).Func(_SC("printNum"), &printNum);
}
// メイン
int main(int argc, char** argv)
{
// Sqratの名前空間
using namespace Sqrat;
// VMの作成
HSQUIRRELVM vm = sq_open(1024);
// エラーハンドラを設定
sqstd_seterrorhandlers(vm);
// 文字列出力関数を設定
sq_setprintfunc(vm, printfunc,printfunc);
// 標準で使うVMの設定
DefaultVM::Set(vm);
// C++からSquirrelへのバインド
bindMyTable(vm);
printf("---manual---\n");
printf("Q = Exit\n");
printf("C = Compile Start\n\n");
// ここからスクリプトのコンパイルと実行
{
Script script;
// Qで終了
// Cでコンパイル
while( !::GetAsyncKeyState( 'Q' ) )
{
// コンパイル
if( ::GetAsyncKeyState( 'C' ) & 0x01 )
{
// C++ 定義のクラスをSquirrelから扱う
printf("\n--- Compile Squirrel Start---\n");
script.CompileFile("enemy.nut");
script.Run();
script.CompileFile("enemySquirrel.nut");
script.Run();
// Squirrel 側で定義したクラスを C++ 内部で扱う
// enemy2 <- EnemySquirrel("敵2");
Object objValClass = RootTable(vm).GetSlot(_SC("enemy2"));
if( !objValClass.IsNull() )
{
/// メンバ変数へのアクセス
Object name = objValClass.GetSlot(_SC("name"));
if( !name.IsNull() )
{
// 文字型にキャスト
Sqrat::string name_str = name.Cast<Sqrat::string>();
printf("name = %s\n",name_str.c_str());
}
/// メンバ関数へのアクセス
Function func = Function(objValClass,_SC("printName"));
if( !func.IsNull() )
{
func.Execute();
}
}
}
}
}
// VMを解放
sq_close(vm);
return 0;
}
SquirrelのコンパイラはC++のライブラリとしてリンクしております。
また、バインダにSqratを利用しております。Squirrelのファイルとして
enemy.nut
enemySquirrel.nutを用意します。
enemy.nutの中身は単純に各関数の実行確認と名前の設定です。
local inst = Enemy();
inst.m_hp = 2;
inst.m_name = "敵1";
inst.name();
inst.shot();
inst.up();
inst.down();
inst.left();
inst.right();
inst.shot();
enemySquirrel.nutの中身はクラスの定義と
そのインスタンスを作ってます。
// Squirrel 内部で定義した敵クラス
class EnemySquirrel
{
constructor(v)
{
name = v;
}
function printName()
{
print(name + "(.nut内の関数)\n");
}
name = null;
}
// ルートテーブルに敵2設定
enemy2 <- EnemySquirrel("敵2(.nut内部)");
順を追って解説をしていきます。
初めにSquirrelのコンパイラなどはvirtual machine上で動くため
その生成と利用するための設定を行なっております。
次にbindMyTable関数の中で
C++ 内部で定義したクラスをSquirrelで使用する準備をしております。
Squirrelは、テーブルとスロットという機能があり、
そこに変数やクラス、関数を設定していきます。
SqratのScriptクラスを使ってコンパイル実行することができます。
Script script;
// C++ 定義のクラスをSquirrelから扱う
printf("\n--- Compile Squirrel Start---\n");
script.CompileFile("enemy.nut");
script.Run();
これでC++で定義した機能をSquirrelで呼び出すことができました。
一つの機能だけを外に置いておきたい場合などはこれだけでも利用ができます。
ただし、もっと複雑なことをしたい時は、
Squirrel側でクラスを定義してC++側で利用したいこともあるはずです。
その時は、Squirrelのファイルをコンパイル、実行した後、
C++で利用します。
script.CompileFile("enemySquirrel.nut");
script.Run();
// Squirrel 側で定義したクラスを C++ 内部で扱う
// enemy2 <- EnemySquirrel("敵2");
Object objValClass = RootTable(vm).GetSlot(_SC("enemy2"));
if( !objValClass.IsNull() )
{
/// メンバ変数へのアクセス
Object name = objValClass.GetSlot(_SC("name"));
if( !name.IsNull() )
{
// 文字型にキャスト
Sqrat::string name_str = name.Cast<Sqrat::string>();
printf("name = %s\n",name_str.c_str());
}
/// メンバ関数へのアクセス
Function func = Function(objValClass,_SC("printName"));
if( !func.IsNull() )
{
func.Execute();
}
}
これでSquirrel側で定義したクラスをC++で実行することができるようになりました。
初期化、毎フレームの処理、終了処理などの関数名を事前に決めておき、
その処理をSquirrel側で書いてC++で利用すればクラスの機能に修正が入っても
長いビルド時間など待たずに修正と実行が可能となります。
Squirrelが実際のゲームに使用された例として
ファイナルファンタジー・クリスタルクロニクルが上げられます。
また、多くのゲームエンジンにこういったスクリプト言語が組み込んであり、
修正と実行のサイクルが素早く行えるようになっております。
是非、Squirrelの面白さに皆さんも触れてください
SquirrelSqratライセンスは
MIT licence となります。
今回のサンプルが動かせる実行ファイルを置いておきます。
細かいエラー処理は行なっておりませんがご了承ください。
SquirrelTest