세피리아의 캐릭터 동작에 맞춰 기본적인 이동과 마우스 방향에 따른 캐릭터 및 장비 위치 변경, 대시나 공격, 방어등의 여러 동작

(기본 이동, 애니메이션 적용 상태)
세피리아는 2.5D 게임이기 때문에 상하좌우 움직임이 필요한데, 움직임 입력은 인풋 시스템을 사용하기로 했다.
//플레이어 이동 입력 받기
public void OnMove(InputAction.CallbackContext context)
{
inputVec = context.ReadValue<Vector2>();
}
//플레이어 움직임 처리
private void PlayerMove()
{
Vector2 nextVec = inputVec * (moveSpeed * Time.fixedDeltaTime);
rb.MovePosition(rb.position + nextVec);
}
OnMove를 통해 인풋 시스템에서 움직임 입력을 받아 inputVec 변수에 저장해주고, PlayerMove 메소드에서 inputVec 변수 및 캐릭터 스피드 변수를 통해 캐릭터의 다음 위치를 지정해주도록 되어있다.

세피리아는 키보드 + 마우스로 조작하는 게임으로, 마우스의 위치에 따라서 캐릭터가 보는 방향, 그에 따른 무기 및 방패 위치 전환, 더 나아가 공격 등 각종 액션이 마우스 위치에 영향을 받게된다.
//Update에서 실행되는 마우스 위치를 방향으로 저장하는 메소드
private void UpdateDirection()
{
//마우스 좌표 가져오기
Vector2 mouseScreenPosition = Mouse.current.position.ReadValue();
//마우스 좌표를 월드 좌표로 변환
Vector2 mouseWorldPosition = mainCamera.ScreenToWorldPoint(mouseScreenPosition);
//마우스의 좌표에서 플레이어의 좌표를 빼주고, 정규화
lookDirection = (mouseWorldPosition - rb.position).normalized;
}
가장 먼저 현재 마우스의 위치를 가져와서 캐릭터의 위치를 빼주어서 마우스가 있는 방향을 lookDirection 이라는 변수에 기록해둔다.
이 lookDirection 변수는 각종 기능들에서 사용되기 때문에 중요한 변수이다.
//Update()에서 호출하는 종합 방향 업데이트 메소드
private void ApplyDirection(Vector2 direction)
{
if (/* 공격중인 상태라면*/) return;
//플레이어 방향, 장비 위치, 검의 각도 조절
UpdatePlayerDirection(direction);
UpdateEquipmentPosition(direction);
UpdateWeaponHandDirection(direction);
}
lookDirection에 저장된 방향을 기준으로 ApplyDirection이라는 메소드를 호출하는데, 이 메소드는 현재 캐릭터가 공격중인 상태가 아니라면 세가지 방향 업데이트를 진행해주도록 되어있다.
//플레이어 스프라이트 좌우 플립
private void UpdatePlayerDirection(Vector2 direction)
{
//보는 방향에 따라서 캐릭터 플립
playerRenderer.flipX = direction.x < 0f;
}
첫번째 메소드는 마우스의 위치가 플레이어보다 왼쪽으로 가면 플레이어의 스프라이트를 X축으로 플립해주어서 캐릭터가 마우스 방향에 맞춰 왼쪽, 오른쪽을 쳐다볼 수 있게 해주고 있다.
//플레이어 장비 위치 컨트롤
private void UpdateEquipmentPosition(Vector2 direction)
{
//보는 방향 체크
bool isLookingUp = direction.y > 0f;
float directionSign = direction.x < 0f ? -1f : 1f;
//보는 방향(위아래)에 맞는 무기, 방패 위치 설정
Vector2 weaponPosition = isLookingUp ? weaponHandUpPosition : weaponHandDownPosition;
Vector2 shieldPosition = isLookingUp ? shieldHandUpPosition : shieldHandDownPosition;
//보는 방향(좌우)에 맞는 무기, 방패 위치 설정
weaponPosition.x *= directionSign;
shieldPosition.x *= directionSign;
//최종 위치 확정
weaponHand.localPosition = weaponPosition;
shieldHand.localPosition = shieldPosition;
}
세피리아의 기본 무기인 검은 한손에는 검, 다른 한손에는 방패를 들고 있는데, 플레이어가 보는 방향쪽에 검이 오도록 되어있다.
또한, 캐릭터가 위쪽을 볼때와 아래쪽을 볼 때 무기 및 방패의 위치가 조금씩 달라지는데, 이 두 현상을 위해 구성된 메소드가 바로 이 두번째 메소드이다.
UpPosition 및 DownPosition에는 위를 볼때와 아래를 볼 때 설정해줄 검, 방패 좌표값이 담겨있으며, 이에 맞춰 위나 아래를 볼 때 알맞은 좌표로 무기의 위치를 설정해준다.
왼쪽 오른쪽을 볼 때는 무기의 X 좌표들을 그에 맞춰 -1 혹은 1을 곱해주어 플레이어가 어느 방향을 보던 무기의 위치에 문제가 없도록 되어있다.
//마우스 위치에 따른 검 회전처리
private void UpdateWeaponHandDirection(Vector2 direction)
{
//방향 벡터를 라디안으로 전환하고, 이를 각도로 전환하여 회전해야하는 각도를 구함
float referenceAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
//검의 앞면이 마우스 방향을 향하도록 각도 보정
float handAngle = referenceAngle - 70f;
//검을 쥔 손의 각도 보정
weaponHand.rotation = Quaternion.Euler(0f, 0f, handAngle);
}
전달받은 마우스의 방향을 Mathf.Atan2를 이용하여 라디안으로 변경하고, 거기서 Mathf.Rad2Deg를 곱해주어서 각도로의 변환을 진행해준다.
이렇게 계산된 각도를 바로 적용하게 되면 검 끝이 항상 마우스로 가게 되는데, 검 끝이 아닌 검의 앞면이 마우스를 보도록 해주기 위해서 70도라는 각도 보정을 진행해준다.
이렇게 보정된 각도를 검을 쥔 손에 적용해주어서 세피리아와 같이 마우스 위치에 맞춰 검을 세우는 연출을 진행할 수 있다.

