Unity

【VariableDebugger】ゲームプレイ中にゲームバランスを調整できるアセット「変数デバッガ」

今回はアセットというよりゲーム開発におけるバランス調整のお話です。
スマホゲームをずっと作ってますが、ゲームバランスを整え、いざ実機でプレイしてみた時

てんぷら
てんぷら
あれ?なんか思ってたんと違う・・・

そんな経験ありませんか?そんな時に役立つ

【VariableDebugger】変数デバッガ

の紹介です!

VariableDebuggerとは

ゲームデータをゲームプレイ中に確認したり変更したり出来るアセットです。

public class GameData : ScriptableObject
{
    public float CameraSpeed = 0.01f;
    public float WalkSpeed   = 0.1f;
    public float RunSpeed    = 0.2f;
    public float JumpTime    = 0.6f;
    public float JumpSpeed   = 0.2f;
    public float JumpHeight  = 2;
}

例えばこういうゲームデータがあった場合、スマホアプリだと値を調整する度にアプリを作り直す必要があります。VariableDebuggerを使うとゲームプレイ中にその場で値を変更できるようになります。

企画
企画
てんぷらさん、このドロップ操作時間、ちょっと短いんで調整してもらえる?
てんぷら
てんぷら
あ、それ、右上のボタン押すと変数デバッガが起動するんでそれで調整してみてください

という感じで企画さんが実機でゲームバランスを調整できるようになります☆

余談ですが、いい感じのゲームバランスを作るのってとても大変です。
理論上問題ないはずなのに実際実機で触ってみたらイメージと違ったり・・・
特にスマホだと触り心地に関わるバランスはめちゃくちゃ重要なので、そういったものは実機で確認しながら調整するといいです。

理論に基づいた数値だけでなく、実際に遊んでみた「なんとなく」って大事です。

動作環境

Unity2021.2.5+
iPhone SE (第2世代) で動作確認済み
Xperia Z3 Compact SO-02G で動作確認済み

ライセンス


本アセットではユニティちゃんライセンス条項の元に提供されています。

使用方法

アセットをインポート

直接ファイルをインポートしてもいいし、VariableDebugger.unitypackageを使ってもOK。
ルートディレクトリは以下のようになってます。

  • GodController:マルチタッチアセット(デモ用)
  • UCL2.0:ユニティちゃんライセンス関連ファイル(デモ用)
  • UnityChan:ユニティちゃんアセット(デモ用)
  • VariableDebugger:アセット本体
  • VariableDebuggerDemo:デモ

VariableDebuggerディレクトリ以外はデモ用なので削除しても大丈夫です(容量削減可能)。

VariableDebuggerを生成し、Initメソッドを呼ぶ

[SerializeField] GameData gameData;
    
var variableDebugger = Instantiate(Resources.Load<VariableDebugger>("Prefabs/VariableDebugger"));
variableDebugger.Init(gameData);

public class GameData : ScriptableObject
{
    public float CameraSpeed = 0.01f;
    public float WalkSpeed   = 0.1f;
    public float RunSpeed    = 0.2f;
    public float JumpTime    = 0.6f;
    public float JumpSpeed   = 0.2f;
    public float JumpHeight  = 2;
}

スクリプトから生成してもいいですし、直接Hierarchyに置いてもOK。
Init引数には操作対象のゲームデータを渡します(ScriptableObject以外でもたぶん大丈夫)。

右上のGameDataボタンで変数デバッガを起動

イベント

以下のイベントを登録可能です。

variableDebugger.OnOpen.AddListener(OnOpen);
variableDebugger.OnClose.AddListener(OnClose);

void OnOpen()  { }
void OnClose() { }

ゲームデータで使用可能な型

指定方法
配列通常:値1, 値2
構造体:(値1, 値2), (値3, 値4)
enumIdle enum名
ScriptableObject  空文字(型名で決まるため)
ScriptableObjects1  ScriptableObjectのキー(1列目の値)
sbyte1
byte1
short1
ushort1
int1
uint1
long1
ulong1
chara
float1.0
double1.0
booltrue 1
G2U4S.xlsx/G2U4SConst/BoolTrueValues参考
stringM4u
SerializableDateTime2019-03-22 00:51
Vector21, 1
カンマ指定の数値は後ろを省略すると0で初期化される
Vector31, 1, 1
Vector41, 1, 1, 1
Rect1, 1, 1, 1
Vector2Int1, 1
Vector3Int1, 1, 1
RectInt1, 1, 1, 1
Quaternion1, 1, 1, 1
Color1, 1, 1, 1
Color32255, 255, 255, 255

値のパースには自作アセット「G2U4S」のコードを流用しています。
なので使用可能な型はG2U4Sと一緒です。

〝 G2U4S 〟G2Uで読み込んだゲームデータを1クリックでScriptableObjectに変換日本語 / English G2U4Sは、SpreadsheetやExcelで作成したゲームデータを1クリックでScr...

デモ

ゲームデータの変更

こんな感じでスマホでゲームデータを変更できます。
ユニティちゃんの操作方法は以下をご覧ください(GodControllerのデモを流用してます)。

【GodController】スマホのマルチタッチに対応したバーチャルコントローラーを作ろう!(Unityエディタでも確認可能) https://github.com/okamura0510/GodController めっちゃ久しぶりのブログですw ...

ゲームデータの保存

Unityの場合、ScriptableObjectならそこに変更結果が保存されます。

スマホの場合、通常はアプリを再起動すると変更した値はリセットされます。
アプリを再起動しても値を保持したい場合は

Demo.cs

variableDebugger.OnClose.AddListener(OnVariableDebuggerClose);

void OnVariableDebuggerClose()
{
    saveData.Save();
}

SaveData.cs

public void Load()
{
    if(Application.isEditor) return; // ScriptableObjectはエディタ上で確認出来るのでセーブ不要

    if(File.Exists(saveDataPath))
    {
        var json = File.ReadAllText(saveDataPath, Encoding.UTF8);
        JsonUtility.FromJsonOverwrite(json, gameData);
    }
    else
    {
        Save();
    }
}

public void Save()
{
    if(Application.isEditor) return; // ScriptableObjectはエディタ上で確認出来るのでセーブ不要

    var json = JsonUtility.ToJson(gameData);
    File.WriteAllText(saveDataPath, json, Encoding.UTF8);
}

こんな感じで端末に保存するといいです(デモは端末に保存するよう組まれています)。
ただセキュリティとかちゃんと考えるなら

このアセットとか使ってちゃんと暗号化した方がいいです。
(開発中は手を抜いちゃうけどリリース時は暗号化した方がベター)

VariableDebugger.cs

public class VariableDebugger : MonoBehaviour
{
    const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;

    [SerializeField] ScrollRect scrollView;
    [SerializeField] UnityEvent onOpen;
    [SerializeField] UnityEvent onClose;
    object gameData;
    Dictionary<FieldInfo, InputField> variables = new ();

    public bool IsShowing     => scrollView.gameObject.activeSelf;
    public UnityEvent OnOpen  => onOpen;
    public UnityEvent OnClose => onClose;
        
    public void Init(object gameData)
    {
        name          = "VariableDebugger";
        this.gameData = gameData;
            
        var fields = this.gameData.GetType().GetFields(Flags);
        foreach(var field in fields)
        {
            var fieldName  = field.Name;
            var go         = Instantiate(Resources.Load<GameObject>("Prefabs/Variable"), scrollView.content);
            var text       = go.GetComponentInChildren<Text>();
            var inputField = go.GetComponentInChildren<InputField>();
            go.name        = fieldName;
            text.text      = fieldName[0].ToString().ToUpper() + fieldName.Substring(1); // パスカルケース(見た目分かりやすいように)
            variables.Add(field, inputField);
            go.SetActive(true);
        }
    }
        
    public void Open()
    {
        onOpen?.Invoke();
            
        foreach(var variable in variables)
        {
            var field       = variable.Key;
            var inputField  = variable.Value;
            var value       = field.GetValue(gameData);
            inputField.text = G2U4SUtil.ParseString(value);
        }

        scrollView.gameObject.SetActive(true);
    }

    public void Close()
    {
        foreach(var variable in variables)
        {
            var field      = variable.Key;
            var inputField = variable.Value;
            var type       = field.FieldType;
            var value      = inputField.text;
            var setValue   = G2U4SUtil.Parse(type, value);
            field.SetValue(gameData, setValue);
        }

        scrollView.gameObject.SetActive(false);
        onClose?.Invoke();
    }
}

リフレクションでゲームデータの変数を取得してその分だけInputFieldを生成。
値のパースには以前作った「G2U4S」のコードを使用しています。

コンポーネント指向の弊害?

Demo.cs

void OnVariableDebuggerClose()
{
    saveData.Save();
    StartCoroutine(ReleaseTouch());
}

IEnumerator ReleaseTouch()
{
    yield return null;
    canTouch = true;
}

変数デバッガを閉じた時、yield return null; と1フレ待たないと、GodControllerUpdateが不正に処理されて攻撃モーションが誤って呼ばれていました。

これ、コンポーネント指向あるあるで、コンポーネント指向だとコンポーネント間の繋がりが薄いがためにそこが問題になりがちです。
ホントなら VariableDebugger.CloseGodController.Update の順で制御すべきなんですけど、そうするとそこで結合が生まれちゃう。だから今回は1フレ待つことでコンポーネントを独立させてます。

コンポーネント指向が好きなんですが、ゲームだと一連の流れで処理する機会が多くて難しいですね〜。スキルハンターの制約みたいな感じ。

Unityでのコンポーネント指向のあれこれここ最近コンポーネント指向にハマってたことがあって、あれこれいろいろ試してた。 そこらへんをまとめておく。 己の肉体と技術に...

最後に

自分が初めて作ったゲームで変数デバッガが使用されていました(もう10年前)。
それから形は違えどなんだかんだずっと使い続けてきていてます。
変数デバッガの生みの親、ホント、優秀な方でした。

生みの親
生みの親
死んでないぞ
【エビでもわかる】オセロプログラミング
〜オセロを作りながらゲームのプログラムを学ぼう〜
「Unityで初めてゲームを作ってみたい!」

そんな人のためにこの本を作りました。
オセロを一から作りながら実践形式でプログラムを学べる本です。
すでに完成したプロジェクトを説明するのではなく、実際に作りながら説明していきます。
一緒に手を動かしながら、プログラムを覚えていきましょう🌟