Unityでキーコンフィグ

今回はUnityでキーコンフィグ作ることになったのでそれについて書きます。

Unityの場合、作ったゲームの起動時にダイアログが表示されて、そこでキーコンフィグを設定できますね。
しかし、今回の場合はゲーム内でキーコンフィグを設定できるようにするのが目標です。

## キーコンフィグの説明というかなんというか

必要ないかもしれないんですが、後々の説明で頭がこんがらがらないようキーコンフィグについてちょっと説明します。

ゲームをコントローラーやらキーボードでやっている時

* ◯キー(ボタン)で攻撃
* ✕キー(ボタン)で防御

とかありますよね。でも、ちょっとボタンの配置とかの関係で

* ✕キー(ボタン)で攻撃
* ◯キー(ボタン)で防御

の方がやりやすい！とかって場合に役立つのがキーコンフィグです。

たとえば、攻撃を別のキーで行いたい場合はキーコンフィグで攻撃に対して割り当てるキーを変更してやればいいわけです。
この設定を外部ファイルに保存しておけば、もう一度ゲームを遊ぶときに変更されたキーコンフィグで遊ぶことが出来ます。


## 注意

今回取り上げるキーコンフィグは、筆者のゲームでしか使えない形になっている部分があります。
筆者のゲームプロジェクトからスクリプトを一部を変更して無理矢理引っ張ってきているためです。
なので、コードをそのままパクっても動かないかもしれません。

実装方法を見ていただければ、必要な部分を変更して使っていただけるかもしれませんが、例外処理をしていなかったり、結構突貫作業なところがあるため、暇な時にでもきちんと整理して配布できる形にしてどこかで公開しようと思っています。

なので、今回の記事ではコードを紹介しつつ、どのようなロジックでキーコンフィグを実装したかを紹介するのみに留めます。

予めご容赦ください。



## 今回作成したキーコンフィグで出来ること

* 軸入力以外のキー設定を変更できるようにした



## 実装

注意にも書きましたが、こちらで紹介させていただくコードは一部改変していたり、そのままコピペしても動かなかったりしますのでご了承ください。

では早速見ていきます。

```csharp:KeyConfig.cs
using LitJson;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;

/// <summary>
/// キーコンフィグ情報を取り扱う
/// </summary>
public class KeyConfig
{
	private Dictionary<string, List<KeyCode>> config = new Dictionary<string, List<KeyCode>>();
	private readonly string configFilePath;

	/// <summary>
	/// キーコンフィグを管理するクラスを生成する
	/// </summary>
	/// <param name="configFilePath">コンフィグファイルのパス</param>
	public KeyConfig(string configFilePath)
	{
		this.configFilePath = configFilePath;
	}

	/// <summary>
	/// 指定したキーの入力状態をチェックする
	/// </summary>
	/// <param name="keyName">キーを示す名前文字列</param>
	/// <param name="predicate">キーが入力されているかを判定する述語</param>
	/// <returns>入力状態</returns>
	private bool InputKeyCheck(string keyName, Func<KeyCode, bool> predicate)
	{
		bool ret = false;
		foreach(var keyCode in config[keyName])
			if(predicate(keyCode))
				return true;
		return ret;
	}

	/// <summary>
	/// 指定したキーが押下状態かどうかを返す
	/// </summary>
	/// <param name="keyName">キーを示す名前文字列</param>
	/// <returns>入力状態</returns>
	public bool GetKey(string keyName)
	{
		return InputKeyCheck(keyName, Input.GetKey);
	}

	/// <summary>
	/// 指定したキーが入力されたかどうかを返す
	/// </summary>
	/// <param name="keyName">キーを示す名前文字列</param>
	/// <returns>入力状態</returns>
	public bool GetKeyDown(string keyName)
	{
		return InputKeyCheck(keyName, Input.GetKeyDown);
	}

	/// <summary>
	/// 指定したキーが離されたかどうかを返す
	/// </summary>
	/// <param name="keyName">キーを示す名前文字列</param>
	/// <returns>入力状態</returns>
	public bool GetKeyUp(string keyName)
	{
		return InputKeyCheck(keyName, Input.GetKeyUp);
	}

	/// <summary>
	/// 指定されたキーに割り付けられているキーコードを返す
	/// </summary>
	/// <param name="keyName">キーを示す名前文字列</param>
	/// <returns>キーコード</returns>
	public List<KeyCode> GetKeyCode(string keyName)
	{
		if(config.ContainsKey(keyName))
			return new List<KeyCode>(config[keyName]);
		return new List<KeyCode>();
	}

	/// <summary>
	/// 名前文字列に対するキーコードを設定する
	/// </summary>
	/// <param name="keyName">キーに割り付ける名前</param>
	/// <param name="keyCode">キーコード</param>
	/// <returns>キーコードの設定が正常に完了したかどうか</returns>
	public bool SetKey(string keyName, List<KeyCode> keyCode)
	{
		if(string.IsNullOrEmpty(keyName) || keyCode.Count < 1)
			return false;
		config[keyName] = keyCode;
		return true;
	}

	/// <summary>
	/// コンフィグからキーコードを削除する
	/// </summary>
	/// <param name="keyName">キーに割り付けられている名前</param>
	/// <returns></returns>
	public bool RemoveKey(string keyName)
	{
		return config.Remove(keyName);
	}

	/// <summary>
	/// 設定されているキーコンフィグを確認用文字列として受け取る
	/// </summary>
	/// <returns>キーコンフィグを表す文字列</returns>
	public string CheckConfig()
	{
		StringBuilder sb = new StringBuilder();
		foreach(var keyValuePair in config)
		{
			sb.AppendLine("Key : " + keyValuePair.Key);
			foreach(var value in keyValuePair.Value)
				sb.AppendLine("  |_ Value : " + value);
		}
		return sb.ToString();
	}

	/// <summary>
	/// ファイルからキーコンフィグファイルをロードする
	/// </summary>
	public void LoadConfigFile()
	{
		//TODO:復号処理
		using(TextReader tr = new StreamReader(configFilePath, Encoding.UTF8))
			config = JsonMapper.ToObject<Dictionary<string, List<KeyCode>>>(tr);
	}

	/// <summary>
	/// 現在のキーコンフィグをファイルにセーブする
	/// </summary>
	public void SaveConfigFile()
	{
		//TODO:暗号化処理
		var jsonText = JsonMapper.ToJson(config);
		using(TextWriter tw = new StreamWriter(configFilePath, false, Encoding.UTF8))
			tw.Write(jsonText);
	}
}
```


