오늘은 개인과제에 어제 강의에 나온 new InputSystem을 적용해서 플레이어 캐릭터의 이동을 구현하던 중에 강의에 나온 방법은 SendMessage로 호출을 하는데, 나는 SendMessage 방식은 조금 느릴 것 같아서 다른 방법으로 구현해봤다.
PlayerInput
에는 Behavior
라는 프로퍼티가 있다. 사용자가 바인딩 된 키를 입력하면 바인딩 된 키에 알맞는 메서드를 호출해야하는데, 호출방식을 어떻게 할지 정하는 프로퍼티이다.
SendMessage
와 BroadcastMessage
는 Object
클래스를 상속받는 클래스들에게 string 매개변수로 넘어간 메서드 이름과 같은 메서드가 있는지 탐색하고, 호출해주는 방식이다. 이 과정을 C#에선 리플렉션이라고 하고, 연산이 조금 걸린다.
InvokeUnityEvents
와 InvokeCSharpEvents
는 delegate
를 이용한 방식이다.
InvokeUnityEvents
를 이용하면 인스펙터 창에서 드래그앤드랍 딸깍딸깍으로 호출될 이벤트를 지정해 줄 수 있고,
InvokeCSharpEvents
를 이용하면 스크립트 상에서 코드로 호출될 이벤트를 구독시켜줘야한다. 귀찮은 만큼 이 녀석이 제일 빠르다.
PlayerInput
컴포넌트를 상속받아서 InvokeCSharpEvents
방식으로 호출해도 되겠지만, 이런 방식도 있다.
InputAction Asset에는 Generate C# Class라는 항목이 있다. 이걸 누르면 자동으로 다음과 같은 C# 스크립트가 생성된다.
나는 Input Action 파일명은 Player Input Action이고, Action Map의 이름은 PlayerContol이므로 인터페이스를 상속 받을 때 다음과 같이 상속 받으면 된다.
public class PlayerController : MonoBehaviour, PlayerInputAction.IPlayerControlActions
이 인터페이스를 상속받으면, 내가 ActionMap에 등록했던 액션들을 호출하기 위한 함수를 구현하라고 알려준다.
나는 이동하기 위한 Move 액션과 마우스를 바라보게 하기 위한 Look 액션을 만들어뒀는데, 그 둘을 호출하기 위한 OnMove 메서드와 OnLook 메서드를 구현하라고 알려주는 모습.
나는 Action 만들 때 Vector2를 받아오게 했는데 InputAction.CallbackContext
라는 뜬금없는 구조체를 매개변수로 넘겨준다고한다. 이 녀석은 ReadValue<T>()
메서드로 변환해서 쓰면 되겠다.
public event Action<Vector2> OnMoved;
public virtual void OnMove(InputAction.CallbackContext context)
{
// ReadValue로 InputActions asset에서 설정한 값을 읽어온다.
OnMoved?.Invoke(context.ReadValue<Vector2>());
}
인터페이스만 구현했다고 끝이 아니다. 아직 해야할 일이 몇 가지 남아있다.
PlayerInput
컴포넌트를 오브젝트에 추가하는 대신, Generate C# Class로 만든 클래스의 객체를 생성해줘야한다. PlayerInputAction inputAction;
protected virtual void Start()
{
//if (inputAction == null)
// inputAction = new();
inputAction ??= new();
// IPlayerControlActions를 상속받아 구현했으므로 this로 Callback을 설정할 수 있다.
inputAction.PlayerControl.SetCallbacks(this);
inputAction.Enable();
}
사용자 입력에 따른 이벤트 호출은 모두 구현했으니, 이제 알맞는 메서드들을 구독시켜주기만 하면 된다.
public class CharacterController : PlayerController
{
Rigidbody2D _rigidBody;
SpriteRenderer _spriteRenderer;
[SerializeField] float moveSpeed = 2f;
Vector2 moveDirection;
protected override void Start()
{
base.Start();
_rigidBody = GetComponent<Rigidbody2D>();
_spriteRenderer = transform.GetChild(0).GetComponent<SpriteRenderer>();
OnMoved += SetMoveDirection;
OnLooked += SetLookFlip;
}
void SetMoveDirection(Vector2 dir)
{
moveDirection = dir.normalized;
}
void SetLookFlip(Vector2 mouseWorldPosition)
{
var lookDir = (mouseWorldPosition - (Vector2)transform.position).normalized;
float angle = Mathf.Atan2(lookDir.y, lookDir.x) * Mathf.Rad2Deg;
if (Mathf.Abs(angle) > 90f)
_spriteRenderer.flipX = true;
else
_spriteRenderer.flipX = false;
}
}
나는 PlayerController를 상속받는 CharacterController라는 클래스를 만들어서 OnMoved에는 SetMoveDirection 메서드를, OnLooked에는 SetLookFlip 메서드를 구독시켜줬다.
PlayerInput
컴포넌트와 비교해서 장단점?둘의 장단점에 대해 토론하는 포럼 글이 있다. 시간 날 때 읽어보면 좋을 것 같다.
https://forum.unity.com/threads/input-system-generate-c-code-what-its-good-for.995674/#post-7073584
https://openmynotepad.tistory.com/76
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour, PlayerInputAction.IPlayerControlActions
{
PlayerInputAction inputAction;
public event Action<Vector2> OnMoved;
public event Action<Vector2> OnLooked;
public virtual void OnMove(InputAction.CallbackContext context)
{
// ReadValue로 InputActions asset에서 설정한 값을 읽어온다.
OnMoved?.Invoke(context.ReadValue<Vector2>());
}
public virtual void OnLook(InputAction.CallbackContext context)
{
// convert world position
var pos = Camera.main.ScreenToWorldPoint(context.ReadValue<Vector2>());
OnLooked?.Invoke(pos);
}
protected virtual void Start()
{
//if (inputAction == null)
// inputAction = new();
inputAction ??= new();
// IPlayerControlActions를 상속받아 구현했으므로 this로 Callback을 설정할 수 있다.
inputAction.PlayerControl.SetCallbacks(this);
inputAction.Enable();
}
}
using UnityEngine;
public class CharacterController : PlayerController
{
Rigidbody2D _rigidBody;
SpriteRenderer _spriteRenderer;
[SerializeField] float moveSpeed = 2f;
Vector2 moveDirection;
protected override void Start()
{
base.Start();
_rigidBody = GetComponent<Rigidbody2D>();
_spriteRenderer = transform.GetChild(0).GetComponent<SpriteRenderer>();
OnMoved += SetMoveDirection;
OnLooked += SetLookFlip;
}
void SetMoveDirection(Vector2 dir)
{
moveDirection = dir.normalized;
}
void SetLookFlip(Vector2 mouseWorldPosition)
{
var lookDir = (mouseWorldPosition - (Vector2)transform.position).normalized;
float angle = Mathf.Atan2(lookDir.y, lookDir.x) * Mathf.Rad2Deg;
if (Mathf.Abs(angle) > 90f)
_spriteRenderer.flipX = true;
else
_spriteRenderer.flipX = false;
}
void FixedUpdate()
{
Move(moveDirection);
}
public void Move(Vector2 moveDirection) => _rigidBody.MovePosition(_rigidBody.position + moveSpeed * Time.fixedDeltaTime * moveDirection);
}