플레이어 캐릭터에게 FSM을 적용했다.
이제 앉기, 서기가 가능하다.
카메라를 빠르게 흔들면 총기의 transform이 카메라의 transform과 동기화 되지 못하고 지터링이 발생하던 버그를 고쳤다.
Weapon 객체에 애니메이터를 적용해서 ADS 시점 변환 연출을 구현했다.
public interface IState
{
void OnStateEnter();
void OnStateStay();
void OnStateExit();
}
public interface IStateMachine<T> where T : IState
{
void StateTransition(T nextState);
}
FSM을 만들기 위해, 상태 쪽과 상태머신 쪽 인터페이스를 만들어줬다.
상태머신은 제네릭으로 IState
를 구현하는 자료형만 넘겨받을 수 있게 조건을 걸어줬다.
using UnityEngine;
public class StateMachine<T> : MonoBehaviour, IStateMachine<T> where T : IState
{
protected T _currentState;
public virtual void StateTransition(T nextState)
{
_currentState?.OnStateExit();
_currentState = nextState;
_currentState?.OnStateEnter();
}
protected virtual void Update()
{
_currentState?.OnStateStay();
}
}
상태머신의 최상위 클래스다. 현재의 상태와 각 상태의 로직 호출, 상태 전이 등을 구현해줬다.
이 클래스를 상속받아서 세부적인 상태 머신을 필요에 따라 구현할 수 있도록 고민해봤다.
PlayerStateMachine
은 이 클래스를 상속받아 구현했다.
public class PlayerStateBase : IState
{
protected PlayerStateMachine _machine;
protected PlayerController _controller;
protected InputManager _input;
protected Transform _cameraTransform;
public PlayerStateBase(PlayerStateMachine machine)
{
_machine = machine;
_controller = machine.Controller;
_input = machine.Input;
_cameraTransform = machine.CameraTransform;
}
public virtual void OnStateEnter()
{
}
public virtual void OnStateExit()
{
}
public virtual void OnStateStay()
{
}
}
플레이어의 상태를 구현하기 위해, 플레이어의 정보를 갖고 있을 수 있도록 상태의 Base를 만들어줬다.
using System.Collections.Generic;
using UnityEngine;
public class PlayerStateMachine : StateMachine<PlayerStateBase>
{
public PlayerController Controller { get; private set; }
public InputManager Input { get; private set; }
public Transform CameraTransform { get; private set; }
public enum PlayerState
{
Stand,
Sit,
}
private Dictionary<PlayerState, PlayerStateBase> _stateDict;
public Dictionary<PlayerState, PlayerStateBase> StateDictionary => _stateDict;
private void Awake()
{
Initialize();
}
public void Initialize()
{
Controller = GetComponent<PlayerController>();
Input = InputManager.Instance;
CameraTransform = Camera.main.transform;
_stateDict = new()
{
{ PlayerState.Stand, new PlayerStandState(this) },
{ PlayerState.Sit, new PlayerSitState(this) },
};
StateTransition(_stateDict[PlayerState.Stand]);
}
}
PlayerStateBase
를 제네릭으로 넘겨받는 PlayerStateMachine
을 작성했다.
딕셔너리와 열거형을 이용해서 상태 객체를 관리하도록 했다.
플레이어는 앉기, 서기 상태가 있을 수 있다. 필요하다면 이후에 점프, 낙하 상태가 필요할 수 있으므로 PlayerStateBase
를 상속받는 PlayerGroundState
를 작성하고, PlayerGroundState
를 상속받는 PlayerStandState
와 PlayerSitState
를 작성했다.
이후, 점프와 낙하 상태의 구현이 필요하다면 PlayerAirState
를 만들고 PlayerJumpState
와 PlayerFallState
를 작성하면 될 것 같다.
public class PlayerGroundState : PlayerStateBase
{
public PlayerGroundState(PlayerStateMachine machine) : base(machine) { }
public override void OnStateStay()
{
_controller.Move();
if (_input.ADSTrigger)
_controller.ChangeADS();
}
}
public class PlayerStandState : PlayerGroundState
{
public PlayerStandState(PlayerStateMachine machine) : base(machine) { }
public override void OnStateEnter()
{
_controller.Stand();
}
public override void OnStateStay()
{
base.OnStateStay();
if (_input.SitKeyDown)
_machine.StateTransition(_machine.StateDictionary[PlayerStateMachine.PlayerState.Sit]);
}
}
public class PlayerSitState : PlayerGroundState
{
public PlayerSitState(PlayerStateMachine machine) : base(machine) { }
public override void OnStateEnter()
{
_controller.Sit();
}
public override void OnStateStay()
{
base.OnStateStay();
if (_input.SitKeyUp)
_machine.StateTransition(_machine.StateDictionary[PlayerStateMachine.PlayerState.Stand]);
}
}
기존에는 Player
객체 아래에 Weapon
이라는 객체를 만들고, Cimemachine
을 이용해 1인칭 시점을 구현하고 그 MainCamera
에 딱 붙여서 총기를 렌더링하게 했다.
그 과정에서 카메라의 회전값만큼 캐릭터와 총기모델을 회전하게 해줬는데, 카메라를 빠르게 흔들면 총기에 지터링이 발생했다.
아무래도, 카메라의 회전값과 총기모델의 회전값을 동기화하는 시점이 달라서 발생하는 문제같아서 LateUpdate()
메서드에서도 동기화해보고, 마우스 인풋값에 따라 카메라를 회전시켜주는 시네머신 확장 클래스의 PostPipelineStageCallback()
메서드에서도 동기화 해봤지만 결과는 모두 같았다.
MainCamera
의 자식 오브젝트로 Weapon
객체를 넣어줬다.
그리고, 카메라를 하나 더 만들어서 Weapon
만 렌더링 되도록 컬링마스크를 건 Depth-Only Camera
를 만들었다.
이동, 앉기, 서기, ADS전환, 카메라에임 모두 잘 동작한다.