https://assetstore.unity.com/packages/3d/animations/warrior-pack-bundle-2-free-42454
공격 애니메이션을 위한 에셋이다.
플레이어의 이동과 적에게 커서를 올렸을때의 행동을 구현했으니, 이제 공격을 구현하도록 한다.
먼저, PlayerController의 UpadateMouseCursor() 함수가 PlayerController와 독립적으로 작동되므로, 커서와 관련된 함수들을 모을 클래스를 따로 작성해준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CursorController : MonoBehaviour
{
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
Texture2D _attackIcon;
Texture2D _handIcon;
enum CursorType
{
None,
Attack,
Hand,
}
CursorType _cursorType = CursorType.None;
void Start()
{
_attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack");
_handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand");
}
void Update()
{
if (Input.GetMouseButton(0))
return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f, _mask))
{
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
{
if (_cursorType != CursorType.Attack)
{
Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto);
_cursorType = CursorType.Attack;
}
}
else
{
if (_cursorType != CursorType.Hand)
{
Cursor.SetCursor(_handIcon, new Vector2(_handIcon.width / 3, 0), CursorMode.Auto);
_cursorType = CursorType.Hand;
}
}
}
}
}
이후에 CursorController 클래스를 씬의 컴포넌트로 부착해준다.
void UpdateMoving()
{
// 몬스터가 내 사정거리보다 가까우면 공격
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= 1)
{
_state = PlayerState.Skill;
return;
}
}
...
}
플레이어가 적을 공격할 수 있는 위치에 있는지 확인하기 위한 코드를 작성해준다.
void UpdateSkill()
{
Debug.Log("UpdateSkill");
// 우선은 로그만 뜨게 작성했다.
}
void Update()
{
switch (_state)
{
...
case PlayerState.Skill:
UpdateSkill();
break;
}
}
그리고 플레이어의 상태가 Skill일 때의 행동을 정하는 코드를 작성하고, PlayerController의 Update()함수에 추가해준다.
이후 게임을 실행해보니 적과 가까이 갔을 때 로그가 뜨지만, 적을 한번만 클릭했을 때는 로그가 뜨지 않는 상황이 발생했다.
이를 해결하기 위해 코드를 보니, 마우스를 뗄 때 _lockTarget 변수를 null로 초기화 하는 코드가 문제였다.
void OnMouseEvent(Define.MouseEvent evt)
{
...
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
...
}
break;
case Define.MouseEvent.Press:
{
...
}
break;
/*case Define.MouseEvent.PointerUp:
_lockTarget = null;
break;*/ // 이부분 삭제
}
}
주석처리한 부분을 삭제하니 정상적으로 작동하였다.
이제 공격하는 애니메이션을 추가해주도록 한다.
유니티짱의 에셋에는 공격 애니메이션이 없어서 에셋 스토어에서 따로 공격 애니메이션 에셋을 가져와서 추가해줬다.
ATTACK으로 가는 트랜지션은 미리 추가한 attack 불리안 값이 true일 때, 반대의 경우는 false일 때로 설정해놓았다.
그리고 설정한 애니메이션에 Event를 추가해준다.
void OnHitEvent()
{
// 애니메이션
Animator anim = GetComponent<Animator>();
// 현재 게임 상태에 대한 정보를 넘겨준다.
anim.SetBool("attack", false);
_state = PlayerState.Idle;
}
이벤트에 대한 코드 또한 PlayerController 클래스에 작성해 주었다.
이후 게임을 실행하니 원하는 대로 작동하지만, 공격 후 앞으로 이동하는 오류가 발생하였다.
이는 추가한 bool값 attack이 false가 될 때 트랜지션의 우선 순위가 RUN으로 되어있기 때문이다.
트랜지션의 우선 순위를 WAIT으로 변경하니 해결되었다.
그리고 기존에 SetBool 혹은 SetFloat을 이용해 애니메이션 상태를 바꾸면서 _state 변수의 값을 변경하기 보다는 한번에 두개를 변경할 수 있도록 코드를 수정하는 것이 좋을 것 같다.
public PlayerState State
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch(_state)
{
case PlayerState.Die:
anim.SetBool("attack", false);
break;
case PlayerState.Idle:
anim.SetFloat("speed", 0);
anim.SetBool("attack", false);
break;
case PlayerState.Moving:
anim.SetFloat("speed", _stat.MoveSpeed);
anim.SetBool("attack", false);
break;
case PlayerState.Skill:
anim.SetBool("attack", true);
break;
}
}
}
기존에 _state 변수에 접근하던 코드들을 State 프로퍼티를 사용해 접근하도록 수정하도록 한다.
그러면 이제부터는 State와 애니메이션이 함께 변경되도록 동작된다.
이제 오류가 두개 더 남았는데, 적을 한번 클릭할 때와 적을 계속 클릭하고 있을 때 공격 모션이 한번만 나오고 Idle 상태를 유지하며 이동이 안되는 오류가 발생한다. 이는 OnHitEvent에서 상태를 Idle로 변경하는데, Idle에서 Moving으로의 상태 변경은 PointerDown에서만 일어난다. 즉 처음 마우스를 클릭하는 경우에만 발생한다.
따라서 계속 마우스를 누르고 있을 때는 한번 Idle로 상태 변경이 되면 다시 마우스 클릭을 할 때 까진 Moving으로 상태 변경이 이루어질 수 없는 것이다.
해결하기 전에, OnMouseEvent() 함수는 State가 Idle, Moving, Die만 있다는 가정 하에 작성한 코드기 때문에, OnMouseEvent() 함수를 두 함수로 나누어서 작업한다.
bool _stopSkill = false;
void OnHitEvent()
{
if (_stopSkill)
{
State = PlayerState.Idle;
}
else
{
State = PlayerState.Skill;
}
}
void OnMouseEvent(Define.MouseEvent evt)
{
switch (State)
{
case PlayerState.Idle:
OnMouseEvent_IdleRun(evt);
break;
case PlayerState.Moving:
OnMouseEvent_IdleRun(evt);
break;
case PlayerState.Skill:
{
if (evt == Define.MouseEvent.PointerUp)
_stopSkill = true;
}
break;
}
}
void OnMouseEvent_IdleRun(Define.MouseEvent evt)
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
switch (evt)
{
case Define.MouseEvent.PointerDown:
{
if (raycastHit)
{
_destPos = hit.point;
State = PlayerState.Moving;
_stopSkill = false;
if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
_lockTarget = hit.collider.gameObject;
else
_lockTarget = null;
}
}
break;
case Define.MouseEvent.Press:
{
if (_lockTarget == null && raycastHit)
_destPos = hit.point;
}
break;
case Define.MouseEvent.PointerUp:
_stopSkill = true;
break;
}
}
OnHitEvent() 함수는 불리안 값 변수 _stopSkill을 이용해 상태를 변경한다.
기존에 OnMouseEvent() 함수는 OnMouseEvent_IdleRun()으로 변경하고, switch문의 마지막에 삭제했던 PointerUp 이벤트 처리를 작성한다.
또한 _stopSkill 변수를 false로 변경하는 코드를 마우스를 누르는 케이스의 처리에 추가했다.
이제 적을 한번 클릭했을때는 공격 모션이 한번 나오고 Idle로 상태 변경 이후에 이동이 된다. 계속 누르고 있을 때는 오류가 해결되지 않았는데, 이는 애니메이션이 loop 옵션이 없었기 때문이다.
ATTACK 상태의 애니메이션의 loop 옵션을 체크하니 해결되었다.
마지막으로 오류를 하나 더 발견했는데, 적과 가까운 위치에 있기만 하면 공격을 아무 방향으로 하는 오류를 발견했다. 이를 수정하기 위해 UpdateSkill() 함수를 작성했다.
void UpdateSkill()
{
if (_lockTarget != null)
{
Vector3 dir = _lockTarget.transform.position - transform.position;
Quaternion quat = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime);
}
}
이후에 정상적으로 작동하는것을 확인했다.
애니메이터의 attack, speed와 같은 변수를 사용하며 트랜지션으로 애니메이션의 변경을 하기 보다는 그냥 state라는 int형 변수를 두고 트랜지션까지 삭제하고 코드 상으로 애니메이션의 변경을 하는 것이 더 간편할 것이다.
public PlayerState State
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch (_state)
{
case PlayerState.Die:
break;
case PlayerState.Idle:
anim.CrossFade("WAIT", 0.2f);
break;
case PlayerState.Moving:
anim.CrossFade("RUN", 0.2f);
break;
case PlayerState.Skill:
anim.CrossFade("ATTACK", 0.2f, -1, 0);
break;
}
}
}
anim.Play()를 사용하면 애니메이션이 자연스럽게 전환되지 않기 때문에 CrossFade() 함수를 사용해준다.
그리고 Skill State에서의 함수만 인자가 다른데, 이는 툴에서의 loop를 체크하지 않고도 애니메이션을 반복 재생하기 위해서이다. 이로서 코드를 이용하고 툴의 기능을 최소화해서 애니메이션을 구현했다.
이후 모든 트랜지션을 삭제하고도 제대로 작동하는 것을 확인했다.
이후에 유니티짱의 프리팹에 무기를 추가해줬다.