개요
FSM적다가 뛰쳐나왔습니다.
생각해보니 Player의 기반 코드를 설명을 안했더군요.
후딱 설명하고 FSM 마저 쓰겠습니다.
Player
저번 글에선 Player의 Input을 처리했습니다.
그렇다면 이제 Player를 움직여줄 차례입니다.
하지만 움직이는 스크립트 전에 Player 클래스를 먼저 만들겠습니다.
Player 만드는 김에 Enemy도 있어야 하니 둘을 총괄하는 추상 클래스 Agent를 만들겠습니다.
public abstract class Agent : MonoBehaviour
{
public bool CanStateChangeable { get; protected set; } = true;
#region Components
public Animator AnimatorCompo { get; protected set; }
#endregion
protected virtual void Awake()
{
Transform visualTrm = transform.Find("Visual");
AnimatorCompo = visualTrm.GetComponent<Animator>();
}
public virtual void Attack() { }
public Coroutine StartDelayCallBack(float delayTime, Action CallBack)
{
return StartCoroutine(DelayCoroutine(delayTime, CallBack));
}
protected IEnumerator DelayCoroutine(float delayTime, Action CallBack)
{
yield return new WaitForSeconds(delayTime);
CallBack?.Invoke();
}
}
저번 글에서 말했듯이 Player는 FSM 기반입니다. 그리고 저번 글에서 말하진 않았지만 Enemy도 FSM 기반입니다.
그렇기 때문에 State 변경 상태를 제어할 CanStateChangeable 변수를 공통으로 사용하고...
Animator는 FSM이 Animator 기반이기 때문에, Attack은 적과 플레이어 둘 다 해야하기 때문에 적어두었습니다.
그리고 FSM에서 혹시 딜레이가 필요할때 사용할 DelayCallback 함수도 만들어놓았습니다.
제가 만들 State들은 각각이 클래스고 MonoBehaviour를 상속하지 않기 때문에 StartCoroutine이 불가능합니다.
그러므로 Player에서 대신 돌려주는거죠.
아무튼 Agent는 대충 짰고 이제 Player로 넘어갑시다.
public class Player : Agent
Agent 상속해주고...
[Header("Setting Values")]
public float moveSpeed = 13f;
public float jumpPower = 0.3f;
public float crouchMoveSpeed = 6f;
public float slidingSpeed = 30f;
public float wallJumpPower = 0.32f;
public float wallRunSpeed = 16f;
[HideInInspector]
public float defaultMoveSpeed, defaultJumpPower;
필요한 변수들 정의 해주고...
[SerializeField]
private InputReader _playerInput;
public InputReader PlayerInput => _playerInput;
public PlayerMovement MovementCompo { get; private set; }
외부에서 접근이 필요한 것들은 프로퍼티화 해주고...
protected override void Awake()
{
base.Awake();
defaultMoveSpeed = moveSpeed;
defaultJumpPower = jumpPower;
MovementCompo = GetComponent<PlayerMovement>();
}
Awake에서 초기화 해주면 끝입니다.
이제 Player의 움직임으로 넘어가죠.
3D 환경에선 Rigidbody와 Collider를 쓰기보단 CharacterController를 많이 사용하곤 합니다.
Rigidbody가 쓸데없는 물리연산 같은걸로 오히려 Player의 원활한 움직임을 방해한다는 느낌을 받아서 그런데...
정확하진 않습니다. 제가 느낀바를 적는거니까요. 해골물일 수도 있죠.
아무튼 CharacterController 기반의 Player들은 귀찮게도 중력까지 손수 구현해주어야 합니다.
[SerializeField]
private float _gravity = -9.8f;
protected CharacterController _characterController;
Physics.gravity 값을 가져와도 되지만... 뭐 이렇게 해도 상관은 없습니다.
아무튼 중력을 가져와서
private void ApplyGravity()
{
if (IsGround && _verticalVelocity <= 0)
{
_verticalVelocity = -0.03f;
}
else
{
_verticalVelocity += _gravity * Time.fixedDeltaTime;
}
_velocity.y = _verticalVelocity;
}
땅에 닿을때까지 중력을 적용해주면 됩니다.
IsGround는 CharacterController.isGrounded로 내장 프로퍼티입니다.
편하긴 한데 정확하진 않다네요. 아직까지는 잘 모르겠습니다.
아무튼 이제 Player의 움직임을 처리하겠습니다.
일단 FPS다보니 마우스 움직임에 따라 Player가 바라보는 방향이 달라져야 합니다.
좌우로 움직일경우 Player의 몸체 자체가 회전하면 되고 상하로 움직일경우 Player의 머리, 그러니까 카메라만 회전하면 됩니다.
일단 InputReader 클래스에서 마우스의 움직인 정도를 받아올겁니다.
public Vector2 CamDelta { get; private set; }
public void OnCameraRotate(InputAction.CallbackContext context)
{
CamDelta = context.ReadValue<Vector2>();
}
접근할 수 있도록 프로퍼티화 시켜주고 이제 이에 맞게 회전만 시켜주면 됩니다.
public void SetPlayerRotate(Vector2 delta)
{
_cameraRotDelta.x = delta.y * _sensivityX * Time.deltaTime;
float rotateY = delta.x * _sensivityY * Time.deltaTime;
transform.Rotate(0, rotateY, 0);
}
private void CameraRotate()
{
SetPlayerRotate(_player.PlayerInput.CamDelta);
_xRotation -= _cameraRotDelta.x;
_xRotation = Mathf.Clamp(_xRotation, _upperLookLimit, _lowerLookLimit);
_playerMainCam.transform.localRotation = Quaternion.Euler(_xRotation, 0, _playerMainCam.transform.localEulerAngles.z);
}
delta, 그러니까 마우스가 저번 위치에서 얼마나 움직였는지 Vector값으로 받아온 후 X값 움직임은 Player의 Y축 회전으로, Y값 움직임은 카메라의 X축 회전으로 처리해주면 됩니다.
private void LateUpdate()
{
CameraRotate();
}
그 후 CameraRotate 함수는 디더링을 방지하기 위해 LateUpdate에서 실행해줍니다.
이제 Player의 움직임을 구현하면 됩니다.
public void SetMovement(Vector3 movement)
{
_movement = movement * Time.fixedDeltaTime;
}
SetMovement라는 함수를 거쳐서 _movement 변수에 움직일 값을 할당하고,
public Vector3 GetRotateVelocity()
{
Vector3 forward = transform.forward;
forward.y = 0;
Vector3 right = transform.right;
right.y = 0;
Vector3 velocity = forward * _movement.z + right * _movement.x;
velocity.y = _velocity.y;
return velocity;
}
Player가 바라보는 방향에 따른 방향 계산을 해주는 함수를 거치면...
private void FixedUpdate()
{
_velocity = GetRotateVelocity() * _player.moveSpeed;
ApplyGravity();
Move();
}
이런식으로 _velocity에 Player가 움직일 방향과 속도가 곱해진 벡터가 들어갑니다.
private void Move()
{
_characterController.Move(_velocity);
}
그 후 _velocity를 기반으로 움직여주면 됩니다.
...아주 쉽죠?
시행착오
저 바라보는 방향으로 이동하는 코드를 적을때 시행착오가 조금 있었습니다.
왠진 모르겠지만 카메라는 잘 돌아가는데 Player가 제 작동을 하지 않았죠.
_velocity 변수를 수정하는 과정에서 여러가지가 꼬인 것 같았습니다.
아무리 봐도 초기화 하는 순서문제 같았죠.
과거 코드를 보면 좀 가관인데 커밋을 안해서 기록은 없습니다.
아무튼 원인은 _velocity가 여기저기서 한번에 초기화 되면서 방향이 이리저리 튀는 것 이였습니다.
_velocity 초기화를 한 구간으로 줄이고 카메라 회전 로직을 단순화 시켰더니 해결됐죠.
마치는 말
프로젝트나 하지 왜 블로그 적냐구요?
그러게요? 원래 프로젝트 하기 싫으면 다른 게 무엇이든 하고 싶어지는 법 아니겠습니까?
마치 공부를 시작할때 책상 정리에만 30분을 쏟는 것과 같은 이치죠.
'개발 > UNNAMED' 카테고리의 다른 글
UNNAMED | 네번째 프로젝트 # 5 : 적 FSM (0) | 2024.06.08 |
---|---|
UNNAMED | 네번째 프로젝트 # 4 : 총 (0) | 2024.06.02 |
UNNAMED | 네번째 프로젝트 # 3 : Player FSM (0) | 2024.06.01 |
UNNAMED | 네번째 프로젝트 # 1 : 사전 준비 및 New Input System (0) | 2024.05.27 |
UNNAMED | 네번째 프로젝트 # 0 : 기획 (0) | 2024.05.24 |