๋ชจ๋ํ์ ํ์์ฑ์ ๋๋ผ๋ค
๊ธฐ์กด PlayerController ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ค๋ฉด, ๊ธฐ์กด ์์ค์ฝ๋๋ฅผ ์์ ํด์ผ ํด์ ํ์ฅ์ด ์ด๋ ค์ ์ต๋๋ค.
์ ํฌ ๊ฒ์์๋ ๋์, ๋์ ๊ณต๊ฒฉ, ์ฐจ์ง ๊ณต๊ฒฉ, ๊ธฐ๋ณธ ๊ณต๊ฒฉ ์ฝค๋ณด 3ํ ๋ฑ๋ฑ ํ ์ํ์์ ๋ค๋ฅธ ์ํ๋ก ๋ถ๊ธฐ๋๋ ๋์๋ค์ด ๋ง์์ต๋๋ค. ๊ทธ๋์ ์ง๊ธ ์ฝ๋ ๊ตฌ์กฐ๋ก ๊ณ์ ํ์ฅํ๋ค๊ฐ๋ ์ธ๊ณ ์ ์ผ์ ์คํ๊ฒํฐ๋ฅผ ๋ง๋ค ๊ฒ ๊ฐ์์ ๊ตฌ์กฐ ๋ณ๊ฒฝ์ด ํ์ํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
์ด์ ์ ๋์์ธ ํจํด(Degisn Pattern)๋ค์ ๋ณด๋ ์ค, ์ํ ํจํด(State Pattern)์ ์ ์ฉํ๋ฉด Controller์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๊ฒ ๊ต์ฅํ ์ฌ์ธ ๊ฒ ๊ฐ์์ต๋๋ค. ์ ๊ฐ ์ด๋ค ์์ผ๋ก ๊ตฌ์กฐ๋ฅผ ๋ฐ๊พธ์๋์ง ์๋์์ ๋ณด๋ฉด์ ์ฒ์ฒํ ์ค๋ช ํด ๋๋ฆด๊ฒ์.
1. ๋ชจ๋ ์ํ๋ค์ ๊ธฐ์์ด ๋๋ ๋ถ๋ชจ ์ถ์ ํด๋์ค, BaseState ์ ์ํ๊ธฐ
๊ฐ ์ํ๋ค์ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ๋ค์ ํ์๋ก ํฉ๋๋ค.
- ํ์ฌ ์ํ์์ ํ๋์ ์ฃผ์ฒด๊ฐ ๋๋ Controller
- ์ํ์ ์ง์ ํ์ ๋, ์คํ๋๋ OnEnterState()
- ํ์ฌ ์ํ์์ ๊ณ์ ๊ฐฑ์ ๋์ด์ผ ํ๋ ์ ๋ณด๋ฅผ ์ ๋ฐ์ดํธํ๋ OnUpdateState()
- ํ์ฌ ์ํ์์ ๋ฌผ๋ฆฌ์ ๊ด๋ จํ์ฌ ์ ๋ฐ์ดํธํ๋ OnFixedUpdate()
- ํ์ฌ ์ํ๋ฅผ ์ข ๋ฃํ ๋, ์คํ๋๋ OnExitState()
์์ ๊ฐ์ด ์ ๋ฆฌ๋ฅผ ํ๊ณ , ๋ค์๊ณผ ๊ฐ์ด ์ถ์ ํด๋์ค๋ฅผ ์ ์ํด ์ฃผ์์ต๋๋ค.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace CharacterController
{
public abstract class BaseState
{
protected PlayerController Controller { get; private set; }
public BaseState(PlayerController controller)
{
this.Controller = controller;
}
public abstract void OnEnterState();
public abstract void OnUpdateState();
public abstract void OnFixedUpdateState();
public abstract void OnExitState();
}
}
์ด์ ์ด BaseState ํด๋์ค๋ฅผ ์์ ๋ฐ์ ์ด๋(Move), ๋์(Dash), ๊ณต๊ฒฉ(Attack)๊ณผ ๊ฐ์ ๊ฐ ์ํ ํด๋์ค๋ฅผ ๋ง๋ค์ด์ ์ถ์ ๋ฉ์๋๋ค์ ๊ตฌํํ๋ฉด ๋๊ฒ ๋ค์! ๊ฐ ์ํ๋ค์ ๊ตฌํํ๋ ๊ฑด ๋ค์ ํ๊ธฐ๋ก ํ๊ณ , ์ด์ ์ํฉ์ ๋ฐ๋ผ ์ํ๋ฅผ ์ ํ์์ผ์ฃผ๋ ํด๋์ค์ธ StateMachine์ด ํ์ํฉ๋๋ค.
2. StateMachine ํด๋์ค ์ ์ํ๊ธฐ
StateMachine ํด๋์ค๋ ํ์ํ ์ํ๋ค์ Dictionary๋ก ๊ด๋ฆฌํ๊ฒ ๋ฉ๋๋ค.
namespace CharacterController
{
public enum StateName
{
MOVE = 100,
DASH,
ATTACK,
}
}
using System.Collections.Generic;
using System.Diagnostics;
namespace CharacterController
{
public class StateMachine
{
public BaseState CurrentState { get; private set; } // ํ์ฌ ์ํ
private Dictionary<StateName, BaseState> states =
new Dictionary<StateName, BaseState>();
public StateMachine(StateName stateName, BaseState state)
{
AddState(stateName, state);
CurrentState = GetState(stateName);
}
public void AddState(StateName stateName, BaseState state) // ์ํ ๋ฑ๋ก
{
if (!states.ContainsKey(stateName))
{
states.Add(stateName, state);
}
}
public BaseState GetState(StateName stateName) // ์ํ ๊บผ๋ด์ค๊ธฐ
{
if (states.TryGetValue(stateName, out BaseState state))
return state;
return null;
}
public void DeleteState(StateName removeStateName) // ์ํ ์ญ์
{
if (states.ContainsKey(removeStateName))
{
states.Remove(removeStateName);
}
}
public void ChangeState(StateName nextStateName) // ์ํ ์ ํ
{
CurrentState?.OnExitState(); //ํ์ฌ ์ํ๋ฅผ ์ข
๋ฃํ๋ ๋ฉ์๋๋ฅผ ์คํํ๊ณ ,
if (states.TryGetValue(nextStateName, out BaseState newState)) // ์ํ ์ ํ
{
CurrentState = newState;
}
CurrentState?.OnEnterState(); // ๋ค์ ์ํ ์ง์
๋ฉ์๋ ์คํ
}
public void UpdateState()
{
CurrentState?.OnUpdateState();
}
public void FixedUpdateState()
{
CurrentState?.OnFixedUpdateState();
}
}
}
์ด์ ์ค๋น๋ ๋ค ํ์ผ๋, Player์ PlayerController ํด๋์ค๋ฅผ ์กฐ๊ธ ์ ๋ฆฌํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
3. Player์ PlayerController ํด๋์ค ์ ๋ฆฌํ๊ธฐ
Player ํด๋์ค
Player ํด๋์ค๋ ์ฑ๊ธํค(Singleton) ํจํด์ผ๋ก ๊ตฌํ๋์ด ์์ต๋๋ค. ๊ฒ์ํ๋ ๋ด๋ด ๋ฐ์ดํฐ๊ฐ ์ ์ง๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ด์ฃ .
์ฑ๊ธํค์ด๊ธฐ์ ๋ค๋ฅธ ํด๋์ค ์ด๋์์๋ ์ ์ญ์ ์ผ๋ก ์ ๊ทผํ ์ ์์ต๋๋ค. ๊ทธ๋์ ๊ฐ ์ํ ํด๋์ค์์ ํ๋์ ๊ตฌํํ๋๋ฐ ํ์ํ ์ปดํฌ๋ํธ ๋ฐ ํด๋์ค๋ค์ ์ฌ๊ธฐ์ ๋์์ต๋๋ค. Rigidbody, Animator, StateMachine ๋ฑ๋ฑ ๋ง์ด์ฃ . ๋ค๋ง ์ธ๋ถ์์ ์์ ์ ํจ๋ถ๋ก ๋ชปํ๊ฒ ์ฝ๊ธฐ ์ ์ฉ ํ๋กํผํฐ๋ก ๋์์ต๋๋ค.
์ด์ Player ํด๋์ค๋ ๋ง ๊ทธ๋๋ก ํ๋ ์ด์ด์ ์ ๋ณด๋ค๋ง ๋ด๊ณ ์๋ ํด๋์ค๊ฐ ๋ ๊ฒ์ ๋๋ค.
*์ํ ํจํด ์งํํ๊ณ , ์ด๊ฒ ์ ๊ฒ ์ข ๋ง์ด ๋ง๋ค๊ณ ์์ ํ ํ์ ๊ธ ์ฐ๋ ๊ฑฐ๋ผ ํ์ํ ๋ถ๋ถ๋ง ๊ฑธ๋ฌ๋ด๊ธฐ๊ฐ ํ๋๋ค์..ใทใท
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using CharacterController;
public class Player : MonoBehaviour
{
public static Player Instance { get { return instance; } }
public StateMachine stateMachine { get; private set; }
public Rigidbody rigidBody { get; private set; }
public Animator animator { get; private set; }
public CapsuleCollider capsuleCollider { get; private set; }
private static Player instance;
#region #์บ๋ฆญํฐ ์คํฏ
public float MaxHP { get { return maxHP; } }
public float CurrentHP { get { return currentHP; } }
public float Armor { get { return armor; } }
public float MoveSpeed { get { return moveSpeed; } }
public int DashCount { get { return dashCount; } }
[Header("์บ๋ฆญํฐ ์คํฏ")]
[SerializeField] protected float maxHP;
[SerializeField] protected float currentHP;
[SerializeField] protected float armor;
[SerializeField] protected float moveSpeed;
[SerializeField] protected int dashCount;
#endregion
#region #Unity ํจ์
void Awake()
{
if(instance == null)
{
instance = this;
rigidBody = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
capsuleCollider = GetComponent<CapsuleCollider>();
DontDestroyOnLoad(gameObject);
return;
}
DestroyImmediate(gameObject);
}
void Start()
{
InitStateMachine();
}
void Update()
{
stateMachine?.UpdateState();
}
void FixedUpdate()
{
stateMachine?.FixedUpdateState();
}
#endregion
public void OnUpdateStat(float maxHP, float currentHP, float armor, float moveSpeed, int dashCount)
{
this.maxHP = maxHP;
this.currentHP = currentHP;
this.armor = armor;
this.moveSpeed = moveSpeed;
this.dashCount = dashCount;
}
private void InitStateMachine()
{
// ์์ง ์ํ๋ค์ ์ ๋ง๋ค์์ผ๋, ๋ง๋ค๊ณ ์ฌ๊ธฐ์์ ๋ฑ๋กํ๋๋ก ํฉ์๋ค.
...
}
}
๊ฐ ์ํ์์ ์ ๋ฐ์ดํธ ๋์ด์ผ ํ๋ ๋ถ๋ถ๋ค์ ๊ฐ Unity ๋ด์ฅ ํจ์๋ค์์ ์คํ์์ผ ์ฃผ์์ต๋๋ค.
void Update()
{
stateMachine?.UpdateState();
}
void FixedUpdate()
{
stateMachine?.FixedUpdateState();
}
PlayerController ํด๋์ค
PlayerController ํด๋์ค๋ ๊ธฐ์กด์๋ ์ ๋ ฅ๋ ๋ฐ๊ณ , ๊ฑฐ๊ธฐ์ ๋ฐ๋ฅธ ํ๋๋ ์ํํ๊ธฐ ๋๋ฌธ์ ํด๋์ค ๊ท๋ชจ๊ฐ ์ข ์ปธ์ต๋๋ค.
์ด๋, ๋์ ๋์์ ์ํํ๋ ๋ถ๋ถ์ ์ฝ๋๋ค๋ ์ฌ๊ธฐ์ ์์๊ธฐ ๋๋ฌธ์ด์ฃ . ์ด์ PlayerController ํด๋์ค๋ ๋์์ ์ํํ์ง ์๊ณ , ์ฌ์ฉ์์ ์ ๋ ฅ ์ ๋ณด๋ง ๋ฐ์ ๊ด๋ฆฌํฉ๋๋ค.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Interactions;
using CharacterController;
[RequireComponent(typeof(Player))]
public class PlayerController : MonoBehaviour
{
public Player player { get; private set; }
public Vector3 inputDirection { get; private set; } // ํค๋ณด๋ ์
๋ ฅ์ผ๋ก ๋ค์ด์จ ์ด๋ ๋ฐฉํฅ
public Vector3 calculatedDirection { get; private set; } // ๊ฒฝ์ฌ ์งํ ๋ฑ์ ๊ณ์ฐํ ๋ฐฉํฅ
public Vector3 gravity { get; private set; }
#region #๊ฒฝ์ฌ ์ฒดํฌ ๋ณ์
[Header("๊ฒฝ์ฌ ์งํ ๊ฒ์ฌ")]
[SerializeField, Tooltip("์บ๋ฆญํฐ๊ฐ ๋ฑ๋ฐํ ์ ์๋ ์ต๋ ๊ฒฝ์ฌ ๊ฐ๋์
๋๋ค.")]
float maxSlopeAngle;
[SerializeField, Tooltip("๊ฒฝ์ฌ ์งํ์ ์ฒดํฌํ Raycast ๋ฐ์ฌ ์์ ์ง์ ์
๋๋ค.")]
Transform raycastOrigin;
private const float RAY_DISTANCE = 2f;
private RaycastHit slopeHit;
private bool isOnSlope;
#endregion
#region #๋ฐ๋ฅ ์ฒดํฌ ๋ณ์
[Header("๋
์ฒดํฌ")]
[SerializeField, Tooltip("์บ๋ฆญํฐ๊ฐ ๋
์ ๋ถ์ด ์๋์ง ํ์ธํ๊ธฐ ์ํ CheckBox ์์ ์ง์ ์
๋๋ค.")]
Transform groundCheck;
private int groundLayer;
private bool isGrounded;
#endregion
#region #UNITY_FUNCTIONS
void Start()
{
player = GetComponent<Player>();
groundLayer = 1 << LayerMask.NameToLayer("Ground");
}
void Update()
{
calculatedDirection = GetDirection(player.MoveSpeed * MoveState.CONVERT_UNIT_VALUE);
ControlGravity();
}
#endregion
public void OnDashInput(InputAction.CallbackContext context)
{
if (context.performed)
{
// ์ํ๋ฅผ ์ถ๊ฐํ ํ, ์ฌ๊ธฐ์๋ ์ํ ์ ํ๋ง ํด์ฃผ๋๋ก ํฉ๋๋ค.
}
}
protected Vector3 GetMouseWorldPosition()
{
Vector3 mousePosition = Mouse.current.position.ReadValue();
Ray ray = Camera.main.ScreenPointToRay(mousePosition);
Debug.DrawRay(ray.origin, ray.direction * 1000f, Color.red, 5f);
if (Physics.Raycast(ray, out RaycastHit HitInfo, Mathf.Infinity))
{
Vector3 target = HitInfo.point;
Vector3 myPosition = new Vector3(transform.position.x, 0f, transform.position.z);
target.Set(target.x, 0f, target.z);
return (target - myPosition).normalized;
}
return Vector3.zero;
}
protected Vector3 GetDirection(float currentMoveSpeed)
{
isOnSlope = IsOnSlope();
isGrounded = IsGrounded();
Vector3 calculatedDirection =
CalculateNextFrameGroundAngle(currentMoveSpeed) < maxSlopeAngle ?
inputDirection : Vector3.zero;
calculatedDirection = (isGrounded && isOnSlope) ?
AdjustDirectionToSlope(calculatedDirection) : calculatedDirection.normalized;
return calculatedDirection;
}
protected void ControlGravity()
{
gravity = Vector3.down * Mathf.Abs(player.rigidBody.velocity.y);
if (isGrounded && isOnSlope)
{
gravity = Vector3.zero;
player.rigidBody.useGravity = false;
return;
}
player.rigidBody.useGravity = true;
}
private float CalculateNextFrameGroundAngle(float moveSpeed)
{
// ๋ค์ ํ๋ ์ ์บ๋ฆญํฐ ์ ๋ถ๋ถ ์์น
Vector3 nextFramePlayerPosition =
raycastOrigin.position + inputDirection * moveSpeed * Time.fixedDeltaTime;
if (Physics.Raycast(nextFramePlayerPosition, Vector3.down,
out RaycastHit hitInfo, RAY_DISTANCE, groundLayer))
{
return Vector3.Angle(Vector3.up, hitInfo.normal);
}
return 0f;
}
public bool IsGrounded()
{
Vector3 boxSize = new Vector3(transform.lossyScale.x, 0.4f, transform.lossyScale.z);
return Physics.CheckBox(groundCheck.position, boxSize, Quaternion.identity,
groundLayer);
}
public bool IsOnSlope()
{
Ray ray = new Ray(transform.position, Vector3.down);
if (Physics.Raycast(ray, out slopeHit, RAY_DISTANCE, groundLayer))
{
var angle = Vector3.Angle(Vector3.up, slopeHit.normal);
return angle != 0f && angle < maxSlopeAngle;
}
return false;
}
public void OnMoveInput(InputAction.CallbackContext context)
{
Vector2 input = context.ReadValue<Vector2>();
inputDirection = new Vector3(input.x, 0f, input.y);
}
public void LookAt(Vector3 direction)
{
if (direction != Vector3.zero)
{
Quaternion targetAngle = Quaternion.LookRotation(direction);
transform.rotation = targetAngle;
}
}
protected Vector3 AdjustDirectionToSlope(Vector3 direction)
{
Vector3 adjustVelocityDirection =
Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
return adjustVelocityDirection;
}
}
์ฐ๋ฆฌ๊ฐ ์ด์ ๊ธ๋ค์์ ๋ง๋ค์๋ ๊ฒฝ์ฌ๋ก ์งํ ๊ฒ์ฌ, ๋ ์ ๋ถ์ด ์๋์ง ๊ฒ์ฌ, ์ค๋ ฅ ์ ์ฉ์ ์ฌ๊ธฐ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ฉ๋๋๋ก ๋์์ต๋๋ค. ์บ๋ฆญํฐ์ PlayerController ์คํฌ๋ฆฝํธ๋ง ๋ถ์ด๋ฉด ์๋์ ์ผ๋ก ํด๋น ๊ธฐ๋ฅ๋ค์ด ์ง์๋๋๋ก ๋ง์ด์ฃ .
๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์์ ์ ๋ ฅ์ ๋ฐ์์ ๋ฐ์ ์ ๋ณด๋ค๋ง ๊ฐ์ง๊ณ ์๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค. ์ด์ ์ด๋, ๋์์ ๊ฐ์ ์ํ๋ฅผ ์ ์ํ์ฌ ๊ฑฐ๊ธฐ์์ ํ๋์ ๊ตฌํํ๋, ํ๋์ ํ์ํ ์ ๋ณด๋ ์ฌ๊ธฐ์์ ๊ฐ์ ธ์ฌ ๊ฒ๋๋ค.
4. ์ํ ์ ์ํ์ฌ ๋ฑ๋กํ๊ธฐ
์ด๋ ์ํ(MoveState) ์ ์ํ์ฌ ๋ฑ๋กํ๊ธฐ
์ฐ๋ฆฌ๊ฐ ๊ธฐ์กด์ PlayerController์์ ์ด๋์ ํ์ํ๋ ์ ๋ณด๋ค ์์ฃ ? ๊ทธ๊ฑธ ์ด ํด๋์ค์์ ์ ์ธํ๊ณ ๋๊ฐ์ด ์ ์ด์ค ๊ฒ๋๋ค.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.Experimental.GraphView.GraphView;
namespace CharacterController
{
public class MoveState : BaseState
{
public const float CONVERT_UNIT_VALUE = 0.01f;
public const float DEFAULT_CONVERT_MOVESPEED = 3f;
public const float DEFAULT_ANIMATION_PLAYSPEED = 0.9f;
private int hashMoveAnimation;
public MoveState(PlayerController controller) : base(controller)
{
hashMoveAnimation = Animator.StringToHash("Velocity");
}
protected float GetAnimationSyncWithMovement(float changedMoveSpeed)
{
if (Controller.inputDirection == Vector3.zero)
{
return -DEFAULT_ANIMATION_PLAYSPEED;
}
// (๋ฐ๋ ์ด๋ ์๋ - ๊ธฐ๋ณธ ์ด๋์๋) * 0.1f
return (changedMoveSpeed - DEFAULT_CONVERT_MOVESPEED) * 0.1f;
}
public override void OnEnterState()
{
// ํ์์๋ ๋ถ๋ถ์ด์ง๋ง, ์ถ์ ๋ฉ์๋๋ ๊ตฌํํด์ผ ํ๋ฏ๋ก ๋น์ด ๋๋ค.
}
public override void OnUpdateState()
{
// ํ์์๋ ๋ถ๋ถ์ด์ง๋ง, ์ถ์ ๋ฉ์๋๋ ๊ตฌํํด์ผ ํ๋ฏ๋ก ๋น์ด ๋๋ค.
}
// ์ด๋์ Rigidbody ๊ธฐ๋ฐ์ด๋ฏ๋ก, FixedUpdate()์์ ๊ตฌํํด์ค๋๋ค.
public override void OnFixedUpdateState()
{
float currentMoveSpeed = Controller.player.MoveSpeed * CONVERT_UNIT_VALUE;
float animationPlaySpeed = DEFAULT_ANIMATION_PLAYSPEED + GetAnimationSyncWithMovement(currentMoveSpeed);
Controller.LookAt(Controller.inputDirection);
Player.Instance.rigidBody.velocity = Controller.calculatedDirection * currentMoveSpeed + Controller.gravity;
Player.Instance.animator.SetFloat(hashMoveAnimation, animationPlaySpeed);
}
// ์ด๋ ์ํ๋ฅผ ์ข
๋ฃํ ๋๋ ์ ๋๋ฉ์ด์
๊ณผ ๋ฌผ๋ฆฌ ์๋๋ฅผ ์ด๊ธฐํ ํด์ค์ผ ํ๋ค.
public override void OnExitState()
{
Player.Instance.animator.SetFloat(hashMoveAnimation, 0f);
Player.Instance.rigidBody.velocity = Vector3.zero;
}
}
}
PlayerController์์ ์ด๋ ๊ตฌํํ๋ ๊ฑธ ๊ทธ๋๋ก ๊ฐ์ ธ์์ ์ ์ ๊ฒ๋๋ค. ์ด์ ์ด๊ฑธ StateMachine์ ์ํ๋ฅผ ๋ฑ๋กํ์ฌ ์ค์๋ค.
// Player ํด๋์ค
private void InitStateMachine()
{
PlayerController controller = GetComponent<PlayerController>();
stateMachine = new StateMachine(StateName.MOVE, new MoveState(controller)); // ๋ฑ๋ก
}
์ด๋ ๊ฒ ๋ฑ๋กํด์คฌ์ผ๋ฉด ์ด์ ๋์ ๋๋ค. ์คํํด์ ํ ๋ฒ ์ ๋๋์ง ํ ์คํธ ํด๋ณด์ฃ .
๋น์ฐํ ๊ธฐ๋ฅ์ ์ผ๋ก ๋ณํ ๊ฑด ์์ต๋๋ค. ๋จ์ง ์ ์ง๋ณด์ ๋ฐ ํ์ฅ์ ์ฉ์ดํ๊ฒ ํ๊ธฐ ์ํด ์ ์ฉํ๋ ๊ฒ์ด์ง์. ๋์๋ ํ ๋ฒ ๋๊ฐ์ด ์ ์ฉํ์ฌ ๋ด ์๋ค.
๋์ ์ํ(DashState) ์ ์ํ์ฌ ๋ฑ๋กํ๊ธฐ
DashState ํด๋์ค๋ ์ ์ํ์ฌ ํ๋์ ๊ตฌํํด์ฃผ๋๋ก ํฉ์๋ค.
์ฝ๋๋ฅผ ์ฐ๋ค๋ณด๋, ๋์(Dash)์ ํ์ํ ๋ณ์๋ค์ด ์ ๋ ๊ฒ ๋ง๋๊ตฐ์. ์ ๊ฒ ๋ค PlayerController ํด๋์ค ์์ ์์์ผ๋ ์ผ๋ง๋ ํด๋์ค๊ฐ ํ๋ค์๊ฒ ์ต๋๊น... ์, ๊ทธ๋ฆฌ๊ณ Animator์ Apply RootMotion์ ๋์ ์ค์ผ ๋๋ ๋๊ณ ํ์ ์์๋ ์ผ๋ ๊ฑธ๋ก ๊ตฌํํด์ฃผ์์ต๋๋ค. ๋์ค์ ๊ฒ์ํ ๊ณต๊ฒฉ ์ ๋๋ฉ์ด์
๋ค์ด RootMotion์ด ํ์ํ ์ ๋ค์ด๋ผ ์ด๋ ๊ฒ ์กฐ์น๋ฅผ ์ทจํ์ด์!
์ด์ PlayerController์์ ๋์ ์
๋ ฅ์ ๋ฐ์ผ๋ฉด ๋์ ์ํ๋ก ์ ํํด์ฃผ๋ ์ฝ๋๋ง ์ถ๊ฐํด์ฃผ๋ฉด ๋๊ฒ ๋ค์.
N๋จ ๋์๋ ๊ตฌํ์ ํด์ค์ผ ํ๋๋ฐ, ์ ๋ ์๊ธฐ ์ํ์์ ์๊ธฐ ์ํ๋ก์ ์ ํ์ ๋ง์๋์ง ์์์ต๋๋ค.
๋์ ์
๋ ฅ์์ ์กฐ๊ฑด์ ๋ณด๊ณ ๋ง์กฑํ๋ฉด ์ํ๋ฅผ ์ ํํ๋๋ก ํด๋จ์ฃ .
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด isAvailableDash ๋ณ์์ ๋ด์๋์ ์กฐ๊ฑด๋ค์ด ๋ฐ๋ก ๊ทธ ๋ถ๋ถ์
๋๋ค. StateMachine์ ๋ฑ๋กํ๊ณ ํ ๋ฒ ํ
์คํธ ํด๋ณด์ฃ . ์ด๋ ์ํ(MoveState)์ ๋์ ์ํ(DashState)๋ฅผ ๋งค๋๋ฝ๊ฒ ์ค๊ณ ๊ฐ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
๋์ ์ค์๋ ์ด๋ ํค ์
๋ ฅ์ด ๋จนํ๋ฉด ์ ๋๋๋ฐ, ๋์ ์ํ์ผ ๋๋ ์ด๋ ์ํ ์ฝ๋๋ค์ด ์คํ๋์ง ์์ผ๋ ์์ฐ์ค๋ฝ๊ฒ ์ ์ฉ๋์์ต๋๋ค. ์ด๋ ์ํ๋ฅผ ๊ตฌํํ์๊ณ , ๊ฑฐ๊ธฐ์ ๋์ ์ํ๋ฅผ ์ถ๊ฐํ์ฌ ๊ธฐ๋ฅ ํ์ฅ์ ํ์์ง๋ง ์ด๋ ์ํ ์ฝ๋๋ค์ ์์ ํ ํ์๊ฐ ์์์ต๋๋ค. ํ์ฅ์ด ์ผ๋ง๋ ํธํด์ก๋์ง ์ฒด๊ฐํ ์ ์์๋ค์.
๊ธฐ์กด์ ๋์ ์ํ๋ ํ๋ ์ฝ๋ฉ์ ์ธ ๋ถ๋ถ์ด ๋ง์๊ณ , ๋ถ์์ฐ์ค๋ฝ๊ณ , ์ ๋ ฅ ๋ฒํผ๊ฐ ์์ด ์ฌ์ฉ์๊ฐ N๋จ ๋์๋ฅผ ์ฌ์ฉํ๊ธฐ ์ด๋ ค์ ์ต๋๋ค. ์ด์ ๋ฐ๋ผ ๋ฆฌํฉํ ๋ง์ ์งํํ์๊ณ , ๐ํด๋น ๊ฒ์๊ธ ๋งํฌ๋ฅผ ์ฌ๊ธฐ์ ์ฒจ๋ถํ ์์ ์ ๋๋ค.
์ด๊ฒ์ผ๋ก ์ํ ํจํด์ ์ด์ฉํ์ฌ ์ฝ๋ ๊ธฐ๋ฅ ํ์ฅ์ ํธํ๊ฒ ํ ์ ์๋๋ก ์ ์ฉํ ๋ด์ฉ๋ค์ ๊ธ๋ก ์จ๋ณด์์ต๋๋ค. ๋ค์ ๊ธ์๋ ๋ฌด๊ธฐ ํ์ฅ์ฐฉ ๊ธฐ๋ณธ ํ๊ณผ ๊ธฐ๋ณธ 3ํ ์ฝค๋ณด ๊ณต๊ฒฉ์ด ์ฃผ์ ๊ฐ ๋๊ฒ ๋ค์.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค!