スクリプト言語AngelScriptの紹介

# 要約
静的型付けスクリプト言語（グルー言語）のAngelScriptの紹介です。  
公式サイト(https://www.angelcode.com/angelscript/)

AngelScriptのクラスについては[別記事](https://qiita.com/unknown_ds/items/317e1c0e03ce4ed80eae "スクリプト言語AngelScriptでのクラスの実装と機能について")に記載しています。

使用したコードはGitHubに置いてあります。(内容は一緒)
https://github.com/unknown-ds/angelscript_sample
https://github.com/unknown-ds/angelscript2.34.0_sample
https://github.com/unknown-ds/angelscript2.35.0_sample

UnrealEngine5へ組み込むプラグインがあるようです。（エンジン修正＋プラグイン？）
[Unreal Engine Angelscript](https://angelscript.hazelight.se/)


## 更新履歴
| 日付 | 内容 |
|:----|:-----|
|2019/12/14| ver.2.34.0 での動作を確認|
|2020/12/26| ver.2.35.0 での動作を確認|
|2022/10/28| ver.2.36.0 での動作を確認|
|2023/03/09| UnrealEngine Angelscriptについて追記|


## Luaと比較した特徴
スクリプト言語で有名なLuaと比較した特徴を挙げてみます。

||Lua|AngelScript|
|---|---|---|
|変数型付け|動的|静的|
|文法|Cライクな別物？|C++ライク|
|バインダ|別で用意する(toLua++など)|標準で装備|
|実行速度|速い|Luaより少し遅い（未確認）|
|ドキュメント|豊富|少ない|

### 長所
- 静的型付けなのでコード量がある程度以上の場合はコードの把握し易くなる。
- 文法がC++にかなり似ているので学習コストが低い。

### 短所
- ドキュメントが少ない。
- アドオンやツール等が少ない。


# プロジェクトの作成
angelscript_2.34.0\sdk\angelscript\projects にプロジェクトファイルが入っていますが、自前で作成してみます。

## 環境
- Windows10 64bit  
- Visual Studio 2017
- AngelScript ver.2.33.0, 2.34.0, 2.35.0, 2.36.0  

## 使用アドオン
- scriptarray
- scriptbuilder
- scriptstdstring
- scriptmath 


### ローカル環境とソリューション作成
1. ローカル環境にプロジェクト用フォルダを用意。
1. VisualStudioで新規プロジェクト作成（windowsコンソールアプリケーション）
1. プロジェクトフォルダに[sdk]を作成、その下に[add_on][angelscript]フォルダを作成。[angelscript]フォルダにangelscrpt_2.33.0.zipから解凍した[sdk/angelscript]の[include]と[source]をコピー。[add_on]フォルダに[sdk/add_on]の[scriptarray]と[scriptbuilder]と[scriptstdstring]をコピー
1. 上記をVisualStudioプロジェクトに追加。
1. スクリプトエンジン作成、関数登録用のクラスを作成するための.cpp./hファイルを追加。

```
ローカルフォルダ構成
└─as_compiler(プロジェクトフォルダ)
　　└─sdk
　　　　├─add_on
　　　　│　├─scriptarray
　　　　│　├─scriptbuilder
　　　　│　├─scriptmath
　　　　│　└─scriptstdstring
　　　　└─angelscript
　　　　　　├─include
　　　　　　└─source
```

![proj_tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/353ffeaa-711f-bc04-881d-be4f3a69cc2a.png)

### プリコンパイルヘッダの使用
プリコンパイルヘッダの使用のために各cppファイルに
`#include "stdafx.h"`
を追加。
「プリコンパイル済みヘッダー ファイルを開けません。」というエラーが出てしまう場合は、以下の設定
- プロジェクトのプロパティ
[構成プロパティ]→[C/C++]→[プリコンパイル済みヘッダー]→[プリコンパイル済みヘッダー]→[使用 (/Yu)]
- StdAfx.cppのプロパティ
[構成プロパティ]→[C/C++]→[プリコンパイル済みヘッダー]→[プリコンパイル済みヘッダー]→[作成 (/Yc)]

### カスタムビルドツールの設定
カスタムビルドツールはファイルごとに設定します。
ファイルのプロパティを開き、「構成プロパティ」→「全般」→「項目の種類」から「カスタム ビルド ツール」を選択し、「適用」する。「構成プロパティ」に「カスタム ビルド ツール」の項目が表示されます。

[as_callfunc_x64_msvc_asm.asm]をプロジェクトに入れてプロパティ→[項目の種類]→[カスタムビルドツール]

```
▼コマンドライン
ml64.exe /c  /nologo /Fo"$(Configuration)\as_callfunc_x64_msvc_asm.obj" /W3 /Zi /Ta "%(RootDir)%(Directory)\%(Filename)%(Extension)"
▼出力ファイル
$(Configuration)\as_callfunc_x64_msvc_asm.obj
```

これでビルドが通るはず。。:slight_smile:


# 実装
スクリプトに必要な処理を幾つか挙げます。
### スクリプトエンジン生成
script_mgr.hにシングルトンクラスCScriptMgrを用意、初期化メソッドInit()内で準備を行います。

```C++
// スクリプトエンジンを生成
mpEngine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
if (mpEngine == 0) {
	printf("Failed to create script engine.\n");
	return;
}
```

### アドオンの登録
アドオンの登録です、自前のアドオンやクラスがある場合も同じように初期化時で登録する必要があります。

```C++
// arrayアドオンの登録
RegisterScriptArray(mpEngine, true);
// stringアドオンの登録
RegisterStdString(mpEngine);
RegisterStdStringUtils(mpEngine);
// mathアドオンの登録
RegisterScriptMath(mpEngine);
```

### C++側での関数実装
Native版はそのまま実装、Generic版はasIScriptGenericクラスにある引数取得メソッドから引数を取得します。intやfloatなどの値型の取得にはGetArgWord()やGetArgFloat()が使えます。
返り値がある場合は返り値設定メソッドSetReturn???()で設定します。(???は型名)

```C++
// スクリプトにバインドするテスト関数１(Native)
float testBindFunc1(
	float _Lhs,			// 引数１
	float _Rhs			// 引数２
	)
{
	return (_Lhs + _Rhs);
}

// Generic
void testBindFunc1Generic(
	asIScriptGeneric *pGen
	)
{
	// 引数取得
	// （この場合は _Lhs = pGen->GetArgFloat(0); でもよい）
	float _Lhs = *reinterpret_cast<float*>(pGen->GetAddressOfArg(0));
	float _Rhs = *reinterpret_cast<float*>(pGen->GetAddressOfArg(1));
	// float型返り値設定
	pGen->SetReturnFloat(testBindFunc1(_Lhs, _Rhs));
}
```

### C++側で実装した関数をバインド
C++側で実装している関数の登録です。NativeとGenericをそれぞれ用意しています。
不要なら一方でも可、その場合はas_config.hにて `#define AS_MAX_PORTABILITY` を定義してGeneric版のみにしたほうがいいと思います。

```C++
// C++実装関数の登録、登録時のresultを細かくチェックすると良い。
if (!strstr(asGetLibraryOptions(), "AS_MAX_PORTABILITY")) {
	// Native
	result = mpEngine->RegisterGlobalFunction("void printf(const string &in)", asFUNCTION(asPrintf0), asCALL_CDECL); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("void printf(const string &in, ?&in)", asFUNCTION(asPrintf1), asCALL_CDECL); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("float testBindFunc1(float, float)", asFUNCTION(testBindFunc1), asCALL_CDECL); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("int testBindFunc2(array<int> &in, int)", asFUNCTION(testBindFunc2), asCALL_CDECL); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("string testBindFunc3(string)", asFUNCTION(testBindFunc3), asCALL_CDECL); assert(result >= 0);
}else{
	// Generic
	result = mpEngine->RegisterGlobalFunction("void printf(const string &in)", asFUNCTION(asPrintf0Generic), asCALL_GENERIC); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("void printf(const string &in, ?&in)", asFUNCTION(asPrintf1Generic), asCALL_GENERIC); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("float testBindFunc1Generic(float, float)", asFUNCTION(testBindFunc1Generic), asCALL_GENERIC); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("int testBindFunc2Generic(array<int> &in, int)", asFUNCTION(testBindFunc2Generic), asCALL_GENERIC); assert(result >= 0);
	result = mpEngine->RegisterGlobalFunction("string testBindFunc3Generic(string)", asFUNCTION(testBindFunc3Generic), asCALL_GENERIC); assert(result >= 0);
}
```

# 実行
スクリプトをコンパイルの後、実行します。
また予めコンパイル後にバイトコードへ出力して、実行時はバイトコードを読んでコンパイルなしにすることもできます。
### 準備
```C++
int result;
asIScriptModule *pModule = mpEngine->GetModule(pModuleName);
		
// スクリプトコンテキストの作成、（スクリプトの実行単位で必要）
asIScriptContext *pCtx = mpEngine->CreateContext();
if (!pCtx) {
	printf("Failed to create the context.\n");
	mpEngine->Release();
	return 0;
}
```

### スクリプト側実装の関数をC++側で実行する１
返り値なし、引き数なしの関数testFunction1の実行例です。

```C++
// 関数を探す
asIScriptFunction *pFunc0 = pModule->GetFunctionByDecl("void testFunction1()");
// 準備
pCtx->Prepare(pFunc0);
// 実行
result = pCtx->Execute();
```

### スクリプト側実装の関数をC++側で実行する２（エラー処理付）
返り値float、引き数(float, float)の関数testFunction2の実行例です。
エラー後はコンテキストの解放（pCtx.Release()）を忘れずに行う。

```C++
asIScriptFunction *pFunc = pModule->GetFunctionByDecl("float testFunction2(float, float)");
if (pFunc == NULL) {
	printf("Function not found.\n");
	return 0;
}
result = pCtx->Prepare(pFunc);
if (result != 0) {
	printf("Failed to prepare the context.\n");
	return 0;
}
// 引き数をセット
result = pCtx->SetArgFloat(0, 5.0f);
if (result != 0) {
	printf("Failed to set arments.[%d]\n", result);
	return 0;
}
result = pCtx->SetArgFloat(1, 3.0f);
if (result != 0) {
	printf("Failed to set arments.[%d]\n", result);
	return 0;
}

// 実行
result = pCtx->Execute();
if (result != asEXECUTION_FINISHED) {
	// 通常終了しなかった場合
	if (result == asEXECUTION_ABORTED){
		// 割り込み強制終了
		printf("Script aborted.");
	}else
	if (result == asEXECUTION_EXCEPTION){
		// 例外発生
		printf("Script threw Exception.\n");
		asIScriptFunction *func = pCtx->GetExceptionFunction();
		printf("%s, %s, %s[line:%d] %s\n"
			, func->GetDeclaration()
			, func->GetModuleName()
			, func->GetScriptSectionName()
			, pCtx->GetExceptionLineNumber()
			, pCtx->GetExceptionString());
	}else
	if (result == asEXECUTION_SUSPENDED){
		// 中断終了
		int line = pCtx->GetExceptionLineNumber();
		printf("Script suspend.[line:%d]\n", line);
	}else{
		// 不明なエラー
		printf("The script ended for some unforeseen reason (%d).\n", result);
	}
}else{
	// 通常終了
	float returnValue = pCtx->GetReturnFloat();	// 返り値取得
	printf("C++ testFunction2: %f\n", returnValue);
}

```

### C++側で実装している関数をスクリプト側で呼ぶ
RegisterGlobalFunction()登録した第1引数の名前でそのまま呼べます。

```test00.as
float ret1 = testBindFunc1(1.0f, 2.0f);   // -> 3.0f
```

呼び出しミス（引数の数や型の間違い）の場合は以下のようなエラーが出ます。
スクリプト名とエラー行数がでます。ただしエラーがあると即終了するため、複数エラーがあってもまとめては出力してくれないようです。

```
// スクリプトファイルtest00.asにて、testBindFunc1の引数を間違えたエラー例（登録されている引数タイプが見当たらない）
test00.as (38, 1) : INFORMATION : Compiling void testFunction1()
test00.as (50, 15) : ERROR : No matching signatures to 'testBindFunc1(float&, float, const float)'
Assertion failed: pMsg->type != asMSGTYPE_ERROR, file c:\as_compiler\script_mgr.cpp, line 186
```

# その他
インターフェイスや列挙型も実装可能なのでかなりC++に近い。
スクリプト文法での違う点をいくつか列挙します。
### キャスト
```
// キャスト用の関数がある感じです。
int a = 10;
float b = float(a);  // -> 10.0f
```

### 配列
```
// C++ の std::array ではなく std::vectorに近い。
array<float> arr = {1.0f, 2.0f, 3.0f};
arr.insertLast(4.0f);　　// 最後尾に追加
```

### クラスオブジェクト
```
class CTest {}           // CTestクラスがあるとして(中身省略)
CTest ctest = CTest();   // オブジェクト生成
ref @hdle = CTest();     // 参照（アドオンscripthandle必須）
```

# まとめ
C++ライクで使いやすいスクリプト言語だと思います。
使用する方が増えてくれることを願っています。:pray:

## 追記
・ver.2.34.0 で `property` が予約語に追加されています。

・ver.2.36.0 を VS2017でビルドした時に `Windows SDK バージョン 10.0.xxx.0 が見つかりませんでした。` というエラーがでる場合は、プロジェクトを右クリックして`プロジェクトの再ターゲット` をして WindowsSDKバージョンを変えて試してみてください。