Autosplitter を制御するための asl ファイルの作成を補助してくれる asl-help というツールがあります。
この記事では Unity 製ゲームにおいての asl-help の基本的な使い方を説明します。
もくじ
はじめに
asl-help の特徴
Unity 製ゲームの asl 作成で asl-help を使う利点
ポインタパスを探さなくても良い
基本的な asl の作り方をする場合に最も手間がかかり得る工程が、変数の値を持っているメモリアドレスのポインタパスを探すことです。
さらに、ゲームがバージョンアップすると基本的には探しなおしが必要です。
asl-help ではクラス名と変数名を利用してメモリアドレスの値を監視できるためポインタパスを探さないでもよく、ゲームプログラムのクラス名と変数名が変わらないならゲームのバージョンアップに強いです。
シーンを利用しやすい
タイトル画面、プレイ画面、ゲームオーバー画面などがそれぞれ別のシーンとして作られているゲームであれば、そのシーンの切り替えを asl で監視できます。
筆者の能力が足りないだけかもしれないですが、基本的な方法でシーンのポインタパスを探そうとすると難しいです。
注意点
クラス名、変数名を知っている必要がある
たぶんここが最もネックになり得るポイントだと思います。
クラス名が分かれば asl-help が変数名を教えてくれるのですが、そのクラス名は自力で調べなければなりません。
あるいはポインタパスを知っている必要がありますが、ポインタパスが分かるなら asl-help を使う必要性が薄まります。
static でない変数は利用できない
asl 制作者にはどうしようもない部分。
自動プロパティの変数の値を取得することもできるらしいですが、筆者が理解できていない範囲なので説明ができません。
Dictionary は取得できない
配列や List は取得できますが、Dictionary は取得できないらしいです。
(たぶん)ドキュメントはない
かつてはドキュメントがありましたが、この記事を書いた時点では削除されています。
asl-help に関する情報を調べたい場合は関連リンクを参照。
GPL ライセンス
asl-help は GPL ライセンスなので、asl-help を利用している asl も GPL ライセンスに従う必要があります。
asl はソースコードそのものを配布しているようなものなので、ソースコードの開示義務については特に問題ないかと思います。
この記事の前提
前提条件
対象のゲームが Unity 製であること
asl-help 自体は Unity 製ゲームに限らず利用できるらしいですが、筆者が理解できていないので説明できません。
ゲームのプログラムで使われているクラス名や変数名を知る手段を持っていること
たぶんこの記事の範囲ではここが最もハードル高い部分だと思います。
ゲームによってはクラスの構造解析などが有効なので、試してみてください。
asl のスクリプトを記述できること
asl-help はあくまで asl の作成を補助するツールで、この記事はあくまで asl-help の使い方を説明する記事です。
asl の基本的な書き方についてはここでは特に説明しないので、Autosplitter の公式ドキュメントやasl の作り方を紹介している記事にてご確認ください。
関連ファイルは全て「Livesplit/Components フォルダ」に
ダウンロードして入手した asl-help ファイルと作った asl ファイル、場合によっては xml ファイルの 2 つ or 3 つのファイルは、Livesplit/Components
フォルダに入れている前提で説明しています。
他の場所にあると説明の通りにならないのでご注意ください。
用意しておくもの
必須ファイル asl-help
asl-help を利用した Autosplitter は、作者にも利用者にも asl-help というファイルが必須なので、各自でダウンロードしてください。
ファイルのバージョン
asl-help を使っている asl で Livesplit のデータベースに登録されているものは、記事執筆時点では全て特定のバージョンの asl-help を使うように設定されています。
そのため、最新版の asl-help を使うよりもデータベースで指定されているバージョンの asl-help を使った方が無難かもしれません。
(指定バージョンは変わるかもしれないので、ここでは明確には示しません。都度、データベースにてご確認ください)
ファイルの保存場所
asl-help ファイルはどの前例でも Livesplit/Components
フォルダに入れているため、特別な理由がない限りはここに入れるようにしましょう。
ほぼ必須アプリ DebugView
DebugView はプログラムが出力するログメッセージを表示できるアプリケーションで、asl を作る際にほぼ必須のアプリです。
asl-help を使って asl を作る場合も同様にほぼ必須です。
エラーメッセージを含め、asl-help からの色々なメッセージが表示されます。
メッセージを確認する手段は他にもあるので、お気に入りの方法があるなら DebugView でなくても大丈夫です。
関連リンク
困ったら
ドキュメントは(おそらく)ないため、より詳しい情報が欲しい方は Discord の Speedrun Tool Development サーバーにて過去ログを調べたり質問したり、あるいはソースコードを読み解くなどしてください。
前例を探す
asl-help を利用している既存の asl も参考になります。
Livesplit のデータベースに登録されている asl であれば、「asl-help」でページ内検索すれば見つけやすいかと思います。
ここ以外の解説資料
Mitchell Merry さんによるドキュメントとテンプレートでは、この記事では触れていない部分の解説もなされています。
Jake Rabinowitz さんによる解説記事では、asl-help を使用する場合にゲーム側でどのようなコードを書けばよいかについても触れられています。(こちらの解説記事は asl-help のバージョンが古いことに注意)
スクリプトの記述(基礎編)
asl-help を有効にする
asl-help の使用に必須の記述です。
1.
2.
3.
4.
5.
6.
7.
state("ゲームのプロセス名"){ }
startup
{
Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
vars.Helper.GameName = "ゲーム名";
}
state ブロック
通常の asl と同様にゲームのプロセス名を引数で渡します。
startup ブロック
asl-help ファイルを読み込むことで、その asl の中で asl-helpの機能が利用できるようになります。
なお、既存の asl を参考にする場合、asl によっては asl-help ではなく LiveSplit.ASLHelper.bin を指定しているものがあり、それは古いバージョンの asl-help を利用しているので注意。
シーンの取得
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
state("ゲームのプロセス名"){ }
startup
{
Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
vars.Helper.GameName = "ゲーム名";
vars.Helper.LoadSceneManager = true;
}
update
{
current.SceneName = vars.Helper.Scenes.Active.Name;
}
start
{
if (current.SceneName == "Stage" && old.SceneName == "Title")
return true;
}
シーン
ここでの「シーン」はタイトル画面やゲームのプレイ画面、ゲームオーバー画面などゲームのシーンのことです。
Unity のシーン機能を使っているかどうかはゲームごとに異なるため、ゲームにタイトル画面があるからと言って必ずしもシーンとしてのタイトル画面があるとは限りません。
startup ブロック
vars.Helper.LoadSceneManager = true;
と記述することでシーンの監視ができるようになります。
update ブロック
シーン監視の更新を行います。
vars.Helper.Scenes
たぶん SceneManager クラス。
Active 以外にも、Loaded や Count がメンバとして定義されています。
変数 SceneName
ここでは変数名を SceneName としていますが、任意の名前で大丈夫です。
この記述例の変数 SceneName は通常の asl で state ブロックで定義した変数と同様に、current.SceneName と old.SceneName が使えます。
'System.Dynamic.ExpandoObject' に 'SceneName' の定義がありません
このようなエラーメッセージが表示されることがありますが、特に気にしないでも大丈夫です。
おそらくは最初だけは old.ScenenName が値を持っていないことが理由だと思います。
ただし、変数名をタイプミスしていて本当に存在しない変数を示している場合もこのようなメッセージが表示されるので、そこは注意。
static な変数の値の取得
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
state("ゲームのプロセス名"){ }
startup
{
Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
vars.Helper.GameName = "ゲーム名";
}
init
{
vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
{
// 基本の記述方法
vars.Helper["任意の変数名"] = mono.Make<T>("ゲーム内のクラス名", "_instance", "ゲーム内の変数名", "オフセット");
// 省略形
vars.Helper["任意の変数名"] = mono.Make<T>("ゲーム内のクラス名", "ゲーム内の変数名");
// クラスの指定と変数の指定を分けて行う場合
// 同じクラスから複数の変数を取得する場合に便利な記述方法
var className = mono["ゲーム内のクラス名"];
vars.Helper["任意の変数名"] = className.Make<T>("ゲーム内の変数名");
return true;
});
}
start
{
if (current.任意の変数名 != old.任意の変数名)
return true;
}
init ブロック
ゲーム内のクラス名と変数名を指定して、任意の名前の変数として利用することができるようになります。
ただし、値の取得ができるのは static な変数に限ります。
vars.Helper["変数名"]
current.変数名や old.変数名のように、通常の asl で state ブロックで定義した変数と同様にして利用できます。
mono.Make<T>() メソッド
Make
'System.Dynamic.ExpandoObject' に '変数名' の定義がありません
このようなエラーメッセージが表示されることがありますが、特に気にしないでも大丈夫です。 おそらくは最初だけは old.変数名 が値を持っていないことが理由だと思います。
ただし、変数名をタイプミスしていて本当に存在しない変数を示している場合もこのようなメッセージが表示されるので、そこは注意。
Settings の生成
区間ごとに自動 Split の ON/OFF を切り替えたい場合などに利用する Advanced 欄のチェックボックスを生成することができます。
xml ファイルの記述
単層構造の例
1.
2.
3.
4.
5.
<Settings>
<Setting Id="ID 名" Label="表示名" State="チェックボックスの初期値の bool 値"/>
<Setting Id="ID 名" Label="表示名" State="チェックボックスの初期値の bool 値"/>
<Setting Id="ID 名" Label="表示名" State="チェックボックスの初期値の bool 値"/>
</Settings>
階層構造の例
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<Settings>
<Setting Id="親の ID 名" Label="親の表示名" State="チェックボックスの初期値の bool 値">
<Setting Id="子の ID 名" Label="子の表示名" State="チェックボックスの初期値の bool 値"/>
<Setting Id="子の ID 名" Label="子の表示名" State="チェックボックスの初期値の bool 値"/>
</Setting>
<Setting Id="親の ID 名" Label="親の表示名" State="チェックボックスの初期値の bool 値">
<Setting Id="子の ID 名" Label="子の表示名" State="チェックボックスの初期値の bool 値">
<Setting Id="孫の ID 名" Label="孫の表示名" State="チェックボックスの初期値の bool 値"/>
<Setting Id="孫の ID 名" Label="孫の表示名" State="チェックボックスの初期値の bool 値"/>
</Setting>
</Setting>
</Settings>
"asl ファイルの名前".Settings.xml
まずは「"asl ファイルの名前".Settings.xml」という名前で xml ファイルを作ります。
asl-help ファイルと同じく、この xml ファイルもどの前例でも Livesplit/Components
フォルダに入れているため、特別な理由がない限りはここに入れるようにしましょう。
Setting タグの属性
属性の詳細については SettingsCreator を参照。
asl の記述
1.
2.
3.
4.
5.
6.
7.
8.
state("ゲームのプロセス名"){ }
startup
{
Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
vars.Helper.GameName = "ゲーム名";
vars.Helper.Settings.CreateFromXml("xml ファイルのパス");
}
startup ブロック
vars.Helper.Settings.CreateFromXml()
メソッドを呼んで、その第 1 引数で xml ファイルのパスを指定します。
省略可能な第 2 引数に bool 値を渡すと、Setting タグで State 属性がない項目のチェックボックスの初期値を一括で指定できます。
IGT 計測やロードレスタイム計測の場合に、タイマーの設定が Real Time だったら切り替えを促す
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
state("ゲームのプロセス名"){ }
startup
{
Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
vars.Helper.GameName = "ゲーム名";
// IGT 計測の場合
vars.Helper.AlertGameTime();
// ロードレスタイム計測の場合
vars.Helper.AlertLoadless();
}
Alert****()
Livesplit は内部に Real Time と Game Time という 2 つのタイマーを持っていて、通常は Real Time を使用しますが、IGT 計測やロードレスタイム計測の場合は Game Time を使用します。
Game Time を使いたいのに設定が Real Time になっている場合に、これらのメソッドを使用してタイマーの切り替えをユーザーに促すことができます。
AlertGameTime() も AlertLoadless() も、タイマーが Game Time であるかをチェックしてそうでなかったら切り替えるという挙動は同じで、その際に表示されるメッセージだけが違います。
これらとは逆に Real Time に切り替えるための AlertRealTime() というメソッドも用意されています。
スクリプトの記述(応用編)
Singleton パターン
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// Singleton パターンの実装
public class Singleton<T>
{
private static T _instance;
// 略
}
// 監視したい変数を持っている、Singleton 継承クラス
public class MyClass : Singleton<MyClass>
{
private int roomId;
// 略
}
Singleton パターンとは
Singleton パターンはオブジェクト指向プログラミングにおける定石のひとつで、これを使ったクラスはインスタンスが 1 つしか存在しないクラスとなります。
この定石は Unity 製ゲームにおいても使われることが多く、単独インスタンスにしたいクラスそのものに Singleton パターンを実装したり、専用のベースクラスで Singleton パターンを実装しておいて単独インスタンスにしたいクラスでそれを継承したりします。
自身のインスタンスを持っている
重要なのが、Singleton を利用したクラスは自身のインスタンスを static なメンバとして持っているということです。
asl-help では static な変数の必要がある、という条件を満たしているのです。
使用しているツールによっては任意のクラスを継承しているクラスの一覧を表示することができるため、Singleton クラスを継承しているクラスの一覧から使えそうなクラスを探すこともできます。
ここで注意が必要なのは、Singleton は Unity に標準で用意されているクラスなどではないぽいので、ゲームごとにクラス名やメンバ名が異なって当然であるということです。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
init
{
vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
{
// 基本の記述方法
vars.Helper["roomId"] = mono.Make<int>("MyClass", 1, "_instance", "roomId");
// クラスの指定と変数の指定を分けて行う場合
// _instance の前までをここで指定
var mc = mono["MyClass", 1];
// _instance から後をここで指定
vars.Helper["roomId"] = mc.Make<int>("_instance", "roomId");
return true;
});
}
"MyClass", 1
この第 2 引数の 1 は、Singleton<T> クラスを継承している場合に必要です。
クラスのメンバ変数が他のクラス
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public class GameManager
{
private static Player _player;
// 略
}
public class Player
{
private int _life;
// 略
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
init
{
vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
{
var m = mono["GameManager"];
var p = mono["Player"];
vars.Helper["life"] = m.Make<int>("_player", p["_life"]);
return true;
});
}
Make<T> の引数
m.Make<int>("_player", p["_life"])
の部分は m.Make<int>("_player", "_life")
でも取得できないことはなさそうでしたが、値の挙動が怪しい感じがしたので記述例にあるような記述方法にしておいた方が無難に思います。
クラスの配列を取得
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
public class Player
{
private static Item[] _items;
// 略
}
public class Item
{
public int Id;
public string Name;
// 略
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
init
{
vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
{
var p = mono["Player"];
vars.Helper["items"] = p.MakeArray<IntPtr>("_items");
vars.OffsetItemId = mono["Item"]["Id"];
vars.OffsetItemName = mono["Item"]["Name"];
return true;
});
}
update
{
int itemId;
string itemName;
if (current.items.Length > old.itemsLength)
{
var newItem = current.items[current.items.Length - 1];
itemId = vars.Helper.Read<int>(newItem + vars.OffsetItemId);
itemName = vars.Helper.ReadString(newItem + vars.OffsetItemName);
}
// 略
}
init ブロック
MakeArray<T> ではクラスそのものではなく IntPtr を指定して、配列のポインタを取得します。
また、配列になっているクラスのメンバのオフセット値を取得します。
取得した配列の任意のメンバの利用
ここでの current.items は配列なので、配列で普通にやるように添え字による任意要素へのアクセスや foreach による全ての要素へのアクセスなどができます。
しかし、あくまで IntPtr の配列なので、クラス内の任意メンバへのアクセスには「ポインタ+オフセット値」による指定が必要です。
各メンバの値の取得は Read<T>() や ReadString() を使います。
List の取得
1.
2.
3.
4.
5.
6.
public class Player
{
private static List<string> _items;
// 略
}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
init
{
vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
{
var p = mono["Player"];
vars.Helper["items"] = p.MakeList<IntPtr>("_items");
return true;
});
}
update
{
var item = current.items[任意の位置を指定];
var itemName = vars.Helper.ReadString(false, item);
// 略
}
init ブロック
MakeList<T> で指定する型は null を入れられない型に限られるようで、例えば string の場合は IntPtr を指定します。
取得した List の任意のメンバの利用
ここでの current.items は List なので、List で普通にやるように添え字による任意要素へのアクセスや foreach による全ての要素へのアクセスなどができます。
しかし、あくまで IntPtr の List です。 各メンバの値の取得は Read<T>() や ReadString() を使います。
この例の場合は最終的に string 型の値が欲しいため、ReadString() を使います。 第 1 引数に false を渡さないとうまく取得できないため注意。
Settings の生成で説明を追加
返信削除注意点でライセンスについての記述を追加
前提でファイルの保存場所についての記述と
使用する asl-help のバージョンについてを追加
誤字などの修正
関連リンクに「ここ以外の解説資料」を追加
返信削除Singleton パターンを追加
返信削除一部文字のエスケープ忘れを修正
返信削除スクリプトの記述を基礎編と応用編に分割
返信削除次の項目を追加
クラスのメンバ変数が他のクラス
クラスの配列を取得
List の取得
「IGT 計測やロードレスタイム計測の場合に、タイマーの設定が Real Time だったら切り替えを促す」を追加
返信削除