* 概要 [#r6930bf3] #contents ** このページについて [#ie2b0c1a] AngelScript(以下AS)を全く知らない人が,ASについてなんとなく理解してもらえるような記事を書きます。 細かいことについては各ページを参照してください。 ** 特徴 [#laa17591] ASはLightweightLanguage(以下LL)の1つでLuaやSquirrelなどと同じような用途で使われます。 ''静的型付け'' ASの最大の特徴は''静的型付け''にあります。 静的型付けとはC/C++などと同様に 未定義の型・シンボルの使用をコンパイル時にチェックすることができ もしそのようなものが使用されていたらエラー扱いにできます。 LuaやSquirrelなど他のLLの多くは動的型付けを採用しています。 動的型付けですと,コンパイルという作業をスキップできる代わりに 実際にそのコードが実行されるまで未定義の型・シンボルのチェックができません。 小規模・少人数なアプリケーションの開発では動的型付けでもなんとかやっていけますが 大規模・多人数なアプリケーションの開発になればなるほど, コンパイル時エラーチェックの重要性が増してきます。 Squirrelでゲームのほとんどを実装したという 「小さな王様と約束の国 ファイナルファンタジー・クリスタルクロニクル」の開発者さんが 「自前である程度のスクリプトコードの事前チェックをできるようにした」というような内容を話していました。 それぐらいアプリケーション実行前のエラーチェックはアプリケーションの品質維持に重要なものです。 ''構文がC++にとても似ている'' ASは構文がC++にとても似ているため,C++経験者であれば構文について少し勉強するだけでコーディングを始められます。 また,C++ソースコードをメインとして使用するプロジェクト(ほとんどのゲーム開発はC++を使っています)では ASソースコードとC++ソースコードの2つをコーディングしていくことになります。 そのような状況では,両者の構文が似ていることから頭を大きく切り換える必要がなくなります。 これが意外と重要なことで,ミスや変な混乱が少なくなります。 ''C/C++バインディング機能'' ASのスクリプトからC/C++の関数や構造体・クラスなどにアクセスができ 逆にC/C++からASの関数や構造体・クラスなどにアクセスができます。 C/C++の関数・構造体・クラスなどをASのスクリプトからアクセスできるようにするには ASライブラリの何個かの関数を呼ぶだけで大丈夫です。 ''コルーチン機能'' 途中で処理を中断するコルーチン機能に対応しています。 この機能があるためテキストアドベンチャースクリプトやRPGのイベントスクリプトとして ASを導入することが比較的簡単です。 ''バイトコードをバイナリ保存可能'' スクリプトコードをデータとしてROM媒体(光ディスクやフラッシュカードのこと)に収録すると ソースコードを配布していることと同じになってしまいます。 これは会社・団体的にNGなことがよくあります。 ASではスクリプトコードをコンパイルしたあと,バイトコードを出力するという機能があります。 バイトコードとは,C言語でいうところのコンパイル後のオブジェクトファイルに相当するもので それだけを見てもぱっとは何をしているのか分からない状態になります。 この機能を使い,製品版のROM媒体にはスクリプトコードでなくバイトコードをのせることで ソースコード配布状態を回避することができます。 ''十分なデバッグ機能'' スクリプトをメインに使ってゲーム開発をするとなるとデバッグ機能が必要になります。 ASでは ・実行ファイル・行の取得 ・変数の中身のダンプ ・コールトレースの取得 ・エラー・例外ハンドリング といった機能をライブラリが提供しています。 これらの機能をアプリケーション側で使えるようにさえすれば バグ調査時も特別な苦労はせずにデバッグすることができます。 ** HelloWorld [#n16ecae6] ASのスクリプトを実行するサンプルコードです。 本家のsampleコードを参考にしています。 #code(c,){{ // include #include <iostream> // cout #include <assert.h> // assert() #include <string.h> // strstr() #include <conio.h> // kbhit(), getch() #include <windows.h> // timeGetTime() // AS用ヘッダのインクルード #include <angelscript.h> #include "../../../add_on/scriptstring/scriptstring.h" // コンソール出力用関数 void PrintString(string &str) { std::cout << str; } // メイン関数 int main(int argc, char **argv) { // スクリプトエンジンの作成 asIScriptEngine *engine = asCreateScriptEngine(ANGELSCRIPT_VERSION); if( engine == 0 ) {// エラー cout << "Failed to create script engine." << endl; return -1; } // スクリプトをコンパイル r = CompileScript(engine); if( r < 0 ) {// エラー engine->Release(); return -1; } // コンテキストを作成 // 1つのコンテキストは1つの実行スレッドのようなもの。 asIScriptContext *ctx = engine->CreateContext(); if( ctx == 0 ) {// エラー cout << "Failed to create the context." << endl; engine->Release(); return -1; } // Find the function id for the function we want to execute. int funcId = engine->GetModule(0)->GetFunctionIdByDecl("float calc(float, float)"); if( funcId < 0 ) { cout << "The function 'float calc(float, float)' was not found." << endl; ctx->Release(); engine->Release(); return -1; } // Prepare the script context with the function we wish to execute. Prepare() // must be called on the context before each new script function that will be // executed. Note, that if you intend to execute the same function several // times, it might be a good idea to store the function id returned by // GetFunctionIDByDecl(), so that this relatively slow call can be skipped. r = ctx->Prepare(funcId); if( r < 0 ) { cout << "Failed to prepare the context." << endl; ctx->Release(); engine->Release(); return -1; } // Now we need to pass the parameters to the script function. ctx->SetArgFloat(0, 3.14159265359f); ctx->SetArgFloat(1, 2.71828182846f); // Set the timeout before executing the function. Give the function 1 sec // to return before we'll abort it. timeOut = timeGetTime() + 1000; // Execute the function cout << "Executing the script." << endl; cout << "---" << endl; r = ctx->Execute(); cout << "---" << endl; if( r != asEXECUTION_FINISHED ) { // The execution didn't finish as we had planned. Determine why. if( r == asEXECUTION_ABORTED ) cout << "The script was aborted before it could finish. Probably it timed out." << endl; else if( r == asEXECUTION_EXCEPTION ) { cout << "The script ended with an exception." << endl; // Write some information about the script exception int funcID = ctx->GetExceptionFunction(); asIScriptFunction *func = engine->GetFunctionDescriptorById(funcID); cout << "func: " << func->GetDeclaration() << endl; cout << "modl: " << func->GetModuleName() << endl; cout << "sect: " << func->GetScriptSectionName() << endl; cout << "line: " << ctx->GetExceptionLineNumber() << endl; cout << "desc: " << ctx->GetExceptionString() << endl; } else cout << "The script ended for some unforeseen reason (" << r << ")." << endl; } else { // Retrieve the return value from the context float returnValue = ctx->GetReturnFloat(); cout << "The script function returned: " << returnValue << endl; } // We must release the contexts when no longer using them ctx->Release(); // Release the engine engine->Release(); return 0; } void ConfigureEngine(asIScriptEngine *engine) { int r; // Register the script string type // Look at the implementation for this function for more information // on how to register a custom string type, and other object types. // The implementation is in "/add_on/scriptstring/scriptstring.cpp" RegisterScriptString(engine); if( !strstr(asGetLibraryOptions(), "AS_MAX_PORTABILITY") ) { // Register the functions that the scripts will be allowed to use. // Note how the return code is validated with an assert(). This helps // us discover where a problem occurs, and doesn't pollute the code // with a lot of if's. If an error occurs in release mode it will // be caught when a script is being built, so it is not necessary // to do the verification here as well. r = engine->RegisterGlobalFunction("void Print(string &in)", asFUNCTION(PrintString), asCALL_CDECL); assert( r >= 0 ); r = engine->RegisterGlobalFunction("uint GetSystemTime()", asFUNCTION(timeGetTime), asCALL_STDCALL); assert( r >= 0 ); } else { // Notice how the registration is almost identical to the above. r = engine->RegisterGlobalFunction("void Print(string &in)", asFUNCTION(PrintString_Generic), asCALL_GENERIC); assert( r >= 0 ); r = engine->RegisterGlobalFunction("uint GetSystemTime()", asFUNCTION(timeGetTime_Generic), asCALL_GENERIC); assert( r >= 0 ); } // It is possible to register the functions, properties, and types in // configuration groups as well. When compiling the scripts it then // be defined which configuration groups should be available for that // script. If necessary a configuration group can also be removed from // the engine, so that the engine configuration could be changed // without having to recompile all the scripts. } int CompileScript(asIScriptEngine *engine) { int r; // We will load the script from a file on the disk. FILE *f = fopen("script.as", "rb"); if( f == 0 ) { cout << "Failed to open the script file 'script.as'." << endl; return -1; } // Determine the size of the file fseek(f, 0, SEEK_END); int len = ftell(f); fseek(f, 0, SEEK_SET); // On Win32 it is possible to do the following instead // int len = _filelength(_fileno(f)); // Read the entire file string script; script.resize(len); int c = fread(&script[0], len, 1, f); fclose(f); if( c == 0 ) { cout << "Failed to load script file." << endl; return -1; } // Add the script sections that will be compiled into executable code. // If we want to combine more than one file into the same script, then // we can call AddScriptSection() several times for the same module and // the script engine will treat them all as if they were one. The script // section name, will allow us to localize any errors in the script code. asIScriptModule *mod = engine->GetModule(0, asGM_ALWAYS_CREATE); r = mod->AddScriptSection("script", &script[0], len); if( r < 0 ) { cout << "AddScriptSection() failed" << endl; return -1; } // Compile the script. If there are any compiler messages they will // be written to the message stream that we set right after creating the // script engine. If there are no errors, and no warnings, nothing will // be written to the stream. r = mod->Build(); if( r < 0 ) { cout << "Build() failed" << endl; return -1; } // The engine doesn't keep a copy of the script sections after Build() has // returned. So if the script needs to be recompiled, then all the script // sections must be added again. // If we want to have several scripts executing at different times but // that have no direct relation with each other, then we can compile them // into separate script modules. Each module use their own namespace and // scope, so function names, and global variables will not conflict with // each other. return 0; } void LineCallback(asIScriptContext *ctx, DWORD *timeOut) { // If the time out is reached we abort the script if( *timeOut < timeGetTime() ) ctx->Abort(); // It would also be possible to only suspend the script, // instead of aborting it. That would allow the application // to resume the execution where it left of at a later // time, by simply calling Execute() again. } // Function implementation with native calling convention void PrintString(string &str) { cout << str; } // Function implementation with generic script interface void PrintString_Generic(asIScriptGeneric *gen) { string *str = (string*)gen->GetArgAddress(0); cout << *str; } // Function wrapper is needed when native calling conventions are not supported void timeGetTime_Generic(asIScriptGeneric *gen) { gen->SetReturnDWord(timeGetTime()); } }} ** スクリプトのサンプルコード [#e7d5afb5] 以下がASのソースコードの例です。 #code(c,){{ // クラスの宣言。 class Vector3 { // メンバ変数の宣言 // public,privateなどのアクセス修飾子はありません float x; float y; float z; // デフォルトコンストラクタ Vector3() { } // コンストラクタ Vector3(float aX, float aY, float aZ) { mX = aX; mY = aY; mZ = aZ; } // デストラクタ ~Vector3() { } // メンバ関数 void add( const Vector3 &in aVec // 引数にconstや参照修飾子を付けることができます ) { mX += aVec.x; mY += aVec.y; mZ += aVec.z; } // constメンバ関数 float sum()const { return mX + mY + mZ; } // 演算子のオーバーロード // これは += をオーバーロードしています Vector3@ opAddAssign( const Vector3 &in aVec ) { add( aVec ); return this; } }; // セミコロンはあってもなくても可能。 // enumの宣言 enum SampleKind { SampleKind_A , SampleKind_B // term , SampleKind_TERMINATE , SampleKind_MIN = 0 , SampleKind_MAX = SampleKind_TERMINATE-1 }; // interfaceを使ったサンプル // C#やD言語のinterfaceと同様です interface IUpdatable { void updatableUpdate(); }; interface IDrawable { void drawableDraw()const; }; class Chara : IUpdatable , IDrawable { void updatableUpdate() { } void drawableDraw()const { } }; // グローバル変数 int globalInt32Value = 0; // グローバル関数 void func() { // 組み込み型の紹介 int8 int8val; int16 int16val; int int32val; int32 int32val2; int64 int64val; uint8 uint8val; uint16 uint16val; uint uint32val; uint32 int32val2; uint64 uint64val; bool boolVal; float floatVal; double doubleVal; // 変数は明示的に初期化値を指定できます int initializedValue = 10; // 未確認情報:最新版では初期化値を指定しないと0(boolはfalse)で初期化されるようです。 // D言語やC#のように未初期化変数が発生しない仕組みは素晴らしいですね // (手元はかなり古いr320版を使っており,この当時ではまだ実装されていません) int notInitializedValue; // 0で初期化される // boolの値としてtrue,falseが使えます bool trueValue = true; bool falseValue = false; // const変数が使えます const int constantValue = 100; // 16進数が使えます const int hexValue = 0xFFFF; // float値の接尾語が使えます const float floatValue = 1.2f; // if-else文が使えます if ( true ) { } else { } // for文が使えます for ( int i = 0; i < 10; ++i ) { break; // continue/breakが使えます } // while文が使えます while ( true ) { break; // continue/breakが使えます } // do-while文が使えます do { break; // continue/breakが使えます } while( false ); // switch文が使えます switch( 3 ) { case 1 : break; // breakが使えます case 2 : break; default : break; // defaultが使えます } // クラスのインスタンスはC++と同じように書きます Vector3 vec(1,1,1); // オブジェクトハンドルというポインタのような概念があります Vector3@ vecHandle = @Vector3(); // オブジェクトの寿命は参照カウンタで管理されます // 参照カウンタが0になるとデストラクタが呼ばれます { Vector3@ objA = @Vector3(); // カウンタ1 Vector3@ objB = @objA; // カウンタ2 @objA = null; // カウンタ1 @objB = null; // カウンタ0 デストラクタが呼ばれる }// スコープを抜けるタイミングでnullでないスコープローカル変数の参照カウンタは減ります } }} |