대시 액션 또한 인풋 시스템을 통해 입력을 받도록 해주었다.
//플레이어 대시 입력 받기
public void OnDash(InputAction.CallbackContext context)
{
if (!context.performed) return;
//현재 대시중이거나, 대시 횟수가 없다면 패스
if(isDashing || currentDashCount <= 0) return;
//이동 입력이 있다면 이동 방향, 아니라면 보고있는(마우스) 방향
dashDirection = inputVec.sqrMagnitude > 0.01f ? inputVec.normalized : lookDirection;
currentDashCount--;
isDashing = true;
dashTimer = dashDuration;
}
코드는 단순하게 대시를 할 수 있는 상황이라면 플레이어가 원하는 방향을 지정해주고, 대시 상태를 활성화 해주도록 되어있다.
이때 대시 카운트 조절과 대시 타이머 활성화까지 같이 진행해준다.
private void FixedUpdate()
{
if (isDashing)
{
PlayerDash();
dashTimer -= Time.fixedDeltaTime;
if(dashTimer <= 0f)
isDashing = false;
}
else
{
PlayerMove();
}
}
활성화 된 대시 상태는 FixedUpdate에서 처리해주는데, 대시중인 상태라면 대시 메소드 호출 및 대시 타이머 차감을 진행한다.
//플레이어 대시 처리
private void PlayerDash()
{
rb.MovePosition( rb.position + dashDirection * (dashSpeed * Time.fixedDeltaTime) );
}
따라서 플레이어 대시 자체는 대시의 방향과 속도를 곱해서 플레이어 위치에 새로 적용해주는 단순한 방식으로 구성해주었다.
//대시 재충전
private void UpdateDashRecharge()
{
//현재 사용 가능한 대시 횟수가 최대라면 패스
if(currentDashCount >= maxDashCount) return;
//대시 충전 타이머
dashRechargeTimer += Time.deltaTime;
//정해진 충전 시간보다 타이머가 커지면 대시 횟수 충전
if (dashRechargeTimer >= dashRechargeTime)
{
currentDashCount++;
dashRechargeTimer = 0f;
}
}
세피리아 대시에는 쿨타임이 있기 때문에, 쿨타임 회복 부분까지 제대로 구현해주었다.
//애니메이션 업데이트
private void UpdateAnimation()
{
//공격 중인 상태 확인
bool isAttack = /* 각종 공격 형태 플래그*/;
//공격중이라면 공격 방향, 아니라면 보는 방향을 애니메이션 방향으로 결정
Vector2 animationDirection = isAttack ? attackDirection : lookDirection;
//위쪽, 아래쪽 애니메이션을 나누기 위한 Y축 검사 및 파라미터 설정
float lookY = animationDirection.y > 0f ? 1f : -1f;
animator.SetFloat("LookY", lookY);
//움직임 등 각종 액션용 애니메이션(생략)
animator.SetBool("IsMoving", isAttack ? false : inputVec.sqrMagnitude > 0.01f);
}
다음으로 넘어가기 전에 애니메이션 처리도 한번 살펴봐야 한다.
세피리아에서 모션 테스트들을 진행하다보면 공격 도중엔 애니메이션 전환이 안되어야 하는 부분들이 존재한다.
대표적으로 공격 도중 마우스의 위치가 변경되어 플레이어가 갑자기 위쪽을 쳐다본다는 등의 현상을 방지해야만 한다.
따라서 공격중인 상태라면 공격 방향을 애니메이션 방향으로 저장해두고, 이를 기준으로 y축 체크를 진행하여 파라미터로 전달해주고 있다.
움직임 애니메이션의 경우 공격 모션 도중엔 움직임이 없기 때문에 그에 맞춰서 애니메이션도 나오지 않도록 처리해주었다.

