今回はアセットというよりゲーム開発におけるバランス調整のお話です。
スマホゲームをずっと作ってますが、ゲームバランスを整え、いざ実機でプレイしてみた時
そんな経験ありませんか?そんな時に役立つ
の紹介です!
目次
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) |
enum | Idle enum名 |
ScriptableObject | 空文字(型名で決まるため) |
ScriptableObjects | 1 ScriptableObjectのキー(1列目の値) |
sbyte | 1 |
byte | 1 |
short | 1 |
ushort | 1 |
int | 1 |
uint | 1 |
long | 1 |
ulong | 1 |
char | a |
float | 1.0 |
double | 1.0 |
bool | true 1 ○ G2U4S.xlsx/G2U4SConst/BoolTrueValues 参考 |
string | M4u |
SerializableDateTime | 2019-03-22 00:51 |
Vector2 | 1, 1 カンマ指定の数値は後ろを省略すると 0 で初期化される |
Vector3 | 1, 1, 1 |
Vector4 | 1, 1, 1, 1 |
Rect | 1, 1, 1, 1 |
Vector2Int | 1, 1 |
Vector3Int | 1, 1, 1 |
RectInt | 1, 1, 1, 1 |
Quaternion | 1, 1, 1, 1 |
Color | 1, 1, 1, 1 |
Color32 | 255, 255, 255, 255 |
値のパースには自作アセット「G2U4S」のコードを流用しています。
なので使用可能な型はG2U4Sと一緒です。
デモ
ゲームデータの変更
こんな感じでスマホでゲームデータを変更できます。
ユニティちゃんの操作方法は以下をご覧ください(GodController
のデモを流用してます)。
ゲームデータの保存
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フレ待たないと、GodController
のUpdate
が不正に処理されて攻撃モーションが誤って呼ばれていました。
これ、コンポーネント指向あるあるで、コンポーネント指向だとコンポーネント間の繋がりが薄いがためにそこが問題になりがちです。
ホントなら VariableDebugger.Close
→ GodController.Update
の順で制御すべきなんですけど、そうするとそこで結合が生まれちゃう。だから今回は1フレ待つことでコンポーネントを独立させてます。
コンポーネント指向が好きなんですが、ゲームだと一連の流れで処理する機会が多くて難しいですね〜。スキルハンターの制約みたいな感じ。
最後に
自分が初めて作ったゲームで変数デバッガが使用されていました(もう10年前)。
それから形は違えどなんだかんだずっと使い続けてきていてます。
変数デバッガの生みの親、ホント、優秀な方でした。
〜オセロを作りながらゲームのプログラムを学ぼう〜