오늘 배운 내용
-
유니티 Input System, 이동 구현
-
유니티 에임 시스템 구현, Atan2(), Mathf.Rad2Deg
-
이벤트와 델리게이트의 차이점이 정확히 뭘까?
유니티 Input System
드디어 유니티 학습 주차에 들어갔다, 강의의 내용은 간단한 TopDownShooting 게임을 만들어보며 실습을 진행했는데
이동부분을 구현할 떄 InputManager가 아닌 InputSystem을 사용하여 구현했다.
InputSystem은 Unity2020에 공식 패키지로 포함된 기능으로 이전의 InputManager보다
다양한 입력 장치들을 바인딩 시스템을 통해 쉽게 매핑하고 수정할 수 있어,
게임의 입력 처리를 훨씬 더 간단하고 효과적으로 만들어주는 시스템 이다.
먼저 TopDownCharacterController 클래스를 정의했다.
이 클래스는 다음으로 만들 PlayerInputController의 부모 클래스로
두개의 이벤트를 선언하고, 두 이벤트를 Invoke하는 메서드들이 구현되어 있다.
public class TopDownCharacterController : MonoBehaviour
{
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public void CallMoveEvent(Vector2 direction)
{
OnMoveEvent?.Invoke(direction);
}
public void CallLoockEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
}
그 다음 PlayerInputController 클래스는 플레이어 오브젝트가 가지고 있을 컴포넌트로 TopDownCharacterController를 상속받았다.
구현된 OnMove(), OnLock(),OnFire() 메서드들을 통해서 Input받은 Vector2 값으로 이벤트를 Invoke한다.
public class PlayerInputController : TopDownCharacterController
{
private Camera _camera;
private void Awake()
{
_camera = Camera.main;
}
public void OnMove(InputValue value)
{
Vector2 moveInput = value.Get<Vector2>().normalized;
CallMoveEvent(moveInput);
}
public void OnLook(InputValue value)
{
// 마우스의 현재위치를 월드좌표로 불러온다.
Vector2 newAim = value.Get<Vector2>();
Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);
// 현재 위치에서 마우스 위치로의 방향을 구한다.
newAim = (worldPos - (Vector2)transform.position).normalized;
if (newAim.magnitude >= .9f)
{
CallLoockEvent(newAim);
}
}
public void OnFire(InputValue value)
{
Debug.Log("OnFire" + value.ToString());
}
}
결국 PlayerInputController 클래스는 Vector2로 방향을 구하여 이벤트에 등록된 메서드들에 전달하는 역할을 한다.
다음으로 구현한 클래스는 TopDownMovement로 마찬가지로 플레이어 오브젝트가 가지며
이벤트에 Move()메서드를 연결하여 방향을 받아 저장하고,
FixedUpdate에서 ApplyMovement() 메서드를 실행하여 현재의 방향으로 speed만큼의 (현재 코드에서는 5로 고정) 값을
_rigidbody.velocity에 넣어서 이동을 구현했다.
public class TopDownMovement : MonoBehaviour
{
private TopDownCharacterController _controller;
private Vector2 _movementDirection = Vector2.zero;
private Rigidbody2D _rigidbody;
private void Awake()
{
_controller = GetComponent<TopDownCharacterController>();
_rigidbody = GetComponent<Rigidbody2D>();
}
private void Start()
{
_controller.OnMoveEvent += Move;
}
private void FixedUpdate()
{
ApplyMovement(_movementDirection);
}
private void Move(Vector2 direction)
{
_movementDirection = direction;
}
private void ApplyMovement(Vector2 direction)
{
direction *= 5;
_rigidbody.velocity = direction;
}
}
처음에 direction값에 *5를 FixedUpdate마다 반복하면 계속해서 direction이 커져서 안되는거 아닌가??
라는 생각을 했는데 코드르 잘 살펴보고 direction은 매개변수여서 항상 입력받은 방향 * speed 만큼의 일정한 값이
유지된다 라는것을 깨달을 수 있었다.
그리고 InputAction을 생성해서 키를 매핑하고 여기서 생성된 Player Input을 Player의 컴포넌트에 추가하는 것으로
입력받은 키를 통해 PlayerInputController클래스의 OnMove(), OnLook(), OnFire()메서드를 실행해주어
입력에 따른 이동이 구현되었다.
결국 InputSystem 란?
Action들을 생성하여 키를 호출하게되면 해당 Action들의 On액션이름()
메서드들이 호출되도록 하고
On액션이름()
메서드를 입력에 따라 이동할 게임 오브젝트의 메서드로 정의해준다.
이번 강의의 코드에서는 PlayerInputController에서는 Input에 따라 호출될 메서드를 정의하여
키입력과 플레이어 게임 오브젝트를 연결해 주었고 (키를 누르면 이벤트가 실행된다!)
TopDownMovement에서는 실제 이동 방향을 정하고 이동하기 위한 메서드들을 정의했다. (이벤트를 구독할 실제 코드)
유니티 에임 시스템 구현, Atan2(), Mathf.Rad2Deg
이번에는 마우스가 이동할 때마다 호출될 에임시스템을 구현했다.
마우스가 이동하면 호출될 OnLookEvent를 구독할 OnAim() 메서드를 만들고 각도를 계산할 RotateArm() 메서드를 만들었다.
RotateArm()은 현재위치에서 마우스까지의 방향을 전달받아서 Vector2를 Mathf.Atan2() 메서드로 라디안을 계산하고
Mathf.Rad2Deg을 곱해서 360도 형식의 degree로 바꿔준다. 그리고 해당 값이 90도 이상인 경우 캐릭터를 flipX한다.
무기의 각도를 계산한 각도로 변경한다
public class TopDownAimRotation : MonoBehaviour
{
[SerializeField] private SpriteRenderer armRenderer;
[SerializeField] private Transform armPivot;
[SerializeField] private SpriteRenderer characterRenderer;
private TopDownCharacterController _controller;
private void Awake()
{
_controller = GetComponent<TopDownCharacterController>();
}
void Start()
{
_controller.OnLookEvent += OnAim;
}
void Update()
{
}
public void OnAim(Vector2 newAimDirection)
{
RotateArm(newAimDirection);
}
private void RotateArm(Vector2 direction)
{
float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
characterRenderer.flipX = Mathf.Abs(rotZ) > 90f;
//characterRenderer.flipX = armRenderer.flipY;
armPivot.rotation = Quaternion.Euler(0, 0, rotZ);
// Mathf.Atan2() 벡터의 x, y 좌표를 통해서 벡터의 아크탄젠트 즉 벡터의 각도(라디안)을 구한다.
// Quaternion을 사용하기 위해서 라디안 값 * Mathf.Rad2Deg를 곱해 360도 형식으로 변경한다.
// 90도를 기준으로 플레이어를 x축 flip.
}
}
이벤트와 델리게이트의 차이점이 정확히 뭘까?
둘의 특별한 차이점을 못 느끼겠어서 chat gpt와 구글링을 통해 자료를 찾아봤다.
델리게이트 : 메서드를 참조하고 호출하기 위한 형식
이벤트 : 다른 객체들이 특정 이벤트에 대해 구독 및 처리하고 이벤트 발생을 알림 받는 메커니즘
하지만 내 생각에는 등록된 메서드들이 Invoke된다는 점에서는 똑같으니까
특별한 차이점이라고 보기는 힘들다고 생각한다.
이벤트는 public으로 선언되어 있어도, 자신이 선언되어 있는 클래스 외부에서 호출할 수 없다?
그럼 외부 객체에서 어떻게 이벤트를 구독하지? 외부 객체에서 이벤트를 +=, -= 으로 구독하고 해제하는 것은 호출이 아닌가?
라고 생각했는데 구독과 해제는 가능하고, Invoke를 직접 호출할 수 없다는 의미라고 생각한다.
그러니까 Invoke의 접근제한자가 private인것이 이벤트의 특징이다.
또 이벤트는 인터페이스 내부에 선언할 수 있지만 델리게이트는 인터페이스 내부에 선언할 수 없다는 큰 차이점이 있다고 한다. 그런데 더 의문이 들었다 왜??
이건 인터페이스와 델리게이트의 역할의 차이에서 생긴것 같다.
인터페이스는 클래스가 꼭 구현해야 하는 메서드 집합을 정의하는데 비해서
델리게이트는 메서드나 함수의 참조를 저장하고 호출하기 위한 형식으로 클래스나 메서드의 동작을 동적으로 바꿀때
사용되기 떄문의 둘의 목적에서 차이가 나고 외부 코드에서 델리게이트를 호출하는 등 예기치 않은 동작을 초래할 수 있는 부분이 있어서 허용하지 않는다고한다.
그런데 델리게이트는 안되는데 이벤트는 허용되는 이유는?
이벤트는 델리게이트를 래핑하여 작업을 단순화하고 캡슐화하고 호출하려면 클래스 내부에서 발생시켜야 하기때문에 허용된다고 한다.