//플레이어 방어 입력 받기
public void OnGuard(InputAction.CallbackContext context)
{
//꾹 누르면 방어 상태를 의미
if (context.performed)
{
isGuardHeld = true;
//만약 특수 공격중이라면 가드는 실행하지 않음
if (/* 대시, 특수공격 플래그 */) return;
//일반 공격은 즉시 취소하고 가드 상태로 전환하기 위해 공격 종료 시 진행될 변수 초기화 진행
if (/* 일반 공격 플래그 */) CancelNormalAttack();
ApplyDirection(lookDirection);
StartGuard();
}
//떼면 방어 해제를 의미
else if (context.canceled)
{
isGuardHeld = false;
//특수 공격 등으로 인해 가드가 이미 해제된 경우 -> 방어 해제 애니메이션 무력화
if (!isGuarding) return;
StopGuard();
}
}
방어 액션 또한 마찬가지로 인풋 시스템을 통해 입력을 받게 해주었다.
우클릭을 누르고 있으면 방어 모션이 실행되며, 우클릭을 떼면 방어 모션을 그만두고 기본 모션으로 돌아가게 되어있다.
세피리아에서 공격 도중 가드 기능을 테스트해보면 일반 공격 도중 가드는 공격을 즉시 중단하고 가드를 실행하지만, 대시 공격 및 특수 공격은 가드 입력을 무시하도록 되어있다.
다만 이런 모션 도중 우클릭을 꾹 누른채 유지한다면 대시 및 특수공격 모션이 종료된 후 즉시 가드를 진행하기 때문에 가드의 경우 플래그용 변수는 isGuardHeld, isGuarding 두가지로 나뉘게 된다.
따라서 위 코드는 일반 공격 도중에 가드 액션이 발생하면 즉시 가드를 진행하고, 대시 및 특수공격 도중에 가드 액션이 발생하면 가드 버튼 홀드 상태만 저장해둔 채 입력을 무시하고 추후 모션이 끝났을 때 다음과 같은 코드를 실행해주도록 되어있다.
//가드버튼을 유지하고 있다면 가드 실행
if (isGuardHeld)
{
StartGuard();
}
Start, Stop Guard 메소드는 있는 그대로 가드 실행, 중단만을 진행한다.
private void StartGuard()
{
isGuarding = true;
equipmentAnimator.Play("StartGuard");
}
private void StopGuard()
{
isGuarding = false;
equipmentAnimator.Play("StopGuard");
}
//StopGuard 애니메이션 종료 시 실행
public void OnGuardingEnd()
{
equipmentAnimator.Play("Weapon_Idle");
}
가드 진행에 맞춰 가드 플래그 변수 및 애니메이션을 제어해주고 있다.
아직은 전투 기능을 구현하지 않았기 때문에 현재로써는 단순하게 애니메이션만 출력하는 정도에 그친다.
//가드 시 방패 이동 애니메이션을 위한 방패 방향 업데이트
//ApplyDirection 메소드에서 실행
private void UpdateShieldHandDirection(Vector2 direction)
{
//어느쪽을 보고있는지 체크
bool isLookingLeft = direction.x < 0f;
if (isLookingLeft)
{
//왼쪽을 본다면 Scale 변경
shieldHand.localScale = new Vector3(-1f, 1f, 1f);
}
else
{
//오른쪽을 본다면 기본 스케일
shieldHand.localScale = Vector3.one;
}
}
다만 가드를 진행하고 있을 때 플레이어가 마우스를 반대로 넘겨서 뒤를 보게될 수가 있는데, 이때 방패의 위치가 기존 위치에 그대로 유지된다면 정면이 아닌 등 뒤를 방어하는 것 처럼 보일수가 있다.
따라서 보는 방향에 맞춰 방향전환을 해주는 부분 또한 중요하다.