キーコンフィグファイルの保存・ロードには[LitJson](https://github.com/lbv/litjson)を用いています。コンフィグファイルの暗号化、復号処理、例外処理等は記述していません。

キーコンフィグの実体はDictionaryで、Keyに文字列、ValueにKeyCode列挙体のリストを持っています。Keyにはそれぞれゲーム内で利用するキーの別名(攻撃ボタン..等)を、Valueには実際に入力させるキー(AボタンとBボタン同時押し..等)を格納していきます。

基本的にやっていることは、KeyConfigの実体であるconfig変数をいじっているだけです。


では続いてこのKeyConfigを利用し、ゲームでの入力を管理するクラスを見ていきます。

```csharp:InputManager.cs
using MBLDefine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;

/// <summary>
/// 入力を管理する
/// </summary>
internal class InputManager : SingletonMonoBehaviour<InputManager>
{
	#region InnerClass

	/// <summary>
	/// キーコンフィグ設定の変更や保存・ロードを管理する
	/// </summary>
	internal class KeyConfigSetting
	{
		private Array keyCodeValues;
		private static InputManager inputManager;
		private static KeyConfigSetting instance;

		public static KeyConfigSetting Instance
		{
			get
			{
				if(inputManager == null)
					inputManager = InputManager.Instance;
				return instance = instance != null ? instance : new KeyConfigSetting();
			}
		}

		private KeyConfigSetting()
		{
		}

		/// <summary>
		/// 呼び出されたフレームで押下状態のKeyCodeリストを返す
		/// </summary>
		/// <returns>押下状態のKeyCodeリスト</returns>
		private List<KeyCode> GetCurrentInputKeyCode()
		{
			List<KeyCode> ret = new List<KeyCode>();
			if(keyCodeValues == null)
				keyCodeValues = Enum.GetValues(typeof(KeyCode));
			foreach(var code in keyCodeValues)
				if(Input.GetKey((KeyCode)(int)code))
					ret.Add((KeyCode)(int)code);
			return ret;
		}

		/// <summary>
		/// コンフィグにキーをセットする
		/// </summary>
		/// <param name="key">キーを表す識別子</param>
		/// <param name="keyCode">割り当てるキーコード</param>
		/// <returns>割り当てが正常に終了したかどうか</returns>
		public bool SetKey(Key key, List<KeyCode> keyCode)
		{
			return inputManager.keyConfig.SetKey(key.String, keyCode);
		}

		/// <summary>
		/// コンフィグから値を消去
		/// </summary>
		/// <param name="key">キーを表す識別子</param>
		/// <returns>値の消去が正常に終了したかどうか</returns>
		public bool RemoveKey(Key key)
		{
			return inputManager.keyConfig.RemoveKey(key.String);
		}

		/// <summary>
		/// 押されているキーを名前文字列に対するキーとして設定する
		/// </summary>
		/// <param name="key">キーに割り付ける名前</param>
		/// <returns>キーコードの設定が正常に完了したかどうか</returns>
		public bool SetCurrentKey(Key key)
		{
			//HACK:マウス入力も受け付けるようにするべきなので今後改善
			//マウス{0~6}の入力を弾く
			var currentInput = GetCurrentInputKeyCode().Where(c => c < KeyCode.Mouse0 || KeyCode.Mouse6 < c).ToList();

			if(currentInput == null || currentInput.Count < 1)
				return false;
			var code = inputManager.keyConfig.GetKeyCode(key.String);
			//既に設定されているキーと一部でも同じキーが押されている場合
			if(code.Count > currentInput.Count && currentInput.All(k => code.Contains(k)))
				return false;
			RemoveKey(key);
			return SetKey(key, currentInput);
		}

		/// <summary>
		/// デフォルトのキー設定を適用する
		/// </summary>
		public void SetDefaultKeyConfig()
		{
			foreach(var key in Key.AllKeyData)
				SetKey(key, key.DefaultKeyCode);
		}

		public List<KeyCode> GetKeyCode(Key keyName)
		{
			return inputManager.keyConfig.GetKeyCode(keyName.String);
		}

		public void LoadSetting()
		{
			InputManager.Instance.keyConfig.LoadConfigFile();
		}

		public void SaveSetting()
		{
			InputManager.Instance.keyConfig.SaveConfigFile();
		}
	}

	#endregion InnerClass

	/// <summary>
	/// 使用するキーコンフィグ
	/// </summary>
	private KeyConfig keyConfig = new KeyConfig(ExternalFilePath.KEYCONFIG_PATH);

	public void Awake()
	{
		Debug.Log("Load key-config file.");

		//最初はデフォルトの設定をコンフィグに格納
		KeyConfigSetting.Instance.SetDefaultKeyConfig();

		//コンフィグファイルがあれば読み出す
		try
		{
			InputManager.Instance.keyConfig.LoadConfigFile();
		}
		catch(IOException e)
		{
			Debug.Log(e.Message);
		}
	}

	/// <summary>
	/// 指定したキーが押下状態かどうかを返す
	/// </summary>
	/// <returns>入力状態</returns>
	internal bool GetKey(Key key)
	{
		return keyConfig.GetKey(key.String);
	}

	/// <summary>
	/// 指定したキーが入力されたかどうかを返す
	/// </summary>
	/// <returns>入力状態</returns>
	internal bool GetKeyDown(Key key)
	{
		return keyConfig.GetKeyDown(key.String);
	}

	/// <summary>
	/// 指定したキーが離されたかどうかを返す
	/// </summary>
	/// <returns>入力状態</returns>
	internal bool GetKeyUp(Key key)
	{
		return keyConfig.GetKeyUp(key.String);
	}

	/// <summary>
	/// 軸入力に対する値を返す
	/// </summary>
	/// <returns>入力値</returns>
	internal float GetAxes(Axes axes)
	{
		return Input.GetAxis(axes.String);
	}

	/// <summary>
	/// 軸入力に対する値を返す
	/// </summary>
	/// <returns>平滑化フィルターが適用されていない入力値</returns>
	internal float GetAxesRaw(Axes axes)
	{
		return Input.GetAxisRaw(axes.String);
	}
}
```

SingletonMonoBehaviourは、コンポーネントがシーン内で唯一のものになるようなロジックを持つMonoBehaviourの派生クラスです。今回はこのクラスの実装等は省かせてもらいますが、検索すると色々な方が実装していらっしゃるので参考にしてください(そのうち筆者の実装パターンを公開するかもです)。

InputManagerはKeyConfigSettingクラスを内包しており、このクラスでキーコンフィグにキーを設定したり、設定を読みだしたりできるようになっています。独自のキーコンフィグメニュー等を作る際は、このクラスのメソッドのお世話になればどうにかなります。

InputManagerは、UnityのInputクラスで行えるGet◯◯のような動作を行うメソッドを実装しています。これらのメソッドがパラメータに利用しているKeyやAxesといった型は、MBLDefineという名前空間上に定義したもので、これには筆者のゲーム特有のデータが含まれています。MBLDefineは以下のような感じです。

```csharp:MBLDefine.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;

[assembly: InternalsVisibleTo("UnitTest")]

/// <summary>
/// このゲームで定義する定数などを扱う
/// </summary>
namespace MBLDefine
{
	/// <summary>
	/// 外部ファイルへの参照に必要なパス群
	/// </summary>
	internal struct ExternalFilePath
	{
		internal const string KEYCONFIG_PATH = "keyconf.dat";
	}

	/// <summary>
	/// 入力値の基底クラス
	/// </summary>
	internal class InputValue
	{
		public readonly string String;

		protected InputValue(string name)
		{
			String = name;
		}
	}

	/// <summary>
	/// 使用するキーを表すクラス
	/// </summary>
	internal sealed class Key : InputValue
	{
		public readonly List<KeyCode> DefaultKeyCode;
		public readonly static List<Key> AllKeyData = new List<Key>();

		private Key(string keyName, List<KeyCode> defaultKeyCode)
			: base(keyName)
		{
			DefaultKeyCode = defaultKeyCode;
			AllKeyData.Add(this);
		}

		public override string ToString()
		{
			return String;
		}

		public static readonly Key Action = new Key("Action", new List<KeyCode> { KeyCode.Z });
		public static readonly Key Jump = new Key("Jump", new List<KeyCode> { KeyCode.Space });
		public static readonly Key Balloon = new Key("Balloon", new List<KeyCode> { KeyCode.X });
		public static readonly Key Squat = new Key("Squat", new List<KeyCode> { KeyCode.LeftShift });
		public static readonly Key Menu = new Key("Menu", new List<KeyCode> { KeyCode.Escape });
	}

	/// <summary>
	/// 使用する軸入力を表すクラス
	/// </summary>
	internal sealed class Axes : InputValue
	{
		public readonly static List<Axes> AllAxesData = new List<Axes>();

		private Axes(string axesName)
			: base(axesName)
		{
			AllAxesData.Add(this);
		}

		public override string ToString()
		{
			return String;
		}

		public static Axes Horizontal = new Axes("Horizontal");
		public static Axes Vertical = new Axes("Vertical");
	}
}
```


MBLDefineでは、使用するキーに名前をつけ、デフォルトで使用するキーのリストを持たせてあります。
これについては特筆するようなことは無いと思います。


ここまでに紹介したものを使うことで、キーコンフィグを実装することが出来ます。


## まとめ

この記事で紹介したMBLDefineのKeyやAxesを変更して頂いたり、コンフィグファイルを格納するファイルパスを変更していただいたりすることで利用できるようになるかと思います。

使用方法の一例として、キー入力をイベントとして扱う例を最後に紹介します。

```csharp:InputEventManager.cs
using MBLDefine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

/// <summary>
/// 入力によるイベントを管理する
/// </summary>
public class InputEventManager : SingletonMonoBehaviour<InputEventManager>
{
	private Dictionary<Key, EventHandler> onKeyEvents = new Dictionary<Key, EventHandler>();
	private Dictionary<Key, EventHandler> onKeyDownEvents = new Dictionary<Key, EventHandler>();
	private Dictionary<Key, EventHandler> onKeyUpEvents = new Dictionary<Key, EventHandler>();
	private Dictionary<Key, EventHandler> onKeyNotPressedEvents = new Dictionary<Key, EventHandler>();
	private Dictionary<Axes, EventHandler> onAxesEvents = new Dictionary<Axes, EventHandler>();
	private Dictionary<Axes, EventHandler> onAxesRowEvents = new Dictionary<Axes, EventHandler>();

	/// <summary>
	/// キー入力イベントの実行を制御する
	/// </summary>
	public bool Execute { get; set; }

	public void Awake()
	{
		Execute = true;

		//キーの種類の数だけイベントを生成する
		foreach(Key key in Key.AllKeyData)
		{
			onKeyEvents.Add(key, (o, a) => { });
			onKeyDownEvents.Add(key, (o, a) => { });
			onKeyUpEvents.Add(key, (o, a) => { });
			onKeyNotPressedEvents.Add(key, (o, a) => { });
		}

		foreach(Axes axes in Axes.AllAxesData)
		{
			onAxesEvents.Add(axes, (o, a) => { });
			onAxesRowEvents.Add(axes, (o, a) => { });
		}
	}

	public void Update()
	{
		if(!Execute)
			return;

		//HACK:EventArgsを継承して様々なデータを受け渡せるように出来る

		KeyEventInvoke(InputManager.Instance.GetKey, onKeyEvents, new EventArgs());
		KeyEventInvoke(InputManager.Instance.GetKeyDown, onKeyDownEvents, new EventArgs());
		KeyEventInvoke(InputManager.Instance.GetKeyUp, onKeyUpEvents, new EventArgs());
		KeyEventInvoke((key) => { return !InputManager.Instance.GetKey(key); }, onKeyNotPressedEvents, new EventArgs());

		AxesEventInvoke(onAxesEvents, new EventArgs());
		AxesEventInvoke(onAxesRowEvents, new EventArgs());
	}

	/// <summary>
	/// キー入力イベントをセットする
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetKeyEvent(Key key, EventHandler eventHandler)
	{
		onKeyEvents[key] += eventHandler;
	}

	/// <summary>
	/// キー入力開始時イベントをセットする
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetKeyDownEvent(Key key, EventHandler eventHandler)
	{
		onKeyDownEvents[key] += eventHandler;
	}

	/// <summary>
	/// キー入力終了時イベントをセットする
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetKeyUpEvent(Key key, EventHandler eventHandler)
	{
		onKeyUpEvents[key] += eventHandler;
	}

	/// <summary>
	/// キーが押されていない場合のイベントをセットする
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetKeyNotPressedEvent(Key key, EventHandler eventHandler)
	{
		onKeyNotPressedEvents[key] += eventHandler;
	}

	/// <summary>
	/// 軸入力時イベントをセットする
	/// </summary>
	/// <param name="axes">軸の種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetAxesEvent(Axes axes, EventHandler eventHandler)
	{
		onAxesEvents[axes] += eventHandler;
	}

	/// <summary>
	/// 軸入力時イベントをセットする
	/// </summary>
	/// <param name="axes">軸の種類</param>
	/// <param name="eventHandler">実行するイベント</param>
	internal void SetAxesRowEvent(Axes axes, EventHandler eventHandler)
	{
		onAxesRowEvents[axes] += eventHandler;
	}

	/// <summary>
	/// キー入力イベントから指定したイベントを削除する
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveKeyEvent(Key key, EventHandler eventHandler)
	{
		onKeyEvents[key] -= eventHandler;
	}

	/// <summary>
	/// キー入力時イベントから指定したイベントを削除する
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveKeyDownEvent(Key key, EventHandler eventHandler)
	{
		onKeyDownEvents[key] -= eventHandler;
	}

	/// <summary>
	/// キー入力終了時イベントから指定したイベントを削除する
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveKeyUpEvent(Key key, EventHandler eventHandler)
	{
		onKeyUpEvents[key] -= eventHandler;
	}

	/// <summary>
	/// キーが入力されていない場合のイベントから指定したイベントを削除する
	/// </summary>
	/// <param name="key">キーの種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveKeyNotPressedEvent(Key key, EventHandler eventHandler)
	{
		onKeyNotPressedEvents[key] -= eventHandler;
	}

	/// <summary>
	/// 軸入力時イベントを削除する
	/// </summary>
	/// <param name="axes">軸の種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveAxesEvent(Axes axes, EventHandler eventHandler)
	{
		onAxesEvents[axes] -= eventHandler;
	}

	/// <summary>
	/// 軸入力時イベントを削除する
	/// </summary>
	/// <param name="axes">軸の種類</param>
	/// <param name="eventHandler">削除するイベント</param>
	internal void RemoveAxesRowEvent(Axes axes, EventHandler eventHandler)
	{
		onAxesRowEvents[axes] -= eventHandler;
	}

	/// <summary>
	/// キーイベントを実行する
	/// キーが入力されていない状態の時はイベントを実行しない
	/// </summary>
	/// <param name="keyEntryDecision">キー入力判定を行う述語</param>
	/// <param name="keyEvent">キーごとのイベントを格納するハッシュマップ</param>
	/// <param name="args">イベント実行に用いる引数</param>
	private void KeyEventInvoke(Func<Key, bool> keyEntryDecision, Dictionary<Key, EventHandler> keyEvent, EventArgs args)
	{
		foreach(Key key in Key.AllKeyData)
			if(keyEntryDecision(key))
				if(keyEvent[key] != null)
					keyEvent[key](this, args);
	}

	/// <summary>
	/// 軸イベントを実行する
	/// </summary>
	/// <param name="axesEntryDecision">軸入力値取得を行う述語</param>
	/// <param name="axesEvent">軸ごとのイベントを格納するハッシュマップ</param>
	/// <param name="args">イベント実行に用いる引数</param>
	private void AxesEventInvoke(Dictionary<Axes, EventHandler> axesEvent, EventArgs args)
	{
		foreach(Axes axes in Axes.AllAxesData)
			if(axesEvent[axes] != null)
				axesEvent[axes](this, args);
	}
}
```

このクラスによって、他のスクリプトでキーが入力された時のイベントハンドラーを実装し、イベント登録をすればキー入力に対する処理を実行できることになります。

長文失礼致しました。