困った時の自分用メモ

読んだ本を考察してメモったり、自分でいじった物の感想をメモったりする場。週1更新を目指します。

Luaの話~4:アプリの振舞を変えるという思考2~

これから、数回の渡り、アプリ開発で得たLuaの知見を共有していく。
今回は、Luaその物の理解を深めるために、基本的な情報を共有していく。

この記事は、前回の続き、アプリの振舞を外部から変えるという設計の具体的な例を示す。

例えば、Unityでボタンが押された時に何かするといった処理を実装するとしたら、何も考えずにシンプルに実装すると、以下の感じになると思う。

using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.IO;


public class MainScene {
    public void Button1Down() {
        // ログを出す
        Debug.Log("OutputLog");
    }
    
    public void Button2Down() {
        // オブジェクトを作る
        GameObject newObj = GameObject.Instantiate();
        newObj.SetParent(this);
    }
    
    public void Button3Down() {
        // シーンを切り替える
        SceneManager.ChangeScene("nextSceneName");
    }
}

各ボタンオブジェクトのクリックイベントハンドラーに、
・Button1Down()
・Button2Down()
・Button3Down()

を、関連付けるやり方だ。

個人的には、これはこれで間違っていないと思っている。
MainSceneに関連する処理は、MainScene内で収めた方が、後から見る側はこのファイルを起点に調べていけるからだ。

ただし、今回の「アプリを更新せずに、アプリの振舞を変更する」という観点からすると、この設計ではダメだ。 ボタンの挙動を変えようとする度に、バイナリ更新が発生するからだ。

少なくとも、ボタンが押された時に行いたい挙動は、外部で変更できる物から取得出来る必要がある。

▼ボタンが押された時の挙動を、外部ファイルから読み込んでみる
以下は表になっているが、以下の構造を持つCSVファイルが存在すると思っていただきたい。

buttonName metaParam 内容
button1 OutputLog ボタン1はログを出す処理なので、metaParamは表示する文字列を表す
button2 CharacterPrefab ボタン2はオブジェクトを生成する処理なので、metaParamは読み込むオブジェクトパスを表す
button3 nextSceneName ボタン3はシーン変更をする処理なので、metaParamはシーン名を表す

このようなCSVファイルがあったとして、buttonNameをキーに渡すと、metaParamが取得できる機構が存在したとすると、以下のような実装が可能になる

using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.IO;


public class MainScene {
    public void Button1Down() {
        var CsvData = CsvDataManager.GetCsvData("button1");
        // ログを出す
        Debug.Log(CsvData.metaParam);
    }
    
    public void Button2Down() {
        var CsvData = CsvDataManager.GetCsvData("button2");
        GameObject rawObj = Resources.Load(CsvData.metaParam);
        // オブジェクトを作る
        GameObject newObj = GameObject.Instantiate(rawObj) as GameObject;
        newObj.SetParent(this);
    }
    
    public void Button3Down() {
        var CsvData = CsvDataManager.GetCsvData("button3");
        // シーンを切り替える
        SceneManager.ChangeScene(CsvData.metaParam);
    }
}

このように実装しておけば、外部データの設定内容によって、振舞を変える事が出来ていると言える。

ただ、多少なりゲーム開発をした事がある人ならわかると思うが、これでもまだ不十分だ。 この例だと、Button1はログしか出せない。Button1にログを出す以外の処理に変えたい時に、バイナリ更新が発生してしまう。

となると、もっと処理を最適化する必要が出てくる。

▼ボタンが押された時の挙動の指定も、外部ファイルから読み込んでみる
ボタンと処理が1対1の関係になっている間は、目標としている設計にはならない。
つまり、ボタンが押された時に行う処理も、外部データに設定する必要がある。

CSV側に、ボタンを押した際に実行する処理を判別できるパラメータ
「functionId」
を追加しておく。

buttonName metaParam functionId 内容
button1 OutputLog OUTPUT_LOG ボタン1はOUTPUT_LOGに該当する関数に、metaParamを渡して処理を行う
button2 CharacterPrefab CREATE_OBJECT ボタン2はCREATE_OBJECTに該当する関数に、metaParamを渡して処理を行う
button3 nextSceneName SCENE_CHANGE ボタン3はSCENE_CHANGEに該当する関数に、metaParamを渡して処理を行う

これらを踏まえて実装を行うと、以下のような感じになる。

// このスクリプトコンポーネントを、ボタンのオブジェクトに付けておく
using UnityEngine;
using System.Collections;

public class ButtonEventHandler : MonoBehaviour {
    public void OnClickButton() {
        string hierarchyName = gameObject.name;
        ButtonEventProcessManager.Instance.OnClickButton(hierarchyName);
    }
}
// ボタンイベントを一括して処理する
// このクラスはシングルトンで、グローバルで使える物だと思ってください
using UnityEngine;
using System.Collections;

public class ButtonEventProcessManager : Singleton<ButtonEventProcessManager> {
    public void OnClickButton(string buttonName) {
        var CsvData = CsvDataManager.GetCsvData(buttonName);
        string functionType = CsvData.functionType;
        if (functionType == "SHOW_LOG") {
            Debug.Log(CsvData.metaParam);
        } else if (functionType == "CREATE_OBJECT") {
            GameObject rawObj = Resources.Load(CsvData.metaParam);
            // オブジェクトを作る
            GameObject newObj = GameObject.Instantiate(rawObj) as GameObject;
            newObj.SetParent(this);
        } else if (functionType == "CHANGE_SCENE") {
            SceneManager.ChangeScene(CsvData.metaParam);
        }
    }
}

この実装であれば、
ボタンを増やす⇒ボタンを追加したプレハブを配布して、後から追加が可能
ボタンを押した時の処理⇒「バイナリにすでに実装されている内容だったら」、押した際の振舞をCSVの設定を参照しているので、自由に変更可能。

という実装が出来上がる。

前の記事で
「バイナリ側には「入力、更新、出力」で行いたい事を書いておく」
というのは、つまりこの外部から渡される情報によって処理したい事は、予め実装しておけば、 それだけ可能な振る舞いが増える、という意味だという事が理解してもらえたと思う。

逆に言えば、処理したい事が実装されていなければ、バイナリ更新が必要になるという事だ。

この実装の考え方は、Unityをスクリプトエンジン化してしまうという事だ。 つまり、ゲームの処理その物はCSVで完結してしまえばいいという発想が基となる。

さて、概念の話はこの説明で終わりにする。
この記事で、Luaを使わなくてもアプリの振舞を変えることは容易に行う事が出来ると理解してもらえたと思う。
では、何故Luaを使うとそれが実現しやすくなるのか、というあたりと次回以降から説明していこうと思う。