세피리아 한손검의 공격은 기본 공격, 특수(강) 공격, 대시 공격으로 나뉘어진다.
//플레이어 공격 입력 받기
public void OnAttack(InputAction.CallbackContext context)
{
if (!context.performed) return;
//특수, 대시 공격 중이라면 패스
if (isStrikeAttacking || isDashAttacking) return;
//가드 중 공격은 특수 공격
if (isGuarding)
{
isGuarding = false;
attackDirection = lookDirection;
StrikeAttackStart();
}
//대시 중 공격은 대시 공격
else if (isDashing)
{
isDashing = false;
attackDirection = lookDirection;
DashAttackStart();
}
//공격 중이라면 버퍼처리
else if (isAttacking)
{
//공격 버퍼 true
hasBufferedAttack = true;
bufferedAttackDirection = lookDirection;
}
//공격 중이 아니라면 바로 공격 실행
else
{
attackDirection = lookDirection;
AttackStart();
}
}
기본 공격은 1타 2타 3타로 콤보가 나뉘어지고, 특수 및 대시 공격도 같은 공격 버튼을 이용하기 때문에 공격 액션을 받으면 4가지 조건중 하나가 진행된다.
코드의 순서와는 다르게 일반 공격부터 진행할 예정이다.
//공격 실행
private void AttackStart()
{
//공격 상태 전환
ApplyDirection(attackDirection);
isAttacking = true;
attackCombo++;
//공격 모션 실행
equipmentAnimator.Play($"Attack_{attackCombo}");
if (attackCombo == 3)
{
attackMoveTimer = attackMoveDuration * 1.3f;
}
else
{
attackMoveTimer = attackMoveDuration;
}
}
공격이 실행되면 초반에 설명했던 ApplyDirection 메소드를 공격방향을 넣어서 실행시켜, 공격 방향에 맞춰 플레이어의 방향을 한번 맞춰주면서 로직이 시작된다.
이후 공격 플래그와 콤보를 늘려주고 그에 맞춰 애니메이션을 동작시킨다.
세피리아의 공격에는 현실의 스텝과도 같이 공격으로 인한 이동이 존재하는데, 이를 구현하기 위해 attackMoveTimer 라는 변수가 존재한다.
attackMoveTimer 변수에 저장된 시간은 FixedUpdate에서 부터 시작되는 PlayerAttackMove라는 메소드에서 이용하게 된다.
//공격 도중 움직임 처리
private void PlayerAttackMove()
{
//attackMoveTimer가 0 이하라면 패스
if (attackMoveTimer <= 0f) return;
//공격 도중 이동할 속도 지정
float speed = isDashAttacking ? dashAttackMoveSpeed : attackMoveSpeed;
//공격 방향과 속도를 곱해줌
Vector2 nextVec = attackDirection * (speed * Time.fixedDeltaTime);
//공격 이동으로 인한 위치 조정
rb.MovePosition(rb.position + nextVec);
//attackMoveTimer값을 줄여줌
attackMoveTimer -= Time.fixedDeltaTime;
}
이 메소드는 attackMoveTimer 타이머를 줄이면서 아직 타이머가 남아있다면 일정 속도로 캐릭터의 위치를 옮겨주도록 되어있다.
다시 위쪽의 공격 코드를 보면 attackCombo가 3인 경우엔 미리 정해둔 attackMoveDuration에 1.3을 곱해서 attackMoveTimer 값을 더 크게 지정해주는 것을 볼 수 있다.
이 3타 공격은 1,2타 공격에 비해 모션이 크고 그만큼 이동거리 및 이동 시간이 더 길기 때문에 타이머 값을 더 크게 잡아주어 비슷하게 이동할 수 있도록 해주었다.
//공격 중이라면 버퍼처리
else if (isAttacking)
{
//공격 버퍼 true
hasBufferedAttack = true;
bufferedAttackDirection = lookDirection;
}
공격 액션에서 위와 같이 공격중에 추가 공격을 눌렀을 때 진행되는 공격이다.
혹은 공격 이후 콤보를 위해 다음 공격을 기다리는 도중 공격을 진행했을 때에도 콤보 공격이 진행된다.
//애니메이션 이벤트로 호출되는 공격 모션 종료 메소드
public void OnAttackAnimationEnd()
{
//공격 상태 해제
isAttacking = false;
//3타 공격이 아직 아니라면
if (attackCombo < 3)
{
//입력 버퍼가 존재한다면 버퍼 처리
if (hasBufferedAttack)
{
//버퍼에 기록해둔 방향을 공격 방향으로 저장하고 버퍼 초기화
attackDirection = bufferedAttackDirection;
hasBufferedAttack = false;
//공격 다시 진행
AttackStart();
}
else
{
//다음 모션 전 방향 업데이트
ApplyDirection(lookDirection);
//버퍼가 없다면 다음 공격 입력 대기
equipmentAnimator.Play($"WaitAttack{attackCombo+1}");
}
}
//즉시 공격 종료
else
{
OnAttackEnd();
}
}
만약 공격 도중 공격이 입력되어 공격 버퍼가 저장됐다면 아직 3타 공격이 아니라는 가정 하에 이 단계에서 버퍼를 이용해 추가 공격을 진행한다.
버퍼는 공격 플래그 버퍼, 공격 방향 버퍼 두 종류로, 공격을 해야한다는 정보와 그 방향에 대한 정보를 담고있다.
버퍼가 있다면 이 데이터들을 기반으로 추가 공격을 진행하게 되는데, 아직은 attackCombo 변수를 초기화 하지 않았기에 이렇게 공격이 진행되면 다음 콤보에 대한 공격을 진행하게 된다.
만약 버퍼가 없다 하더라도, 추가 공격을 기다리는 시간을 갖기 위해 공격 입력 대기 애니메이션을 시작한다.
이 애니메이션 기간 동안 공격이 진행되면 처음으로 돌아가서 다시 AttackStart 메소드를 실행하게 되는데, 마찬가지로 콤보는 아직 초기화가 되지 않았기 때문에 다음 콤보에 대한 공격이 진행된다.
WaitAttack 애니메이션이 끝나거나 애초에 3타 공격이였다면 OnAttackEnd 메소드가 실행되게 된다.
//기본 공격 완전 종료
public void OnAttackEnd()
{
//공격에 사용되는 모든 변수 초기화 및 Idle 애니메이션 실행
CancelNormalAttack();
equipmentAnimator.Play("Weapon_Idle");
}
//공격 관련 변수들
private void CancelNormalAttack()
{
//공격 및 공격 버퍼 플래그,
//공격 및 공격 버퍼 방향,
//공격 시 이동할 타이머 및 공격 콤보 초기화
isAttacking = false;
hasBufferedAttack = false;
attackDirection = Vector2.zero;
bufferedAttackDirection = Vector2.zero;
attackMoveTimer = 0f;
attackCombo = 0;
}
OnAttackEnd 메소드까지 실행이 되고 나면 기본(+콤보) 공격이 전부 마무리 되게 된다.
//가드 중 공격은 특수 공격
if (isGuarding)
{
isGuarding = false;
attackDirection = lookDirection;
StrikeAttackStart();
}
공격 액션이 가드를 진행하는 도중 입력되었을 때 실행되는 공격을 의미한다.
입력받는 즉시 가드 상태를 해제하고 공격 방향을 저장하면서 특수공격 메소드를 실행한다.
//특수 공격 실행
private void StrikeAttackStart()
{
//공격 방향 및 특수 공격 플래그 전환
ApplyDirection(attackDirection);
isStrikeAttacking = true;
//공격 모션 실행
equipmentAnimator.Play("StrikeAttack");
attackMoveTimer = attackMoveDuration * 2;
}
기본 공격 방식과 거의 유사한데, 방향 설정 및 플래그 전환을 진행하고, 알맞는 공격 모션 진행과 함께 attackMoveTimer 를 좀 더 크게 지정해준다.
//특수, 대시 공격 모션 종료
public void OnSpecialAttackEnd()
{
isDashAttacking = false;
isStrikeAttacking = false;
CancelNormalAttack();
ApplyDirection(lookDirection);
//가드 버튼이 아직 해제 안됐다면
if (isGuardHeld)
{
StartGuard();
}
//해제됐다면 기본 상태로 변경
else
{
equipmentAnimator.Play("Weapon_Idle");
}
}
이 메소드는 대시 공격의 모션이 종료됐을 때도 동일하게 호출되는 메소드로, 공격과 관련된 변수를 전부 초기화 해준 다음 가드버튼이 눌려져있는 상태인지 아닌지를 판단한다.
방어 파트에서 한번 설명하긴 했지만, 가드 버튼이 눌려져 있다면 가드를 진행하고, 아니라면 다시 기본 상태로 애니메이션을 전환해주게 된다.
//대시 중 공격은 대시 공격
else if (isDashing)
{
isDashing = false;
attackDirection = lookDirection;
DashAttackStart();
}
공격 액션이 대시 도중 입력되었을 때 실행되는 공격을 의미한다.
private void DashAttackStart()
{
ApplyDirection(attackDirection);
isDashAttacking = true;
//공격 모션 실행
equipmentAnimator.Play("DashAttack");
attackMoveTimer = attackMoveDuration;
}
대시 공격이라고 해서 로직이 크게 달라질게 없다.
위에서 본 내용대로 공격 방향과 플래그 설정을 진행하고, 애니메이션을 실행하면서 attackMoveTimer 변수를 저장해준다.
attackMoveTimer 에 저장되는 값은 기본 공격과 동일한 attackMoveDuration를 입력해주지만, PlayerAttackMove 메소드를 다시 보면 대시 공격의 경우 속도를 별도로 지정해주기 때문에 일반 공격보다 더 멀리 이동하게 된다.
//PlayerAttackMove 메소드 내용 일부
float speed = isDashAttacking ? dashAttackMoveSpeed : attackMoveSpeed;
대시 공격의 모션이 끝나면 특수 공격 파트에서 설명한 OnSpecialAttackEnd 메소드가 실행되면서 공격 로직이 마무리 된다.