【Unity】スクリプトベースでノベルゲーを作る（XLua）

# 最初に
この記事では、Tencentが公開しているOSSの[XLua](https://github.com/Tencent/xLua)を使わせていただき、量産できるノベルゲーシステムの紹介です。

![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/bd375d84-d6c9-846a-150a-2e7739193542.gif)


## ターゲット
* アドベンチャーゲームを作りたい方
* [宴](https://madnesslabo.net/utage/)が高くて手が出せない方
* テキスト（スクリプトベース）でアドベンチャーゲームを作りたい方

## XLuaとは
Unityのランタイムで動作するLua言語が使えるようになるOSSです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/186879d1-fc0e-6f2f-8126-3a753075df10.png)

今回はこちらの[XLua](https://github.com/Tencent/xLua)を用いて**量産できるノベルゲーシステム**を作りました。

## 拡張すると出来上がるスクリプト

```sample.lua
label("主人公") --名前のところに表示する文字

show_unit("主人公", "boy") -- 登録する名前
text("初めまして、主人公です")
text("アドベンチャーゲームとかで使えるシステムです。")

move_unit("主人公", 100) -- 名前, X, Y
text("右に移動することもできます")

move_unit("主人公", -100, 1)
text("左に移動することもできます")

move_unit("主人公", 0, 1)
text("他にも実装すればさまざまなことができます")
```

これらのスクリプトは、事前に読み込んでコマンドを積み実行していく方式ではなくランタイムで動作します。


### 実行するために必要なこと
1. XLuaをダウンロード

Unity 2018LTS or higher
XLua 2.1.15 or hight
mac os BigSur 11.2.1

https://github.com/Tencent/xLua/releases/download/v2.1.15f1/xlua_v2.1.15.zip
URLから最新のXLuaをダウンロードします。

### 2.Unityプロジェクトに配置

Unityプロジェクトを作成し、Assetsと同階層にダウンロードしてきたXLua/Toolsを配置します。

**projectname/Tools/FileSignature.exe
projectname/Tools/FileSignature.ext.meta
projectname/Tools/XXX...**
![スクリーンショット 2021-04-23 18.28.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/3ebebbc8-85ec-6ad8-5a85-f48e92b0a859.png)

このようになるはずです。

次にAssets/XLuaディレクトリを作成します。
ダウンロードしてきたxlua_v2.1.15/Assets/XLuaの中身を全てAssets/XLua/に配置します。

Assets/Pluginsディレクトリを作成します。（既にある場合は大丈夫です）
ダウンロードしてきたxlua.v2.1.15/Assets/Plugins/の中身を全てAssets/Pluginsに配置します。

ここまでで大体の配置（インポート）は完了です。

参考：https://baba-s.hatenablog.com/entry/2017/09/26/090000


## AssemblyDefinisionの定義

必要な方は設定してください。
残念ながらXLuaは標準でAssembluDefinisionが定義されていませんので自分で作成する必要があります。
**Assets/XLua/Tencent.XLua.asmdef
Assets/XLua/Src/Editor/Tencent.XLua.Editor.asmdef**
以上の二つを定義すれば動作するかと思います。
もちろんEditorはEditorにチェックを入れてXLuaのみを参照します。
XLuaは何も参照しません。
（name spaceをXlua.asmdefにすると命名が被ってエラーになるのでご注意を）

## プログラム

XLuaが正しくインポートされているか確認します。
適当なゲームオブジェクトを作成し、以下のプログラムを実行してください

```Sample.cs
using UnityEngine;
using XLua;

public class Sample : MonoBehaviour
{
    void Start()
    {
        var luaEnv = new LuaEnv();
        luaEnv.DoString("print(\"luaスクリプトの実行\")");
    }
}
```

Consoleにて、**luaスクリプトの実行**と表示されたら成功です🎉


![スクリーンショット 2021-04-23 19.05.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/090a9f86-f405-663a-2f15-f9bf2eaddbc6.png)

### DLLNotFoundとか他のエラーが出た時
以下の対応をしてみてください。

* PlayerSetting.Configuration.ScriptingBackEndをMonoからIL2CPPに変更
![スクリーンショット 2021-04-23 19.02.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/42a5d33a-1476-3832-5518-e904142c494d.png)

* 公式のIssueに上がっているもの
https://github.com/Tencent/xLua/issues/481
https://github.com/Tencent/xLua/issues/766

-----------------------
XLuaのインポート、動作確認が終わりました。
ここから実際にノベルゲーシステムの構成をしていきます。

これは任意ですが、非同期処理やクリック処理などの処理簡潔化のために以下のライブラリを導入しています。
（コードにライブラリのコードが散見されるかもしれませんが、ご容赦ください）

* UniTask

## 実装

**Resources/Scenario/sample.lua.txt**を作成します。
ディレクトリがない場合は作成してください。
![スクリーンショット 2021-04-23 19.12.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/362bad1e-875b-d909-8259-8e002d0f3cb2.png)

sample.lua.txtが動くように実装していきます。

```sample.lua.txt
text("表示するテキスト")
```

一旦このようなスクリプトを記述しておきます。

次に、このtextコマンドの実装をします。

**Resources/Scenario/lib.lua.txt**を作成します。
ディレクトリがない場合は作成してください。

```lib.lua.txt
command = CS.Main.MainPresenter;
util = require 'xlua.util'

function text(text)
    command.ShowText(text)
end
```
(CS.Main.MainPresenterについて呼び出したいpublicでstaticな関数が定義されているC#側のネームスペースを指定すれば大丈夫です)

### Main.MainPresenterの作成
lua側から呼び出す関数をC#側に記述します。
Main.MainPresenterにしていますが、別の名前でも問題ないです。(lib.lua.txtの**CS.Main.MainPresenter**は**CS.変更後の名前**にする必要があります)

```MainPresener.cs
using UnityEngine;
using XLua;

namespace Main
{
    private static MainView _view;
    public class MainPresenter
    {
        public void Initialize(MainView mainView)
        {
            _view = mainView;
            var luaEnv = new LuaEnv();
            var libText = GetScenario("ScenarioLib/lib.lua"); //libの実行
            luaEnv.DoString(libText);
            var scenario = GetScenario("Scenario/sample.lua"); // メインシナリオの実行
            luaEnv.DoString(scenario);
        }
        private string GetScenario(string path)
        {
            return Resources.Load<TextAsset>(path).text;
        }

        public static void ShowText(string text)
        {
            Debug.Log(text);
        }
    }
}
```

この状態で起動してもInitialzieが呼ばれないため、MainViewを作成します。

```MainView.cs
using UnityEngine;
namespace Main
{
    public class MainView : MonoBehaviour
    {
        private void Start()
        {
            var mainPresenter = new MainPresenter();
            mainPresenter.Initialize(this);
        }
    }
}
```
MainViewを適当なゲームオブジェクトにアタッチし起動します。
コンソールに**表示するテキスト**と表示されれば成功です🎉
![スクリーンショット 2021-04-23 19.33.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/2baec7a2-1ffe-e8de-e0c4-08a0881e0ab4.png)

一旦この状態でUI有りの画面に表示してみます。
## UI適用

以下のような構成画面配置にしています。
ここはお好きなデザインにしてみてください。

![スクリーンショット 2021-04-23 19.42.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/5cc00682-27fb-734c-2539-3d321e8b78c3.png)

MainView、MainPresenterにSetText系のメソッドを増やします。

```MainView.cs
using UnityEngine;
using UnityEngine.UI;

namespace Main
{
    public class MainView : MonoBehaviour
    {
        [SerializeField]
        private Text _text;
        private void Start()
        {
            var mainPresenter = new MainPresenter();
            mainPresenter.Initialize(this);
        }

        public void SetText(string text)
        {
            _text.text = text;
        }
    }
}

```


```MainPresenter.cs
using UnityEngine;
using XLua;

namespace Main
{
    public class MainPresenter
    {
        private static MainView _view;
        public void Initialize(MainView mainView)
        {
            _view = mainView;
            var luaEnv = new LuaEnv();
            var libText = GetScenario("ScenarioLib/lib.lua");
            luaEnv.DoString(libText);
            var scenario = GetScenario("Scenario/sample.lua");
            luaEnv.DoString(scenario);
        }
        private string GetScenario(string path)
        {
            return Resources.Load<TextAsset>(path).text;
        }

        public static void ShowText(string text)
        {
            _view.SetText(text);
        }
    }
}
```

Unity Scene上のMainViewコンポーネントにTextをアタッチし、起動すると以下の画面のようになります。
![スクリーンショット 2021-04-23 19.46.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/82223561-d83d-090b-37d2-6ed372cfcfec.png)

これでUIの適用が完了です🎉
ラベルや背景の命令も同様に増やしていくことが可能です。

## クリック待ちを行う
クリック待ちを実装していきます。

現状だと普通のノベルゲーのようにクリックで物語を進めることができません。
テキストを表示するたびにクリック待ちのコマンドを入れることで実装していきます。

### 事前準備
Main直下に以下のC#を追加してください。
lua側からUnityの型を扱いたいときに事前に登録しておく必要があります。
Dotween等も使いたい場合はこちらに追加記述します。

```XLuaGenConfig.cs
using System.Collections.Generic;
using System;
using UnityEngine;
using XLua;

public static class XLuaGenConfig
{
    //lua中要使用到C#库的配置，比如C#标准库，或者Unity API，第三方库等。
    [LuaCallCSharp]
    public static List<Type> LuaCallCSharp = new List<Type>()
    {
        typeof(System.Object),
        typeof(UnityEngine.Object),
        typeof(Vector2),
        typeof(Vector3),
        typeof(Vector4),
        typeof(Quaternion),
        typeof(Color),
        typeof(Ray),
        typeof(Bounds),
        typeof(Ray2D),
        typeof(Time),
        typeof(GameObject),
        typeof(Component),
        typeof(Behaviour),
        typeof(Transform),
        typeof(Resources),
        typeof(TextAsset),
        typeof(Keyframe),
        typeof(AnimationCurve),
        typeof(AnimationClip),
        typeof(MonoBehaviour),
        typeof(ParticleSystem),
        typeof(SkinnedMeshRenderer),
        typeof(Renderer),
        typeof(WWW),
        typeof(Mathf),
        typeof(System.Collections.Generic.List<int>),
        typeof(Action<string>),
        typeof(UnityEngine.Debug),
        typeof(WaitForSeconds),
        typeof(System.Collections.IEnumerator),
    };

    //C#静态调用Lua的配置（包括事件的原型），仅可以配delegate，interface
    [CSharpCallLua]
    public static List<Type> CSharpCallLua = new List<Type>()
    {
        typeof(Action),
        typeof(Func<double, double, double>),
        typeof(Action<string>),
        typeof(Action<double>),
        typeof(UnityEngine.Events.UnityAction),
        typeof(System.Collections.IEnumerator)
    };

    //黑名单
    [BlackList]
    public static List<List<string>> BlackList = new List<List<string>>()
    {
        new List<string>() {"System.Xml.XmlNodeList", "ItemOf"},
        new List<string>() {"UnityEngine.WWW", "movie"},
#if UNITY_WEBGL
                new List<string>(){"UnityEngine.WWW", "threadPriority"},
    #endif
        new List<string>() {"UnityEngine.Texture2D", "alphaIsTransparency"},
        new List<string>() {"UnityEngine.Security", "GetChainOfTrustValue"},
        new List<string>() {"UnityEngine.CanvasRenderer", "onRequestRebuild"},
        new List<string>() {"UnityEngine.Light", "areaSize"},
        new List<string>() {"UnityEngine.Light", "lightmapBakeType"},
        new List<string>() {"UnityEngine.WWW", "MovieTexture"},
        new List<string>() {"UnityEngine.WWW", "GetMovieTexture"},
        new List<string>() {"UnityEngine.AnimatorOverrideController", "PerformOverrideClipListCleanup"},
#if !UNITY_WEBPLAYER
        new List<string>() {"UnityEngine.Application", "ExternalEval"},
#endif
        new List<string>() {"UnityEngine.GameObject", "networkView"}, //4.6.2 not support
        new List<string>() {"UnityEngine.Component", "networkView"}, //4.6.2 not support
        new List<string>() {"System.IO.FileInfo", "GetAccessControl", "System.Security.AccessControl.AccessControlSections"},
        new List<string>() {"System.IO.FileInfo", "SetAccessControl", "System.Security.AccessControl.FileSecurity"},
        new List<string>() {"System.IO.DirectoryInfo", "GetAccessControl", "System.Security.AccessControl.AccessControlSections"},
        new List<string>() {"System.IO.DirectoryInfo", "SetAccessControl", "System.Security.AccessControl.DirectorySecurity"},
        new List<string>() {"System.IO.DirectoryInfo", "CreateSubdirectory", "System.String", "System.Security.AccessControl.DirectorySecurity"},
        new List<string>() {"System.IO.DirectoryInfo", "Create", "System.Security.AccessControl.DirectorySecurity"},
        new List<string>() {"UnityEngine.MonoBehaviour", "runInEditMode"},
    };
}

```

```MainView.cs
[SerializeField]
private Button _button; 


public IEnumerator WaitClicked()
{
    yield return _button.OnClickAsync().ToCoroutine(); // OnClickAsync()はUniTaskの機能です
}

```
_buttonがないと思うので、以下の画像のようなヒエラルキー構造にしbuttonを追加してください。
大きさは画面全体のストレッチでColorのaを0に設定し透明な状態にします。
![スクリーンショット 2021-04-28 11.48.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376109/a6a1c79b-098f-2bfa-e798-aacc48eaca52.png)
lua側でWaitClickedを扱えるようにlib.lua.txtに定義します

```lib.lua.txt
function wait_clicked()
    command.WaitClicked()
end

```

実際に再生したいスクリプトで使用してみます

```sample.lua.txt
text("表示するテキスト1")
wait_clicked()
text("表示するテキスト2")
wait_clicked()
text("表示するテキスト3")
wait_clicked()
```

入力待ちができていればOKです🙆‍♂️

## まとめ

ここまでで基本的なコマンドの実装を行ってきました。
この流れで一番最初にgif画像で上げたような画面を作るには、他のコマンドの実装していくことになります。
ぜひ頑張ってみてください！