이전 FSM 4편에서 플레이어의 움직임 그리고 달리기까지 만들었다면
이번에는 플레이어의 점프를 만들어 보도록 하자.
ForceReciver 를 통한 떨어짐 구현
ForceReciver는 힘을 받아 주는 역할을 하는 스크립트이며 여기서는 점프시의 중력값과 점프 힘 등을 받아주는 역할을 한다.
스크립트 생성
Player에 컴포넌트 추가
ForceReceiver.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ForceReceiver : MonoBehaviour
{
[SerializeField] private CharacterController controller;
[SerializeField] private float drag = 0.3f;
private Vector3 dampingVelocity;
private Vector3 impact;
private float verticalVelocity;
public Vector3 Movement => impact + Vector3.up * verticalVelocity; // 수직속도에 기타 영향을 줄 수 있는 impact를 더함
void Update()
{
// 땅체크 characterContorller에 내장되어있음
if (verticalVelocity < 0f && controller.isGrounded)
{
verticalVelocity = Physics.gravity.y * Time.deltaTime;
}
else
{
//땅이 아니면 계속 감소
verticalVelocity += Physics.gravity.y * Time.deltaTime;
}
// 저항값으로 차근히 감소
impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);
}
public void Reset()
{
impact = Vector3.zero;
verticalVelocity = 0f;
}
public void AddForce(Vector3 force)
{
impact += force;
}
public void Jump(float jumpForce)
{
verticalVelocity += jumpForce;
}
}
코드에서 Update문에서는 플레이어의 수직속도를 verticalVelocity라 정하여 땅에 있을 때 중력과 deltatime을 더한 계산값을 담아준다.
verticalVelocity 라는 값에 땅이 있을 때에는 초기화 시켜주는 이유가 초기화를 시켜주지 않는다면 땅이 아닐시에 값이 계속 감소하고 그 감소한 값이 유지가 되기 때문에 추후에 떨어질 때에는 점점 빨라지는 것을 볼 수 있다.
아래 비교 veticalVelocity를 땅에 있을 때 중력값 * deltatime으로 초기화 해줄 때 vs 초기화 하지 않을 때
- 초기화 해주는 모습
- 초기화 해주지 않는 모습
Vector3.SmothDamp()
그리고 smothDamp값으로 impact에 담아 차근히 감소시켜준다. Vector3.SmothDamp() 값은 impact의 벡터가 다음 매개변수인 Vector.zero의 값까지 부드럽게 감소하는데 이는 drag의 수치에 영향을 받아 점차 Vector.zero의 값까지 변화하게 된다.
drag의 값이 크면 클수록 변화속도가 느리게 감소되는것이 특징이다.
그리고 추후에 사용할 reset함수와 힘을 더 받는 addforce, jump의 함수들을 만들어준다.
물론 이렇게 한다고 아직 동작하는것은 아니다!! 그러니 안심하고 밑에 코드를 천천히 따라가도록 하자
동작하기 위해서는 PlayerBaseState에 가서 이동처리를 하는 Move에서 수정해주도록 하자.
그렇다면 플레이어 스크립트에 ForceReceiver를 생성해줘야 한다.
Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[field: Header("References")]
[field: SerializeField] public PlayerSO Data { get; private set; }
[field: Header("Animations")]
[field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }
public Rigidbody Rigidbody { get; private set; }
public Animator Animator { get; private set; }
public PlayerInput Input { get; private set; }
public CharacterController Controller { get; private set; }
public ForceReceiver ForceReceiver { get; private set; }
public PlayerUI PlayerUI { get; private set; }
private PlayerStateMachine stateMachine;
private PlayerData playerData;
private void Awake()
{
//Debug.Log(Data.GetPlayerName());
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Input = GetComponent<PlayerInput>();
Controller = GetComponent<CharacterController>();
// 추가 수정된 부분 ForceReciver 컴포넌트 추가
ForceReceiver = GetComponent<ForceReceiver>();
PlayerUI = GetComponentInChildren<PlayerUI>();
stateMachine = new PlayerStateMachine(this);
}
private void Start()
{
Cursor.lockState = CursorLockMode.Locked;
stateMachine.ChangeState(stateMachine.IdleState);
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
}
PlayerBaseState.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
public class PlayerBaseState : IState
{
protected PlayerStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
public PlayerBaseState(PlayerStateMachine playerStateMachine)
{
stateMachine = playerStateMachine;
groundData = stateMachine.Player.Data.GroundedData;
}
public virtual void Enter()
{
AddInputActionsCallbacks();
}
public virtual void Exit()
{
RemoveInputActionsCallbacks();
}
public virtual void HandleInput()
{
ReadMovementInput();
}
public virtual void PhysicsUpdate()
{
}
public virtual void Update()
{
Move();
}
protected virtual void AddInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled += OnMovementCanceled;
input.PlayerActions.Run.started += OnRunStarted;
}
protected virtual void RemoveInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled -= OnMovementCanceled;
input.PlayerActions.Run.started -= OnRunStarted;
}
protected virtual void OnRunStarted(InputAction.CallbackContext context)
{
}
protected virtual void OnMovementCanceled(InputAction.CallbackContext context)
{
}
//
private void ReadMovementInput()
{
stateMachine.MovementInput = stateMachine.Player.Input.PlayerActions.Movement.ReadValue<Vector2>();
}
private void Move()
{
Vector3 movementDirection = GetMovementDirection();
Rotate(movementDirection);
Move(movementDirection);
}
private Vector3 GetMovementDirection()
{
Vector3 forward = stateMachine.MainCameraTransform.forward;
Vector3 right = stateMachine.MainCameraTransform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x;
}
//수정된 부분
private void Move(Vector3 movementDirection)
{
float movementSpeed = GetMovemenetSpeed();
stateMachine.Player.Controller.Move(
((movementDirection * movementSpeed) + stateMachine.Player.ForceReceiver.Movement)* Time.deltaTime
);
}
private void Rotate(Vector3 movementDirection)
{
if (movementDirection != Vector3.zero)
{
Transform playerTransform = stateMachine.Player.transform;
Quaternion targetRotation = Quaternion.LookRotation(movementDirection);
playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
}
}
private float GetMovemenetSpeed()
{
float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
return movementSpeed;
}
protected void StartAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, true);
}
protected void StopAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, false);
}
}
아래 코드가 수정된 부분이다.
deltaTime을 곱하기 이전에 ForceReciver에서 verticalVelocity 와 impact의 값을 더한 것이 바로 movement인데 이 값을 이동하는 값에 더해주는 것이다.
정리하자면 이동하는 값과 ForceReciver에서 넘어온 힘을 합친 값에 deltatime의 값을 받아 떨어지게 된다.
private void Move(Vector3 movementDirection)
{
float movementSpeed = GetMovemenetSpeed();
stateMachine.Player.Controller.Move(
((movementDirection * movementSpeed) + stateMachine.Player.ForceReceiver.Movement)* Time.deltaTime
);
}
캐릭터 점프 구현
다음은 캐릭터 점프 구현이다.
먼저 스크립트 세 가지 PlayerAirState, PlayerJumpState, PlayerFallState를 만들어준다.
FallState는 추후에 따로 적용해주기 위해서 생성하도록 한다.
PlayerAirState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAirState : PlayerBaseState
{
public PlayerAirState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.AirParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.AirParameterHash);
}
}
PlayerBaseState를 상속받아 애니매이션을 켜주고 꺼주는 역할을 담당한다.
그렇다면 Airstate를 상속받아야 하는 JumpState와 FallState를 작성해주도록 하자.
PlayerAirState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerJumpState : PlayerAirState
{
public PlayerJumpState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
stateMachine.JumpForce = stateMachine.Player.Data.AirData.JumpForce; // AirData의 JumpForce값을 담아주고
stateMachine.Player.ForceReceiver.Jump(stateMachine.JumpForce); //ForceReveiver에 Jump 함수에 전달해준다.
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
// 점프할때는 값이 크게 들어갔다가 떨어질 때에는 0보다 작은 값으로 떨어지고있기 때문에
// ChageState를 해준다.
if(stateMachine.Player.Controller.velocity.y <= 0)
{
stateMachine.ChangeState(stateMachine.FallState);
return;
}
}
}
Enter함수에서는 Data에 있는 Jump값을 담아주고, 이를 점프 함수에 전달해준다.
PhysicsUpdate에서는 점프할 때 값이 들어갔다가 떨어지면서 0으로 바뀌는 조건을 줘서 이 경우에는 State가 FallState로 바뀌게 해주도록 한다.
PlayerFallState.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PlayerFallState : PlayerAirState
{
public PlayerFallState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.FallParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.FallParameterHash);
}
public override void Update()
{
base.Update();
if(stateMachine.Player.Controller.isGrounded)
{
stateMachine.ChangeState(stateMachine.IdleState);
return;
}
}
}
그렇다면 FallState도 마찬가지로 애니메이션을 켜주고 꺼주는 작업을 해주고, Update에서 isGrounded로 땅을 체크한 후 땅에 닿았다면 IdleState로 다시 바꿔주도록 한다.
이제 이렇게 만들어둔 클래스들을 등록을 해줘야 하기 때문에 어디로 가야한다? PlayerStateMachine으로가서 등록을 해줘야 한다.
PlayerStateMachine.cs 수정
using System.Collections;
using System.Collections.Generic;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;
public class PlayerStateMachine : StateMachine
{
public Player Player { get; }
public PlayerIdleState IdleState { get; }
public PlayerWalkState WalkState { get; }
public PlayerRunState RunState { get; }
// JumpState와 FallState추가
public PlayerJumpState JumpState { get; }
public PlayerFallState FallState { get; }
public Vector2 MovementInput { get; set; }
public float MovementSpeed { get; private set; }
public float RotationDamping { get; private set; }
public float MovementSpeedModifier { get; set; } = 1f;
public float JumpForce { get; set; }
public Transform MainCameraTransform { get; set; }
public PlayerStateMachine(Player player)
{
this.Player = player;
IdleState = new PlayerIdleState(this);
WalkState = new PlayerWalkState(this);
RunState = new PlayerRunState(this);
// 추가한 내용 생성
JumpState = new PlayerJumpState(this);
FallState = new PlayerFallState(this);
MainCameraTransform = Camera.main.transform;
MovementSpeed = player.Data.GroundedData.BaseSpeed;
RotationDamping = player.Data.GroundedData.BaseRotationDamping;
}
}
이제는 순환이 다 되는 상태이고 이제 점프를 언제 해야하느냐가 중요하다.
점프는 언제하는가? 점프는 땅에 있을 때 점프를 하게된다.
그렇기 때문에 BaseState에서 Jump함수를 만들어주고, InputCallbacks 함수들에 구독을 해주고 해지를 해준다.
PlayerBaseState.cs 수정
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
public class PlayerBaseState : IState
{
protected PlayerStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
public PlayerBaseState(PlayerStateMachine playerStateMachine)
{
stateMachine = playerStateMachine;
groundData = stateMachine.Player.Data.GroundedData;
}
public virtual void Enter()
{
AddInputActionsCallbacks();
}
public virtual void Exit()
{
RemoveInputActionsCallbacks();
}
public virtual void HandleInput()
{
ReadMovementInput();
}
public virtual void PhysicsUpdate()
{
}
public virtual void Update()
{
Move();
}
protected virtual void AddInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled += OnMovementCanceled;
input.PlayerActions.Run.started += OnRunStarted;
//jump 구독 내용 추가 수정
input.PlayerActions.Jump.started += OnJumpStarted;
}
protected virtual void RemoveInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled -= OnMovementCanceled;
input.PlayerActions.Run.started -= OnRunStarted;
//jump 구독해지 내용 추가 수정
input.PlayerActions.Jump.started -= OnJumpStarted;
}
protected virtual void OnRunStarted(InputAction.CallbackContext context)
{
}
protected virtual void OnMovementCanceled(InputAction.CallbackContext context)
{
}
// 메서드 생성 추가, 이는 GroundState가 상속받아서 사용할 예정
protected virtual void OnJumpStarted(InputAction.CallbackContext context)
{
}
//
private void ReadMovementInput()
{
stateMachine.MovementInput = stateMachine.Player.Input.PlayerActions.Movement.ReadValue<Vector2>();
}
private void Move()
{
Vector3 movementDirection = GetMovementDirection();
Rotate(movementDirection);
Move(movementDirection);
}
private Vector3 GetMovementDirection()
{
Vector3 forward = stateMachine.MainCameraTransform.forward;
Vector3 right = stateMachine.MainCameraTransform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x;
}
private void Move(Vector3 movementDirection)
{
float movementSpeed = GetMovemenetSpeed();
stateMachine.Player.Controller.Move(
((movementDirection * movementSpeed) + stateMachine.Player.ForceReceiver.Movement)* Time.deltaTime
);
}
private void Rotate(Vector3 movementDirection)
{
if (movementDirection != Vector3.zero)
{
Transform playerTransform = stateMachine.Player.transform;
Quaternion targetRotation = Quaternion.LookRotation(movementDirection);
playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
}
}
private float GetMovemenetSpeed()
{
float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
return movementSpeed;
}
protected void StartAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, true);
}
protected void StopAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, false);
}
}
그리고 땅에서 점프를 할 수 있기 때문에 GroundState를 수정해주도록 하자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerGroundState : PlayerBaseState
{
public PlayerGroundState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
}
public override void Update()
{
base.Update();
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
}
// 입력키가 떼졌을 때의 동작이다. ground에서 해주는 이유는 ground가 아닌 다른 state에 있을 때의 키입력이 없는 경우는 또 다른 동작을 해야하기 때문
protected override void OnMovementCanceled(InputAction.CallbackContext context)
{
if (stateMachine.MovementInput == Vector2.zero)
{
return;
}
stateMachine.ChangeState(stateMachine.IdleState);
base.OnMovementCanceled(context);
}
//Jump는 JumpState에서 다 처리하고 있기 때문에 상태만 변환해준다.
protected override void OnJumpStarted(InputAction.CallbackContext context)
{
stateMachine.ChangeState(stateMachine.JumpState);
}
protected virtual void OnMove()
{
stateMachine.ChangeState(stateMachine.WalkState);
}
}
점프 완성!
'Unity' 카테고리의 다른 글
[JSON] JSON을 제네릭으로 받아오기 및 실수 기록 (문.시.해.알) (2) | 2024.01.17 |
---|---|
JSON 을 받아오지 못하는 오류 + FSM 대쉬 구현중 쿨타임 오류 (1) | 2024.01.15 |
[FSM] 4. 플레이어 이동 상태 만들기 (1) | 2024.01.11 |
디자인 패턴 2 (0) | 2024.01.10 |
[FSM] 플레이어 상태머신 3 플레이어 상태 만들기 (0) | 2024.01.09 |