0.들어가기에 앞서
어제자에서 플레이어의 좌우 이동 및 충돌체 위치 변경을 구현했었다. 오늘은 그에 이어 몇 가지 행동을 추가하여 테스트를 진행하고, 게임의 전체적인 구조를 만들어보고자 한다.
우선은 추가 기능을 넣기에 앞서 InputSystem의 세팅을 아래와 같이 진행했다.
원래는 SwordAttack을 키보드로 넣고 싶었지만, 테스트를 진행해보니 아무래도 공격을 왼손으로 하는 건 많이 불편했다.
어쩔 수 없이 현재 세팅으로는 마우스 좌클릭으로 검 공격을 하고, 오른쪽 마우스 버튼을 누른 채로 조준한 후 떼면 발사되는 형식으로 세팅했다.
다만 추가로 E키 등으로 스킬 캔슬 같은 건 넣어두는 편이 좋겠다는 생각을 했다.
이와 같이 세팅하고, 테스트를 위해 구현한 기능은 점프, 검 공격, 그리고 조준과 스펠 발사이다.
테스트만을 위해 작성한 코드라 중구난방하지만, 코드는 두 개만 작성하였다. (PlayerController, Spell)
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
public Vector2 InputDirection { get; private set; }
private Rigidbody2D _rigid;
private Animator _animator;
private SpriteRenderer _spriteRenderer;
private CapsuleCollider2D _capsuleCollider2D;
// InputSystem 키
private InputAction _jumpInputAction;
private InputAction _SwordAttackInputAction;
private InputAction _aimInputAction;
private bool _isMove;
private bool _isJump;
private bool _isAim;
private float _spellCoolTime;
private Transform _spellMuzzle;
[SerializeField] private float _moveSpeed;
[SerializeField] private float _jumpPow;
[SerializeField] private int _spellDelay;
[SerializeField] private GameObject _spellPrefab;
private void Awake()
{
_rigid = GetComponent<Rigidbody2D>();
_animator = GetComponent<Animator>();
_spriteRenderer = GetComponent<SpriteRenderer>();
_capsuleCollider2D = GetComponent<CapsuleCollider2D>();
_jumpInputAction = GetComponent<PlayerInput>().actions["Jump"];
_SwordAttackInputAction = GetComponent<PlayerInput>().actions["SwordAttack"];
_aimInputAction = GetComponent<PlayerInput>().actions["Aim"];
_spellMuzzle = GetComponentInChildren<Transform>();
}
private void Update()
{
_spellCoolTime -= Time.deltaTime;
OnJump();
OnAttack();
OnAim();
}
private void FixedUpdate()
{
SetMove(_moveSpeed);
}
public void OnMove(InputValue value)
{
InputDirection = value.Get<Vector2>();
}
// 점프
public void OnJump()
{
if(_isJump == false && _jumpInputAction.IsPressed())
{
_rigid.AddForce(Vector2.up * _jumpPow, ForceMode2D.Impulse);
_isJump = true;
_animator.SetBool("IsJump",_isJump);
}
}
// 공격
private void OnAttack()
{
if(_SwordAttackInputAction.WasPressedThisDynamicUpdate())
{
_animator.SetTrigger("SwordAttack");
}
}
// 스펠 발동
private void OnAim()
{
if (_spellCoolTime <= 0 && _aimInputAction.WasPressedThisDynamicUpdate())
{
_animator.SetTrigger("IsAim");
_isAim = true;
}
if (_isAim && _aimInputAction.WasReleasedThisDynamicUpdate())
{
_animator.SetTrigger("IsShoot");
Instantiate(_spellPrefab, _spellMuzzle);
_spellCoolTime = _spellDelay;
_isAim = false;
}
}
private void SetMove(float moveSpeed)
{
Vector2 moveDirection = (transform.right * InputDirection.x).normalized;
if(moveDirection.x == 0)
{
_isMove = false;
}
else
{
_rigid.velocity = new Vector2(moveDirection.x * moveSpeed, _rigid.velocity.y);
_isMove = true;
_spriteRenderer.flipX = moveDirection.x < 0;
_capsuleCollider2D.offset = moveDirection.x < 0? new Vector2(-0.4f, 0.8f) : new Vector2(0.4f, 0.8f);
}
_animator.SetBool("IsMove", _isMove);
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
_isJump = false;
_animator.SetBool("IsJump", _isJump);
}
}
}
using System.Collections;
using UnityEngine;
public class Spell : MonoBehaviour
{
private Animator _animator;
private Rigidbody2D _rigid;
[SerializeField] private float _spellSpeed;
private void Awake()
{
_animator = GetComponent<Animator>();
_rigid = GetComponent<Rigidbody2D>();
}
private void Start()
{
_rigid.AddForce(Vector2.right * _spellSpeed);
}
private void OnCollisionEnter2D(Collision2D collision)
{
_animator.SetTrigger("Burst");
StartCoroutine(DestroyTerm());
}
IEnumerator DestroyTerm()
{
yield return new WaitForSeconds(3f);
Destroy(gameObject);
}
}
플레이어의 점프는 이전 강의에서 진행했던 방식 거의 그대로 구현했다. 점프를 했는지 확인을 하고, 점프를 하지 않은 상태이면 점프를 진행, 이후 Ground로 착지를 하면 다시 점프가 가능하게 했다.
다만 더 좋은 구현 방법이 있다면 고민해볼 필요가 있다.
점프 시에 조금 속도감을 주기 위해 중력을 8로 두고, 점프 높이를 본인 키높이 정도로 할 수 있게끔 세팅했다.
좌클릭을 누를 시 공격하도록 설정하였다. 애니메이터 파라미터를 Trigger로 선택하여 공격 종료 후 바로 Idle로 돌아가게 설정했다.
스펠 공격의 경우 생각해야 할 부분이 많았다.
스펠 캐스트 모션은 아래와 같이 하나로 이어져 있는 느낌의 스프라이트였다.
하지만 아무래도 스킬로 조준을 했다가 우클릭을 떼면 발사되는 형태다 보니 차징하는 모션과 발동하는 모션을 따로 분리하는 게 나아 보였다. 따라서 차징하는 모션을 따로 만들고 반복할 수 있게 하고, 캐스트가 발동하면 Exit 하는 방식을 채용했다.
최종적으로 코드가 저렇게 나왔지만, 처음에는 키 입력과 뗐을 시에만 해당 트리거가 발동하도록 했다.
하지만 스펠에 쿨타임을 넣어주고 나니 쿨타임 중에 키를 눌렀다가 떼면 캐스트가 선입력되는 문제가 발생했었다.
이를 해결하기 위해서 키입력이 들어간 상황에서 Aim이 true가 되고, 그때 비로소 Cast의 트리거가 들어가도록 고쳤다.
오늘 작업에서 상당히 애먹었던 부분이다. 문제는 받아온 스프라이트가 제대로 잘리지 않는 것부터 시작했다.
이렇게 눈으로 봤을 때에는 딱 9개로 나눌 수 있는 스펠 스프라이트였다. 하지만 Automatic 으로 슬라이스를 해 주니 이렇게 잘라주었다.
의도했던 것과는 다른 모양이었기에 이걸 수동으로 자르는 방법에 대해서 찾아봤고, 상당히 애먹었다.
처음엔 다른 모드로 설정해서 자르라 하는 글을 보고 시도해봤는데, 아무리 시도해봐도 안 되서 다시 Automatic으로 해 보니, 이 사각형 영역이 선택된다는 사실을 알게 되었다.
그냥 단순하게 박스 사이즈 수동으로 조절하고, 빈 공간에서 드래그하면 또 새 박스 영역을 만들 수 있었다.
괜히 이상한 데서 애먹은 것 같지만, 일단은 해결했으니 배운 점으로 남겨놓고자 했다.
하지만 문제는 이게 끝이 아니었다. 왠지 모르겠는데 분명 영역을 제대로 잘랐는데 막상 스프라이트 사진을 보니 사진이 이상하게 표시되었다.
공백으로 표시되는 0번 스프라이트와 이상하게 출력되는 4번 스프라이트...
이거는 몇 번을 시도해 봐도 안 고쳐지기도 하고, 다행히 없어도 치명적인 파트는 아니라서 일단은 넘기기로 했다.
이제 프리팹과 스프라이트를 만들 건데 날아가는 모션에는 애니메이션을 넣지 않고, 터지는 모션만 애니메이션을 만들어서 제작했다.
여기서 충돌 발생으로 폭발로 넘어가는 형태인데, 플레이어, 그리고 스펠끼리는 충돌하면 안 되기 때문에 충돌 무시 설정을 해야 했다.
Project Setting에서 세팅을 진행했다.
분명 설정을 했는데 계속 플레이어와 충돌하는 것을 보고 왜 이렇지 생각을 해 봤는데, 보니까 Physics2D가 따로 있었다...
설마 이거까지 따로 있을 거라고는 생각을 못했었는데 기억해 두도록 하자.
아직은 임시 테스트용으로 만든 거라 여기저기 구멍이 많은 상황이다. 스펠도 오른쪽으로만 날아가고, 플레이어가 왼쪽으로 돌아볼 때 Muzzle이 돌아가지 않아서 이 부분에 대한 개선이 필요하다.
또한 충돌에 의해 폭발 모션으로 넘어간 다음 파괴되는 형태로 테스트했기 때문에, 앞으로 이걸 오브젝트 풀 패턴으로 구현하여 최적화를 위한 방법을 고민해야 할 것이다.
싱글톤 패턴
옵저버 패턴
오브젝트 풀 패턴
상태 패턴
MVC/MVP 패턴
MVC/MVP 패턴과 상태패턴을 융합하여 관리 및 유지 보수 등에 활용할 수 있는지 확인할 예정
옵저버 패턴으로 행동 변화에 대한 입출력의 최적화를 시도해 볼 예정