개요
플레이어 공격 모션을 구현하는데, 이동하면서 공격이 가능해야했다. 기존에는 Attack도 State로 구현했으나, 구현하다보니 결국 Action으로 분리하기로 했다.
내용
먼저 현 코드상으로 공격 상태일 때 이동할 수 있도록 처리했다. 하지만 그렇게 하니 다리 애니메이션이 움직이지 않아, Animator에서 레이어를 오버라이드하여 공격모션은 상체만 적용하도록 구현했다.
하지만 그 상태로도 State 구조 상 공격 중에는 상태가 공격 상태로 고정되어 다리 애니메이션이 부자연스러웠다. AI와 해당 내용을 공유하며 상의해본 결과, 현 구조가 맞지 않는다고 판단했다.
나로서도 서 있을 때도, 움직일 때도 공격이 가능해야하기 때문에 공격은 행동으로 빼는 게 더 맞다고 판단하여 해당 구조로 변경했다.
IPlayerAction을 생성하여 기존 AttackState를 재활용한 PlayerActionAttack을 생성했다.
public interface IPlayerAction {
void StartAction(PlayerController player);
void UpdateAction();
void EndAction();
bool IsActive { get; }
}
using UnityEngine;
public class PlayerActionAttack : IPlayerAction {
private PlayerController player;
private int comboStep = 1;
private bool comboQueued = false;
private bool canReceiveCombo = false;
public bool IsActive { get; private set; }
public void StartAction(PlayerController player) {
this.player = player;
IsActive = true;
comboStep = 1;
comboQueued = false;
PlayComboAnimation(comboStep);
player.PlayerAnimator.SetBool("Attack", true);
}
public void UpdateAction() {
if (Input.GetKeyDown(KeyCode.X) && canReceiveCombo) {
comboQueued = true;
}
}
public void EndAction() {
player.PlayerAnimator.SetBool("Attack", false);
IsActive = false;
player = null;
}
public void EnableCombo() {
canReceiveCombo = true;
}
public void DisableCombo() {
canReceiveCombo = false;
if (comboQueued && comboStep < 4) {
comboStep++;
PlayComboAnimation(comboStep);
comboQueued = false;
} else {
EndAction(); // 행동 종료
}
}
private void PlayComboAnimation(int step) {
player.PlayerAnimator.SetInteger("ComboStep", step);
// 무기에 콤보 단계 전달
var weapon = player.GetComponentInChildren<WeaponController>();
if (weapon != null)
{
weapon.SetComboStep(step);
}
}
}
또한 기존에 PlayerStateIdle, PlayerStateMove에서 공격 키 인풋 처리하는 대신 PlayerController에서 입력을 받아 두 상태 각각에서 처리하는 게 아닌 공통으로 동작을 처리하도록 구현했다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum PlayerState { None, Idle, Move, Hit, Dead }
public class PlayerController : CharacterBase, IObserver<GameObject>
{
// 외부 접근 가능 변수
[Header("Attach Points")]
[SerializeField] private Transform rightHandTransform;
// 내부에서만 사용하는 변수
private CharacterController _characterController;
private bool _isBattle;
private GameObject weapon;
private WeaponController _weaponController;
private IPlayerState CurrentStateClass { get; set; }
private IPlayerAction currentAction;
// 상태 관련
private PlayerStateIdle _playerStateIdle;
private PlayerStateMove _playerStateMove;
// 행동 관련
private PlayerActionAttack attackAction;
// 외부에서도 사용하는 변수
public FixedJoystick joystick { get; private set; }
public PlayerState CurrentState { get; private set; }
private Dictionary<PlayerState, IPlayerState> _playerStates;
public Animator PlayerAnimator { get; private set; }
public CharacterController CharacterController => _characterController;
private void Awake()
{
PlayerAnimator = GetComponent<Animator>();
_characterController = GetComponent<CharacterController>();
if (joystick == null)
{
joystick = FindObjectOfType<FixedJoystick>();
}
}
protected override void Start()
{
base.Start();
// 상태 초기화
_playerStateIdle = new PlayerStateIdle();
_playerStateMove = new PlayerStateMove();
_playerStates = new Dictionary<PlayerState, IPlayerState>
{
{ PlayerState.Idle, _playerStateIdle },
{ PlayerState.Move, _playerStateMove },
};
attackAction = new PlayerActionAttack();
PlayerInit();
}
private void Update()
{
if (CurrentState != PlayerState.None)
{
_playerStates[CurrentState].Update();
}
// 현재 액션이 활성화 되어 있으면 Update 호출
if (currentAction != null && currentAction.IsActive) {
currentAction.UpdateAction();
}
// 공격 입력 처리
if (Input.GetKeyDown(KeyCode.X) && (currentAction == null || !currentAction.IsActive)) {
StartAttackAction();
}
}
public void StartAttackAction() {
currentAction = attackAction;
currentAction.StartAction(this);
}
#region 초기화 관련
private void PlayerInit()
{
SetState(PlayerState.Idle);
InstantiateWeapon();
}
private void InstantiateWeapon()
{
if (weapon == null)
{
GameObject weaponObject = Resources.Load<GameObject>("Player/Weapon/Chopstick");
weapon = Instantiate(weaponObject, rightHandTransform);
_weaponController = weapon?.GetComponent<WeaponController>();
_weaponController?.Subscribe(this);
weapon?.SetActive(_isBattle);
}
}
#endregion
public void SetState(PlayerState state)
{
if (CurrentState != PlayerState.None)
{
_playerStates[CurrentState].Exit();
}
CurrentState = state;
CurrentStateClass = _playerStates[state];
CurrentStateClass.Enter(this);
}
public void SwitchBattleMode()
{
_isBattle = !_isBattle;
weapon.SetActive(_isBattle);
}
// Animation Event에서 호출될 메서드
public void SetAttackComboTrue() {
if (_weaponController.IsAttacking) return; // 이미 공격 중이면 실행 안함
if (currentAction == attackAction) {
attackAction.EnableCombo();
_weaponController.AttackStart();
}
}
public void SetAttackComboFalse() {
if (currentAction == attackAction) {
attackAction.DisableCombo();
_weaponController.AttackEnd();
}
}
#region IObserver 관련
public void OnNext(GameObject value)
{
Debug.Log("무기 타격");
float playerAttackPower = _weaponController.AttackPower * attackPower; // 플레이어 공격 데미지(막타는 일반 데미지의 4배)
}
public void OnError(Exception error)
{
}
public void OnCompleted()
{
_weaponController.Unsubscribe(this);
}
#endregion
}
사실 오늘 초반에 WeaponController에서 null 에러가 계속 났는데 내가 스크립트를 비활성화 해두고 깜박했기 때문이었다. 또한 무기 안들었을 때도 동일한 에러가 발생하는 것을 확인하여 hitColliders null시 로그 출력을 추가해서 그 부분도 추후 예방할 수 있게 했다.
public void AttackStart()
{
if (_hitColliders == null)
{
Debug.LogError("_hitColliders가 null입니다. 무기를 들고 있는지 확인해 주세요!");
return;
}
_isAttacking = true;
_hitColliders.Clear();
for (int i = 0; i < _triggerZones.Length; i++)
{
_previousPositions[i] = transform.position + transform.TransformVector(_triggerZones[i].position);
}
}
느낀 점
중간에 머리가 좀 아팠지만 일단은 해결되어 다행이다. 이런 오류 없는 버그가 역시 제일 어려운 것 같다. 그래도 이번에 처음으로 animator의 layer 기능도 써보고 avatar mask도 써보는 등 새로운 기능을 많이 사용해서 재밌었다.
'공부 > [TIL] Game Bootcamp' 카테고리의 다른 글
[TIL] #6. Animation event 작동 안함(뭔가 만들었다면 꼭 초기화를 하자) : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.25 |
---|---|
[TIL] #5. 플레이어 대시 Action : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.24 |
[TIL] #3. 플레이어 State 분리 : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.18 |
[TIL] #2. 에셋 캐릭터 애니메이션 편집 : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.17 |
[TIL] #1. 모바일 게임 플레이어 이동 구현 : 멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 (0) | 2025.04.16 |