유니티의 입력 시스템
- 유니티에는 2가지 Input System이 있다.
- 첫 번째는 Input.Get~ 꼴의 메소드를 이용해 이번 프레임에 해당 입력이 있었는지 검사하는 레거시 시스템
- 두 번째는 2019년 소개된 New Input System.
- New Input System을 사용하려면
- 패키지 매니저에서 Input System을 설치하고,
- 프로젝트 세팅에서 Active Input Handling을 지정하고,
- InputAction 윈도우에서 Action Map, Action, Property를 설정하고,
- New Input System을 사용할 게임오브젝트에 PlayerInput 컴포넌트를 넣고,
- 이제 스크립트에서 콜백 메소드를 작성해서 입력을 처리한다.
- 그리고 여기서 가장 중요한 런타임 바인딩 변경 기능.
- https://www.youtube.com/watch?v=dUCcZrPhwSo&ab_channel=DapperDino 여기에도 친절하게 설명되어 있다.
- 바인딩 JSON 세이브 로드 기능은 https://forum.unity.com/threads/how-to-save-input-action-bindings.799311/ 여기서 찾을 수 있었다.
1. 기능과 입력의 분리
- 기능 또는 사용자의 행동과 실제 입력값을 분리해야 한다.
- 예를 들어 기능은 MoveLeft, MoveRight, Jump 등이 있고, 실제 입력값은 Input.GetKeyDown()의 파라미터로 넣어주는 KeyCode.A, KeyCode.D, KeyCode.Space 등이 있다.
- 바인딩을 고려하지 않는다면 곧장 Input.GetKeyDown(KeyCode.A) 꼴로 사용하게 되는데, 입력값을 의미하는 KeyCode를 직접 사용하지 않고 기능과 관련된 코드를 사용하도록 만들어야 한다.
- 따라서 게임 내에서 사용할 기능들을 미리 정의한다.
- int, string으로 0, 1, 2 …, 또는 “MoveLeft”, “MoveRight”, … 꼴로 만들 수도 있지만,
- 정수형은 직관적이지 않고 string은 오탈자를 유발할 수 있으므로 enum으로 정의한다.
- 레거시 입력 시스템의 입력값은 키보드는 KeyCode, 마우스는 정수 값을 사용한다.
- 그런데 KeyCode에 마우스 입력값도 Mouse0 ~ Mouse6까지 포함되어 있으므로 KeyCode로 통합하여 사용할 수 있다.
- 사용자 행동(기능)에 대한 enum을 다음과 같이 정의한다.
public enum UserAction
{
MoveForward,
MoveBackward,
MoveLeft,
MoveRight,
Attack,
Run,
Jump,
// UI
UI_Inventory,
UI_Status,
UI_Skill,
}
2. 바인딩 구현
- 기능에 따라 KeyCode 값을 참조할 수 있도록 하려면 간단히 딕셔너리를 사용하면 된다.
private Dictionary<UserAction, KeyCode> _bindingDict;
3. 바인딩 클래스 작성
- 딕셔너리만을 사용해 곧장 바인딩 기능을 사용할 수 있으나, 모듈화하여 바인딩을 프리셋으로 사용하고 저장, 불러오기도 할 수 있게 할 것이므로 클래스로 묶어 작성한다.
[Serializable]
public class InputBinding
{
public Dictionary<UserAction, KeyCode> Bindings => _bindingDict;
private Dictionary<UserAction, KeyCode> _bindingDict;
// 생성자
public InputBinding(bool initalize = true)
{
_bindingDict = new Dictionary<UserAction, KeyCode>();
if (initalize)
{
ResetAll();
}
}
// 새로운 바인딩 적용
public void ApplyNewBindings(InputBinding newBinding)
{
_bindingDict = new Dictionary<UserAction, KeyCode>(newBinding._bindingDict);
}
// 바인딩 지정 메소드 : allowOverlap 매개변수를 통해 중복 바인딩 허용여부를 결정한다.
public void Bind(in UserAction action, in KeyCode code, bool allowOverlap = false)
{
if (!allowOverlap && _bindingDict.ContainsValue(code))
{
var copy = new Dictionary<UserAction, KeyCode>(_bindingDict);
foreach (var pair in copy)
{
if (pair.Value.Equals(code))
{
_bindingDict[pair.Key] = KeyCode.None;
}
}
}
_bindingDict[action] = code;
}
// 초기 바인딩셋 지정 메소드
public void ResetAll()
{
Bind(UserAction.Attack, KeyCode.Mouse0);
Bind(UserAction.MoveForward, KeyCode.W);
Bind(UserAction.MoveBackward, KeyCode.S);
Bind(UserAction.MoveLeft, KeyCode.A);
Bind(UserAction.MoveRight, KeyCode.D);
Bind(UserAction.Run, KeyCode.LeftControl);
Bind(UserAction.Jump, KeyCode.Space);
Bind(UserAction.UI_Inventory, KeyCode.I);
Bind(UserAction.UI_Status, KeyCode.P);
Bind(UserAction.UI_Skill, KeyCode.K);
}
}
- 클래스로 묶었으니, 각각의 바인딩 프리셋을 만들고 객체를 변경해가며 서로 다른 바인딩을 사용할 수 있다.
4. 직렬화 가능한 클래스 작성
- 중요한 문제점이 있는데, 일반적으로 딕셔너리는 직렬화가 안된다.
- 따라서 저장 및 불러오기 기능을 위해 직렬화 가능한 형태의 새로운 클래스를 작성한다.
- 간단히 KeyValuePair<>를 쓰려 했지만, KeyValuePair도 직렬화가 안되기 때문에 새로운 Pair 클래스도 작성한다.
[Serializable]
public class SerializableInputBinding
{
public BindPair[] bindPairs;
public SerializableInputBinding(InputBinding binding)
{
int len = binding.Bindings.Count;
int index = 0;
bindPairs = new BindPair[len];
foreach (var pair in binding.Bindings)
{
bindPairs[index++] =
new BindPair(pair.Key, pair.Value);
}
}
}
[Serializable]
public class BindPair
{
public UserAction key;
public KeyCode value;
public BindPair(UserAction key, KeyCode value)
{
this.key = key;
this.value = value;
}
}
5. 저장, 불러오기 구현
- 우선 SerializableInputBinding 클래스를 InputBinding 클래스에서 빠르게 변환하여 사용할 수 있도록 새로운 생성자와 메소드를 작성한다.
// 생성자
public InputBinding(SerializableInputBinding sib)
{
_bindingDict = new Dictionary<UserAction, KeyCode>();
foreach (var pair in sib.bindPairs)
{
_bindingDict[pair.key] = pair.value;
}
}
public void ApplyNewBindings(SerializableInputBinding newBinding)
{
_bindingDict.Clear();
foreach (var pair in newBinding.bindPairs)
{
_bindingDict[pair.key] = pair.value;
}
}
- 저장, 불러오기 메소드를 작성한다.
- 로컬 또는 서버 등 환경에 따라 구현할 수 있다.
- 여기서는 로컬로 구현하였으며, 구현부는 본문에서 생략하였다.
public void SaveToFile()
{
SerializableInputBinding sib = new SerializableInputBinding(this);
string jsonStr = JsonUtility.ToJson(sib);
LocalFileIOHandler.Save(jsonStr, filePath); // Save
}
public void LoadFromFile()
{
string jsonStr = LocalFileIOHandler.Load(filePath); // Load
if (jsonStr == null)
{
Debug.Log("File Load Error");
return;
}
var sib = JsonUtility.FromJson<SerializableInputBinding>(jsonStr);
ApplyNewBindings(sib);
}
구현 결과
- 바인딩 저장 파일 내용
- Input 클래스를 통한 키 입력
- 파일로부터 읽어들인 바인딩 설정에 따라 동일한 기능에 대해 입력받는 키값이 달라진다.
public InputBinding _binding = new InputBinding();
private void Start()
{
_binding.LoadFromFile();
}
private void Update()
{
if (Input.GetKeyDown(_binding.Bindings[UserAction.MoveLeft]))
{
LogBindingInfo(UserAction.MoveLeft);
}
if (Input.GetKeyDown(_binding.Bindings[UserAction.MoveRight]))
{
LogBindingInfo(UserAction.MoveRight);
}
if (Input.GetKeyDown(_binding.Bindings[UserAction.Attack]))
{
LogBindingInfo(UserAction.Attack);
}
}
private void LogBindingInfo(UserAction action)
{
Debug.Log($"Action : {action}, KeyCode : {_binding.Bindings[action]}");
}
- 추가 : UI를 통한 바인딩 변경 기능 구현
'게임엔진 > Unity' 카테고리의 다른 글
[Unity] 트랜스폼을 이용해서 AI 만들기 (0) | 2022.12.07 |
---|---|
[Unity] 키보드와 마우스 입력받기 (0) | 2022.12.04 |
[Unity] 프러스텀과 오클루전 컬링 (Frustum & Occlusion Culling) (0) | 2022.11.29 |
[Unity] 셰이더 Shader (0) | 2022.10.30 |
[Unity] DOTS란 무엇인가? Unity ECS 시스템 요약 (0) | 2022.10.26 |