Unity์ Universal Render Pipeline์ ์ฝ์๋ก, ์คํฌ๋ฆฝํธ ๊ฐ๋ฅํ ๋ ๋ ํ์ดํ๋ผ์ธ(Scriptable Render Pipeline)์ด๋ค.
๋ค์๊ณผ ๊ฐ์ ํน์ง์ ๊ฐ์ง๊ณ ์๋ค.
Cross-Platform Compatibility : URP๋ Unity๊ฐ ์ง์ํ๋ ๋ชจ๋ ํ๋ซํผ์์ ๋์ํ๋๋ก ์ค๊ณ๋์๋ค. ์ด๋ ๋ชจ๋ฐ์ผ, ๋ฐ์คํฌํฑ, ์ฝ์ ๊ฒ์ ๋ฟ๋ง ์๋๋ผ AR, VR ์ดํ๋ฆฌ์ผ์ด์ ์๋ ์ ํฉํ๋ค.
Performance and Scalability : URP๋ ์ฑ๋ฅ ๋ฐ ํ์ฅ์ฑ์ ๋ชฉํ๋ก ์ค๊ณ๋์๋ค. ํนํ ์ ์ฌ์ ์ฅ์น์์๋ ๋ฐ์ด๋ ์ฑ๋ฅ์ ์ ๊ณตํ๋๋ก ์ต์ ํ๋์ด ์๋ค. ๋ํ, ๊ทธ๋ํฝ ์ค์ ์ ์ฝ๊ฒ ์กฐ์ ํ ์ ์์ด ๋ค์ํ ์ฅ์น์ ์ ํฉํ๊ฒ ์ค์ผ์ผ๋ง ํ ์ ์๋ค.
Modern Rendering Features : URP๋ ํ๋์ ์ธ ๋ ๋๋ง ๊ธฐ์ ์ ์ ๊ณตํ๋ค. ์ด์๋ ์ฃผ์ ๋ผ์ดํธ ์ ํ, ํ์ค ์์ด๋ฉ ๋ชจ๋ธ, ํ๊ฒฝ ๋ฆฌํ๋ ์ ๋ฑ์ด ํฌํจ๋๋ค.
Customizability : URP๋ ์ฌ์ฉ์ ์ ์ ๋ ๋๋ง ํ์ดํ๋ผ์ธ์ ์์ฑํ ์ ์๋๋ก ํด์ฃผ๋ ์ ์ฐ์ฑ์ ์ ๊ณตํ๋ค. ์ด๋ฅผ ํตํด ํน์ ๊ฒ์ ๋๋ ํ๋ก์ ํธ์ ํ์ํ ๊ณ ์ ํ ๋ ๋๋ง ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์๋ค.
Graphics Quality : URP๋ ๋์ ํ์ง์ ๊ทธ๋ํฝ์ ์ ๊ณตํ๋ค. ์ด๋ ํฅ์๋ ๋ผ์ดํธ ๋ชจ๋ธ, ํ๋ฉด ์์ด๋ฉ, ํฌ์คํธ ํ๋ก์ธ์ฑ ํจ๊ณผ ๋ฑ์ ์ฌ์ฉํ์ฌ ํ๋๋๋ค.
Simplicity : URP๋ Unity์ ๊ธฐ์กด ๋ ๋๋ง ์์คํ ์ ๋นํด ์ฌ์ฉํ๊ธฐ ์ฝ๋ค. ์ด๋ ๊ทธ๋ํฝ ์ค์ ์ ๋จ์ํํ๊ณ , ์ด๊ธฐ ์ค์ ์ ์ฝ๊ฒ ํ๋๋ก ๋์์ฃผ๋ ๋๊ตฌ๊ฐ ํฌํจ๋์ด ์๋ค.
์ ํ ์ํ ๊ธฐ๊ณ (Finite State Machine, FSM)
FSM์ ๊ฐ๋
FSM์ ๊ตฌ์ฑ ์์
FSM์ ๋์ ์๋ฆฌ
FSM์ ์ฅ์
FSM์ ์์ : ํ๋ ์ด์ด ์ํ ๊ด๋ฆฌ
ํ๋ก๋น๋(ProBuilder)๋ Unity์ ์ ์ฉํ ์์ ์ค ํ๋๋ก, ๊ฒ์ ๊ฐ๋ฐ์๋ค์ด ๋น ๋ฅด๊ณ ์ฝ๊ฒ 3D ๋ชจ๋ธ์ ๋ง๋ค๊ณ ํธ์งํ๋ ๋๊ตฌ์ด๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก Unity์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ผ๋ก, ์ ๋ฃ ๋ผ์ด์ผ์ค๊ฐ ํ์์์ด ๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์๋ค.
ํ๋ก๋น๋์ ์ฃผ์ ๊ธฐ๋ฅ๊ณผ ํน์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
3D ๋ชจ๋ธ๋ง : ํ๋ก๋น๋๋ ๋ด์ฅ๋ ๋๊ตฌ๋ค์ ์ฌ์ฉํ์ฌ 3D๋ชจ๋ธ์ ์ฝ๊ฒ ์์ฑํ ์ ์๋ค. ๊ธฐ๋ณธ์ ์ธ ๊ธฐํ ๋ํ(์ ์ก๋ฉด์ฒด, ์๊ธฐ๋ฅ ๋ฑ)์ ์์ฑํ๊ฑฐ๋, ๋ค๋ฅธ ๋ชจ๋ธ๋ง ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋ณต์กํ ๋ชจ๋ธ์ ๋ง๋ค ์ ์๋ค.
ํธ์ง๊ณผ ์์ : ํ๋ก๋น๋๋ ๊ฐ์ฒด๋ฅผ ์ ํํ๊ณ ์ด๋, ํ์ ์ค์ผ์ผ ๋ฑ์ ์ํํ๋ ํธ์ง ๋๊ตฌ๋ฅผ ํฌํจํ๊ณ ์๋ค. ๋ํ ์ ์ , ์ฃ์ง, ๋ฉด ์์ค์์ ์ธ๋ถ์ ์ธ ํธ์ง์ด ๊ฐ๋ฅํ๋ค.
UV ๋งคํ : 3D๋ชจ๋ธ์ ํ ์ค์ฒ UV๋งคํ์ ํ๋ก๋น๋์์ ์ง์ ํ ์ ์๋ค. ์ด๋ฅผ ํตํด ํ ์ค์ฒ๋ฅผ ์ ํํ๊ฒ ์ ์ฉํ ์ ์๋ค.
์ฝ๋ฆฌ์ ์ค์ : ํ๋ก๋น๋๋ ๊ฐ๋จํ ์ฝ๋ฆฌ์ ๋ฉ์ฌ๋ฅผ ๋ง๋ค ์ ์์ผ๋ฉฐ, ์ด๋ฅผ ํตํด ๊ฒ์ ์บ๋ฆญํฐ๋ ๊ฐ์ฒด์ ์ํธ์์ฉํ๋๋ฐ ์ ์ฉํ ์ ์๋ค.
์ฑ๋ฅ ์ต์ ํ : ํ๋ก๋น๋๋ ๋ค๋ฅธ ๋ชจ๋ธ๋ง ๋๊ตฌ์ ํจ๊ป ์ฌ์ฉํ์ฌ ๋ฉ์์ ํด๋ฆฌ๊ณค ๊ฐ์๋ฅผ ์ต์ ํํ๊ณ ๊ฒ์ ์ฑ๋ฅ์ ํฅ์์ํฌ ์ ์๋ค.
์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ(Character Controller)๋ Unity์์ ์บ๋ฆญํฐ๋ ํ๋ ์ด์ด์ ์์ง์๊ณผ ์ถฉ๋์ ๊ด๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ์ปดํฌ๋ํธ์ด๋ค. ์ด ์ปดํฌ๋ํธ๋ ๋ฌผ๋ฆฌ ์์ง์ด ์๋ ์บ๋ฆญํฐ์ ์์ง์์ ํ๋ ์ ๊ธฐ๋ฐ์ผ๋ก ์ฒ๋ฆฌํ๋ฏ๋ก, ์ฃผ๋ก 3D์บ๋ฆญํฐ๋ฅผ ์ ์ดํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ์ ์ฃผ์ ๊ธฐ๋ฅ๊ณผ ํน์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
์บ๋ฆญํฐ ์ด๋ : ์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ๋ ๋จ์ํ ์ด๋์ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋๋ก ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค. ์ฃผ๋ก ์ด๋ ๋ฐฉํฅ๊ณผ ์ด๋ ์๋ ฅ์ ์ค์ ํ์ฌ ์บ๋ฆญํฐ๋ฅผ ์์ง์ด๊ฒ ํ๋ค.
์ค๋ ฅ ์ ์ฉ : ์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ๋ ์ค๋ ฅ์ ์ ์ฉํ์ฌ ์ ํ๋ ๋จ์ด์ง์ ์์ฐ์ค๋ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ค.
์ถฉ๋ ์ฒ๋ฆฌ : ์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ๋ ๋ฌผ๋ฆฌ ์์ง์ ์ฌ์ฉํ์ง ์๊ณ , ์บ๋ฆญํฐ์ ์ถฉ๋์ ๊ฐ์งํ๊ณ ์ฒ๋ฆฌํ ์ ์๋ค. ๋ค๋ฅธ ์ฝ๋ฆฌ๋์์ ์ถฉ๋์ ํต์ ํ๊ณ , ๊ฒฝ์ฌ๋ก์์ ์ํธ์์ฉ ๋ฑ์ ์ง์ํ๋ค.
๋ฐ๋ฅ ๊ฒ์ถ : ์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ๋ ์บ๋ฆญํฐ๊ฐ ๋ฐ๋ฅ ์์ ๋์ด๋๋ก ๋ฐ๋ฅ ๊ฒ์ถ์ ์ฒ๋ฆฌํ๋ค. ๋ฐ๋ฅ๊ณผ์ ๊ฑฐ๋ฆฌ, ํ๋ฉด ๋ ธ๋ฉ ๋ฑ์ ๊ณ ๋ คํ์ฌ ์บ๋ฆญํฐ์ ๋์ด๋ฅผ ์กฐ์ ํ๊ฑฐ๋ ์ ํ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํ๋ค.
์์ง์ ์ ํ : ์บ๋ฆญํฐ ์ปจํธ๋กค๋ฌ๋ ์์ง์์ ์ ํํ๋ ๊ธฐ๋ฅ๋ ์ ๊ณตํ๋ค. ์ง์ ๋ ์์ญ ๋ด์์๋ง ์์ง์ด๋๋ก ํ๊ฑฐ๋, ์งํ์ ๊ฒฝ์ฌ๋ฅผ ๋ฐ๋ผ ์ด๋ํ ์ ์๋๋ก ์ค์ ํ ์ ์๋ค.
Tools - ProBuilder - ProBuilder Window
New Shape ํด๋ฆญ - Pro Builder Shape Size (50 0 50)
ํฌํจ๋ Texture01 ๋๋๊ทธ ์ค ๋๋
๋ฉด๊ณผ ์ ์ ์ ํํ์ฌ ๋ค์๊ณผ ๊ฐ์ ๋ชจ์์ ์งํ์ ์ค๋น.
ํ๋ธ์ ์์น๋ฅผ ์ถ๊ฐ.
๋น ์ค๋ธ์ ํธ ์์ฑ - Player ์ด๋ฆ ๋ณ๊ฒฝ
Paladin ๋ชจ๋ธ ์ถ๊ฐ
Window - Rendering - Render Pipeline Converter ํด๋ฆญ
์ ์ฒด ์ ํ - Convert ํด๋ฆญ
Input Action ์์ฑ
๋ค์๊ณผ ๊ฐ์ด ์ค์
public class PlayerInput : MonoBehaviour
{
public PlayerInputActions InputActions { get; private set; }
public PlayerInputActions.PlayerActions PlayerActions { get; private set; }
private void Awake()
{
InputActions = new PlayerInputActions();
PlayerActions = InputActions.Player;
}
private void OnEnable()
{
InputActions.Enable();
}
private void OnDisable()
{
InputActions.Disable();
}
}
[Serializable]
public class PlayerAnimationData
{
[SerializeField] private string groundParameterName = "@Ground";
[SerializeField] private string idleParameterName = "Idle";
[SerializeField] private string walkParameterName = "Walk";
[SerializeField] private string runParameterName = "Run";
[SerializeField] private string airParameterName = "@Air";
[SerializeField] private string jumpParameterName = "Jump";
[SerializeField] private string fallParameterName = "Fall";
[SerializeField] private string attackParameterName = "@Attack";
[SerializeField] private string comboAttackParameterName = "ComboAttack";
// ๋
์ ์๋ค.
public int GroundParameterHash { get; private set; }
public int IdleParameterHash { get; private set; }
public int WalkParameterHash { get; private set; }
public int RunParameterHash { get; private set; }
// ๊ณต์ค์ ์๋ค.
public int AirParameterHash { get; private set; }
public int JumpParameterHash { get; private set; }
public int fallParameterHash { get; private set; }
// ๊ณต๊ฒฉ์ค์ด๋ค.
public int AttackParameterHash { get; private set; }
public int ComboAttackParameterHash { get; private set; }
// ํด์ ์ด๊ธฐํ ๋ฉ์๋
public void Initialize()
{
GroundParameterHash = Animator.StringToHash(groundParameterName);
IdleParameterHash = Animator.StringToHash(idleParameterName);
WalkParameterHash = Animator.StringToHash(walkParameterName);
RunParameterHash = Animator.StringToHash(runParameterName);
AirParameterHash = Animator.StringToHash(airParameterName);
JumpParameterHash = Animator.StringToHash(jumpParameterName);
fallParameterHash = Animator.StringToHash(fallParameterName);
AttackParameterHash = Animator.StringToHash(attackParameterName);
ComboAttackParameterHash = Animator.StringToHash(comboAttackParameterName);
}
}
public class Player : MonoBehaviour
{
// AnimationAdata๋ฅผ Header์ ์ฐ๊ฒฐ
[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; }
private void Awake()
{
// AnimationData ์ด๊ธฐํ ์งํ
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Input = GetComponent<PlayerInput>();
Controller = GetComponent<CharacterController>();
}
private void Start()
{
// ์ฒ์ ์์์ Cursor์ Lock์ ๊ฑธ์ด์ค๋ค.
Cursor.lockState = CursorLockMode.Locked;
}
}
public interface IState
{
public void Enter();
public void Exit();
public void HandleInput();
public void Update();
public void PhysicsUpdate();
}
public abstract class StateMachine
{
protected IState currentState;
public void ChangeState(IState newState)
{
currentState?.Exit(); // ์ด ์ ์ํ๋ฅผ ๋ง๋ฌด๋ฆฌ
currentState = newState; // ์๋ก์ด ์ํ ์ ์ฉ
currentState.Enter(); // ๋ค์ ์์
}
// ํ์ฌ State์ Handle์ ์คํ
public void HandleInput()
{
currentState?.HandleInput();
}
// ํ์ฌ State์ Update() ์คํ
public void Update()
{
currentState?.Update();
}
public void PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
}
[Serializable]
public class PlayerGroundData // ํ์ , ์ด๋์ ์ฒ๋ฆฌ
{
[field: SerializeField][field: Range(0f, 25f)] public float BaseSpeed { get; private set; } = 5f;
[field: SerializeField][field: Range(0f, 25f)] public float BaseRotationDamping { get; private set; } = 1f;
[field: Header("IdleData")]
[field: Header("WalkData")]
[field: SerializeField][field: Range(0f, 2f)] public float WalkSpeedModifier { get; private set; } = 0.225f;
[field: Header("RunData")]
[field: SerializeField][field: Range(0f, 2f)] public float RunSpeedModifier { get; private set; } = 1f;
}
[Serializable]
public class PlayerAirData // ์ ํ ๋ฐ์ดํฐ
{
[field: Header("JumpData")]
[field: SerializeField][field: Range(0f, 25f)] public float JumpForce { get; private set; } = 4f;
}
[CreateAssetMenu(fileName = "Player", menuName = "Characters/Player")]
public class PlayerSO : ScriptableObject
{
[field: SerializeField] public PlayerGroundData GroundData { get; private set; }
[field: SerializeField] public PlayerAirData AirData { get; private set; }
}
Create๋ฉ๋ด๋ฅผ ํตํด PlayerSO์์ฑ
์๋์ ๊ฐ์ด ์ค์
public class PlayerBaseState : IState
{
protected PlayerStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
// ์ญ์ฐธ์กฐ
public PlayerBaseState(PlayerStateMachine playerStateMachine)
{
stateMachine = playerStateMachine;
groundData = stateMachine.Player.Data.GroundData;
}
public virtual void Enter()
{
AddInputActionsCallbacks();
}
public virtual void Exit()
{
RemoveInputActionsCallbacks();
}
public virtual void HandleInput()
{
ReadMovementInput();
}
public virtual void PhysicsUpdate()
{
}
public virtual void Update()
{
Move();
}
// InputActionํํ
Callback์ ๊ฑธ์ด์ค ๊ฒ์ด๋ค. (Evnet๋ฅผ ๊ฑธ์ด์ฃผ๋ ๋๋)
protected virtual void AddInputActionsCallbacks()
{
}
protected virtual void RemoveInputActionsCallbacks()
{
}
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; // y๊ฐ์ ์ ๊ฑฐํด์ผ ๋
๋ฐ๋ฅ์ ๋ณด๊ณ ๊ฐ์ง ์๋๋ค.
right.y = 0;
// Normalize : Vector ์์ฒด๋ฅผ Normalizeํ๋ค.
// Normalized : Vector๋ฅผ Normalizeํด์ ๊ฐ์ง๊ณ ์จ๋ค.
forward.Normalize();
right.Normalize();
// ๊ฐ๊ฐ ์ด๋ํด์ผํ๋ Vector. ์์ผ๋ก๊ฐ๋ Vector, ์ฐ์ธก์ผ๋ก ๊ฐ๋ Vector ์๋ค ์
๋ ฅํ
// ์ด๋ ๋ฐฉํฅ์ ๊ฐ์ ๊ณฑํด์ ์ด๋
return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x;
}
private void Move(Vector3 movementDiraction)
{
float movementSpeed = GetMovementSpeed();
stateMachine.Player.Controller.Move((movementDiraction * movementSpeed) * Time.deltaTime); // ์ด๋ ๋ฐฉํฅ * ์ด๋ ์คํผ๋
}
private void Rotate(Vector3 movementDirection)
{
if (movementDirection != Vector3.zero) // ์
๋ ฅ์ด ๋์ ๋, (zero๊ฐ ์๋ ๋)
{
Transform playerTransform = stateMachine.Player.transform;
Quaternion targetRotation = Quaternion.LookRotation(movementDirection); // ()์์ ๋ฐฉํฅ์ ๋ฐ๋ผ๋ณด๋ Quaternion
playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime); // Slerp : ํ์ํ ๋ณด๊ฐ
}
}
private float GetMovementSpeed()
{
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);
}
}
public class PlayerGroundState : PlayerBaseState
{
public PlayerGroundState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
base.Enter();
// Ground์ ์์ผ๋ฉด GroundState๋ผ๋ bool๊ฐ์ด ์ผ์ ธ์์ ๊ฒ์ด๋ผ Ground์ํ์ธ์ง ๊ฒ์ฌ ๊ฐ๋ฅ
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();
}
}
public class PlayerIdleState : PlayerGroundState // ๋
์ ์์ ๋, Idle์ ํ ํ
๋ Ground์์
{
public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0f; // Idle์ํ์ ์์ง์์ ๋ฏธ์ฐ์ ๋ฐฉ์ง
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
}
public override void Update()
{
base.Update();
}
}
public class PlayerStateMachine : StateMachine
{
public Player Player { get; }
// States
public PlayerIdleState idleState { get; }
public Vector2 MovementInput { get; set; } // Input ์
๋ ฅ๊ฐ Vector
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);
MainCameraTransform = Camera.main.transform;
MovementSpeed = player.Data.GroundData.BaseSpeed;
RotationDamping = player.Data.GroundData.BaseRotationDamping;
}
}
''' ์๋ต
// ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๊ฒ ์ค๋น
[field: Header("References")]
[field: SerializeField] public PlayerSO Data { get; private set; }
''' ์๋ต
private void Awake()
{
// AnimationData ์ด๊ธฐํ ์งํ
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Input = GetComponent<PlayerInput>();
Controller = GetComponent<CharacterController>();
stateMachine = new PlayerStateMachine(this); // PlayerStateMachine ์์ฑ์์๊ฒ this(์๊ธฐ์์ .Player)๋ฅผ ๋๊ฒจ์ค๋ค.
}
private void Start()
{
// ์ฒ์ ์์์ Cursor์ Lock์ ๊ฑธ์ด์ค๋ค.
Cursor.lockState = CursorLockMode.Locked;
stateMachine.ChangeState(stateMachine.idleState);
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
''' ์๋ต
PlayerSO ์ฐ๊ฒฐ
Player Input ์ถ๊ฐ
Character Controller ์ถ๊ฐ
Animator Controller ์์ฑ
ํ๋ ์ด์ด ๋ชจ๋ธ - Animator ์ถ๊ฐ - Animator Controller ์ฐ๊ฒฐ
'''์๋ต
// InputActionํํ
Callback์ ๊ฑธ์ด์ค ๊ฒ์ด๋ค. (Evnet๋ฅผ ๊ฑธ์ด์ฃผ๋ ๋๋)
protected virtual void AddInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled += OnMoveCanceled; // .canceled : ํค๊ฐ ๋ผ์ด์ก์ ๋
input.PlayerActions.Run.started += OnRunStarted;
}
protected virtual void RemoveInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled -= OnMoveCanceled;
input.PlayerActions.Run.started -= OnRunStarted;
}
protected virtual void OnMoveCanceled(InputAction.CallbackContext context)
{
}
protected virtual void OnRunStarted(InputAction.CallbackContext context)
{
}
'''์๋ต
''' ์๋ต
protected override void OnMoveCanceled(InputAction.CallbackContext context)
{
if (stateMachine.MovementInput == Vector2.zero) // ์
๋ ฅ์ด ์์ผ๋ฉด
{
return;
}
// ์ด๋ํค๊ฐ ๋ผ์ด์ก์ ๋, ์ด๊ฑธ ์ Ground์์ ๋ง๋๋?
// ๊ณต์ค(Air)์ํ || ๊ตฌ๋ฅผ ๋, ๋ฑ Ground๊ฐ ์๋ ๋ค๋ฅธ State์ ์์ ๋, ํค๋ฅผ ๋ผ๋ฉด ๋ค๋ฅธ ๋์์
// ํด์ผํ๋ค. Idle๋ก ๋์ด์ค๋ฉด ์๋๋ค. ๊ทธ๋์ ์ด ๋์์ Ground๊ฐ ๋ณด๋๊ฒ ๋ง๋ค.
// Ground์ผ ๋, ํค๋ฅผ ๋ผ๋ฉด ์ด๋ป๊ฒ ํ ๊ฒ์ธ๊ฐ? ์ ๋ํ ๊ฒ์ด๋๊น.
stateMachine.ChangeState(stateMachine.IdleState);
base.OnMoveCanceled(context);
}
protected virtual void OnMove()
{
// ์ด๋์ด ์ผ์ด๋๋ฉด Walk๋ก ์ฎ๊ฒจ์ค๋ค.
stateMachine.ChangeState(stateMachine.WalkState);
}
public class PlayerWalkState : PlayerGroundState
{
public PlayerWalkState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = groundData.WalkSpeedModifier; // Walk์ ์๋๋ก ๊ณฑํด์ฃผ๋ ๊ฐ
// ๋ถ๋ชจ PlayerGroundState๊ฐ ๊ฐ๊ณ ์๋ Enter()๋ฅผ ์คํํด Groundํ๋ผ๋ฏธํฐ๋ฅผ ํค๊ณ
// ๋ค์ ๋์์์ Walkํ๋ผ๋ฏธํฐ๋ฅผ ์ผ์ค๋ค. ๊ทธ๋ฌ๋ฉด WalkState์ ๋ค์ด์์ผ๋ฉด
// Ground, Walk ๋๊ฐ์ bool๊ฐ์ด ์ผ์ง๋ค.
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
}
// Walk(๊ฑท๋ค๊ฐ) Run์ ๋๋ฅธ๋ค? ๊ทธ๋ผ Run์ผ๋ก ์ ํ
protected override void OnRunStarted(InputAction.CallbackContext context)
{
base.OnRunStarted(context);
stateMachine.ChangeState(stateMachine.RunState);
}
}
public class PlayerRunState : PlayerGroundState
{
public PlayerRunState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
// ์ด๋์ฒ๋ฆฌ ๋ค์ Base์์ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ์ ๋๋ฉ์ด์
๋ฐ ์๋๋ง ์ฒ๋ฆฌ
public override void Enter()
{
stateMachine.MovementSpeedModifier = groundData.RunSpeedModifier; // Run ์๋๋ก
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.RunParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.RunParameterHash);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStateMachine : StateMachine
{
''' ์๋ต
// States
public PlayerIdleState IdleState { get; }
public PlayerWalkState WalkState { get; }
public PlayerRunState RunState { get; }
''' ์๋ต
public PlayerStateMachine(Player player)
{
this.Player = player;
IdleState = new PlayerIdleState(this);
WalkState = new PlayerWalkState(this);
RunState = new PlayerRunState(this);
MainCameraTransform = Camera.main.transform;
MovementSpeed = player.Data.GroundedData.BaseSpeed;
RotationDamping = player.Data.GroundedData.BaseRotationDamping;
}
}
''' ์๋ต
public override void Update()
{
base.Update();
if (stateMachine.MovementInput != Vector2.zero) // ์ด๋์ด ์ผ์ด๋ฌ๋ค๋ฉด?
{
OnMove(); // OnMove() : Idle์์ Walk๋ก ๋ฐ๊ฟ์ฃผ๋ ์ฒ๋ฆฌ
return;
}
}
์๋ค๋จธ์ (Cinemachine)์ Unity์์ ์ ๊ณตํ๋ ๊ณ ๊ธ ์นด๋ฉ๋ผ ์์คํ ์ด๋ค. ์ด ์์คํ ์ ๊ฒ์, ์ํ, ์ ๋๋ฉ์ด์ ๋ฑ์์ ๋ค์ํ ์นด๋ฉ๋ผ ์ํฌํ๋ก์ฐ๋ฅผ ์ง์ํ๋ฉฐ, ์นด๋ฉ๋ผ ์ด๋, ์ถ์ , ๋งคํ, ๋ธ๋ ๋ฉ ๋ฑ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋๋ก ๋์์ค๋ค. ์๋ค๋จธ์ ์ Unity 2017 ๋ฒ์ ์ดํ๋ถํฐ ๊ธฐ๋ณธ์ ์ผ๋ก ํฌํจ๋์ด ์์ผ๋ฉฐ, ์ ๋ํฐ ์์ ์คํ ์ด์์๋ ์ถ๊ฐ์ ์ธ ํ์ฅ ํจํค์ง๋ฅผ ์ป์ ์ ์๋ค.
์๋ค๋จธ์ ์ ์ฃผ์ ๊ธฐ๋ฅ๊ณผ ํน์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
๊ฐ์ ์นด๋ฉ๋ผ(Virtual Camera) : ์๋ค๋จธ์ ์ ๊ฐ์ ์นด๋ฉ๋ผ๋ฅผ ์ฌ์ฉํ์ฌ ์ค์ ์นด๋ฉ๋ผ๊ฐ ์๋ ์ํ์์๋ ์ฌ๋ฌ ๊ฐ์ ์นด๋ฉ๋ผ์ ์์ ๊ณผ ์์ฑ์ ์กฐ์ํ ์ ์๋ค. ๊ฐ ๊ฐ์ ์นด๋ฉ๋ผ๋ ๋ ๋ฆฝ์ ์ธ ์นด๋ฉ๋ผ ์์ฑ์ ๊ฐ์ง๋ฉฐ, ์ํ๋ ์๊ฐ์ ํ์ฑํ ๋๊ฑฐ๋ ๋นํ์ฑํ๋ ์ ์๋ค.
์นด๋ฉ๋ผ ๋ธ๋ ๋ฉ : ์ฌ๋ฌ ๊ฐ์ ์นด๋ฉ๋ผ ๊ฐ์ ์์ฐ์ค๋ฝ๊ฒ ์ ํํ๊ธฐ ์ํด ๋ธ๋ ๋ฉ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์นด๋ฉ๋ผ์ ์์น, ํ์ ,์์ผ ๋ฑ์ด ๋ถ๋๋ฝ๊ฒ ์ ํ๋์ด ์์ฐ์ค๋ฌ์ด ์นด๋ฉ๋ผ ์ด๋์ ๊ตฌํํ ์ ์๋ค.
ํธ๋๋ง๊ณผ ์ถ์ : ์๋ค๋จธ์ ์ ์ค๋ธ์ ํธ๋ ์บ๋ฆญํฐ๋ฅผ ์ถ์ ํ์ฌ ์๋์ผ๋ก ์นด๋ฉ๋ผ๋ฅผ ๋ฐ๋ผ๊ฐ๋๋ก ์ง์ํ๋ค. ํน์ ํ๊ฒ์ ์ถ์ ํ๊ฑฐ๋ ๊ฒฝ๋ก๋ฅผ ๋ฐ๋ผ ์ด๋ํ๋ ์นด๋ฉ๋ผ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌํํ ์ ์๋ค.
์ ๋๋ฉ์ด์ ์ฐ๋ : ์๋ค๋จธ์ ์ ์ ๋๋ฉ์ด์ ์์คํ ๊ณผ ์ฐ๋ํ์ฌ ์นด๋ฉ๋ผ์ ์์ฑ์ ์ ๋๋ฉ์ด์ ๊ณผ ๋๊ธฐํํ ์ ์๋ค.
๋ ์ฆ ์ธํธ : ๋ ์ฆ ์ธํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ฆ ์กฐ์์ ํธ๋ฆฌํ๊ฒ ํ ์ ์๋ค. ์ค, ํ๊ฐ, ํน์ ๋ ์ฆ ํจ๊ณผ ๋ฑ์ ์ฝ๊ฒ ์ ์ฉํ ์ ์๋ค.
Create - Cinemachine - Virtual Camera ํด๋ฆญ
CameraLookPoint ์ถ๊ฐ
public class ForceReciver : MonoBehaviour
{
[SerializeField] private CharacterController controller;
[SerializeField] private float drag = 0.3f;
private Vector3 dampingVelocity;
private Vector3 impact;
private float verticalVelocity;
// impact : ์ถ๊ฐ์ ์ธ ํ์ ๋ฐ์ผ๋ฉด? ๊ทธ ํ์ ์ฌ์ฉํ๊ธฐ ์ํ impacte
public Vector3 Movement => impact + Vector3.up * verticalVelocity; // verticalVelocity : ์์ง์ ํ
private void Update()
{
if (verticalVelocity < 0f && controller.isGrounded) // ๋
์ ๋ถ์ด์๋๊ฐ?
{
verticalVelocity = Physics.gravity.y * Time.deltaTime; // gravity : -9.7 ?
}
else // ๋
์ด ์๋๋ผ๋ฉด?
{
verticalVelocity += Physics.gravity.y * Time.deltaTime; // ๊ฐ์๋ ์ฒ๋ฆฌ
}
// SmoothDamp : current => target ๊น์ง ๊ฐ์ ๋ถ๋๋ฌ์ด ๋ณํ
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;
}
}
''' ์๋ต
public ForceReceiver ForceReceiver { get; private set; }
''' ์๋ต
ForceReceiver = GetComponent<ForceReceiver>();
''' ์๋ต
private void Move(Vector3 movementDiraction)
{
float movementSpeed = GetMovementSpeed();
stateMachine.Player.Controller.Move(((movementDiraction * movementSpeed) + stateMachine.Player.ForceReciver.Movement) * Time.deltaTime); // ์ด๋ ๋ฐฉํฅ * ์ด๋ ์คํผ๋ + ForceReciver
}
''' ์๋ต
protected virtual void AddInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled += OnMovementCanceled;
input.PlayerActions.Run.started += OnRunStarted;
stateMachine.Player.Input.PlayerActions.Jump.started += OnJumpStarted;
}
protected virtual void RemoveInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled -= OnMovementCanceled;
input.PlayerActions.Run.started -= OnRunStarted;
stateMachine.Player.Input.PlayerActions.Jump.started -= OnJumpStarted;
}
protected virtual void OnJumpStarted(InputAction.CallbackContext context)
{
}
''' ์๋ต
''' ์๋ต
protected override void OnJumpStarted(InputAction.CallbackContext context)
{
// ์ ํ ํค๊ฐ ๋๋ฆฌ๋ฉด JumpState๋ก ๋ณ๊ฒฝ (GroundState์์ด๋ ๋
์ ๋ถ์ด์๋ ์ํ์ด๋ค.)
stateMachine.ChangeState(stateMachine.JumpState);
}
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);
}
}
public class PlayerJumpState : PlayerAirState
{
public PlayerJumpState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
stateMachine.JumpForce = stateMachine.Player.Data.AirData.JumpForce; // SO์ ์ค์ ํ ์ ํ ๊ฐ ๋งํผ
stateMachine.Player.ForceReciver.Jump(stateMachine.JumpForce); // ๋งค๊ฐ๋ณ์๋ก ๋์
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
// velocity.y๊ฐ 0๋ณด๋ค ์์์ ์์๊ฐ ๋์ด ๋จ์ด์ง๋ ์์ ์ด ๋ ๋
if (stateMachine.Player.Controller.velocity.y <= 0)
{
stateMachine.ChangeState(stateMachine.FallState); // ๋จ์ด์ง๋(Fall)๋ก ์ ํ
return;
}
}
}
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;
}
}
}
'''์๋ต
public PlayerJumpState JumpState { get; }
public PlayerFallState FallState { get; }
'''์๋ต
JumpState = new PlayerJumpState(this);
FallState = new PlayerFallState(this);
''' ์๋ต
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
// ๋
์ด ์๋ ๋ && velocity.y ๊ฐ์ด Physics.gravity.y * Time.fixedDeltaTime ๋ณด๋ค ์์ ๋
// Physics.gravity.y * Time.fixedDeltaTime : gravity๋ฅผ ํ ํ์์ ์ ์ฉํ๋ ๊ฐ , ํจ์ฌ ๋ ๋น ๋ฅธ์๋๋ก ๋จ์ด์ง๊ณ ์๋ค๋ฉด
// ์ฆ, ๋
์ด ์๋๊ณ ๋จ์ด์ง๊ณ ์๋ค๋ฉด FallState๋ก ๋ฐ๊ฟ๋ผ
if (!stateMachine.Player.Controller.isGrounded && stateMachine.Player.Controller.velocity.y < Physics.gravity.y * Time.fixedDeltaTime)
{
stateMachine.ChangeState(stateMachine.FallState);
return;
}
}
[Serializable]
public class AttackInfoData
{
[field: SerializeField] public string AttackName { get; private set; } // ๊ณต๊ฒฉ์ ์ด๋ฆ
[field: SerializeField] public int ComboStateIndex { get; private set; } // ์ฝค๋ณด ์ํ์ ์ธ๋ฑ์ค
[field: SerializeField][field: Range(0f, 1f)] public float ComboTransitionTime { get; private set; } // ์ฝค๋ณด๊ฐ ์ ์ง๋๋ ์๊ฐ
[field: SerializeField][field: Range(0f, 3f)] public float ForceTransitionTime { get; private set; } // ๊ณต๊ฒฉ์ ๋๋ฅด๋ ์๊ฐ
[field: SerializeField][field: Range(-10f, 10f)] public float Force { get; private set; } // ํ์ ์ธ์ ํด๋ฆญํ๋ฉด ์ธ์ ์ ์ฉํ ๊ฑด์ง
[field: SerializeField] public int Damage { get; private set; }
}
[Serializable]
public class PlayerAttackData
{
[field: SerializeField] public List<AttackInfoData> AttackInfoDatas { get; private set; } // ์ฝค๋ณด์ ๋ํ ์ข
๋ฅ๋ฅผ ๊ฐ์ง
public int GetAttackInfoCount() { return AttackInfoDatas.Count;} // AttackInfo์ ๋ํ Count๋ฅผ ๊ฐ์ ธ์ด. ๋ช๊ฐ์ธ์ง
public AttackInfoData GetAttackInfo(int index) { return AttackInfoDatas[index]; } // index๋ฅผ ๋์
ํ๋ฉด ํ์ฌ ์ฌ์ฉํ๊ณ ์๋ AttackData์ ๋ํด์ ๊ฐ์ ธ์ด
}
'''์๋ต
[field: SerializeField] public PlayerAttackData AttakData { get; private set; }
public class PlayerAttackState : PlayerBaseState
{
public PlayerAttackState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0; // ๊ณต๊ฒฉ์ค์ ์์ง์ด์ง ์๊ฒ
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
}
}
'''์๋ต
public PlayerComboAttackState ComboAttackState { get; }
'''์๋ต
public bool IsAttacking { get; set; }
public int ComboIndex { get; set; }
public PlayerStateMachine(Player player)
{
'''์๋ต
ComboAttackState = new PlayerComboAttackState(this);
''' ์๋ต
protected void ForceMove()
{
// ์ Move()์ ๋ฌ๋ฆฌ Direction๊ฐ(๋งค๊ฐ๋ณ์)๋ฅผ ๋ฐ์ง์๊ณ ForceReciver๊ฐ ๊ฐ๊ณ ์๋ Movemenet๊ฐ์ผ๋ก๋ง ์ฒ๋ฆฌํ๋๋ก ๋ง๋ค์ด์ก์ ๋ฟ์ด๋ค stateMachine.Player.Controller.Move(stateMachine.Player.ForceReceiver.Movement * Time.deltaTime);
}
''' ์๋ต
// InputActionํํ
Callback์ ๊ฑธ์ด์ค ๊ฒ์ด๋ค. (Evnet๋ฅผ ๊ฑธ์ด์ฃผ๋ ๋๋)
protected virtual void AddInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled += OnMoveCanceled; // .canceled : ํค๊ฐ ๋ผ์ด์ก์ ๋
input.PlayerActions.Run.started += OnRunStarted;
stateMachine.Player.Input.PlayerActions.Jump.started += OnJumpStarted;
// ์ง๊ธ์ ๊ณต๊ฒฉ, ์ ํ, ๋ฌ๋ฆฌ๊ธฐ, ์ด๋ ๋ฑ ์ ๋ถ ๊ณต์ฉ์ผ๋ก ์ฌ์ฉ๋๋ ํค ์ด๊ธฐ ๋๋ฌธ์ ์ด๋ฐ์์ผ๋ก
// ๋ง๋ค์ด ๋์์ง๋ง, ๋์ค์ ํน์ ์ํ์์๋ง ์ฌ์ฉํ ํค๋ฅผ ๊ตฌํํ๋ค๊ณ ํ๋ฉด, ์ฌ๊ธฐ์ ๊ณ ์ฉ์ผ๋ก
// ๋ง๋ค๊ธฐ ๋ณด๋ค๋ ๊ฐ ์ํฉ์ ํฐ ์ํ๋ค, Ground, Ataack, Air ๋ฑ ๊ทธ๊ณณ์์ overrideํด์ ๊ตฌํํ๋
// ๊ฒ์ด ํจ์ฌ ๋ ๊น๋ํ ๊ฒ์ด๋ค.
// ์ง๊ธ์ ๊ณต์ฉ์ด๋๊น.-> ๊ณต๊ฒฉ๋ฒํผ์ ๊ณต์ค์์๋ ์ฐ๊ณ , ์ ํ์ค์์๋ ์ฐ๊ณ ํ ์์๋ ์ํ์ด๊ธฐ ๋๋ฌธ
stateMachine.Player.Input.PlayerActions.Attack.performed += OnAttackPerformed;
stateMachine.Player.Input.PlayerActions.Attack.canceled += OnAttackCanceled;
}
protected virtual void RemoveInputActionsCallbacks()
{
PlayerInput input = stateMachine.Player.Input;
input.PlayerActions.Movement.canceled -= OnMovementCanceled;
input.PlayerActions.Run.started -= OnRunStarted;
stateMachine.Player.Input.PlayerActions.Jump.started -= OnJumpStarted;
stateMachine.Player.Input.PlayerActions.Attack.performed -= OnAttackPerformed;
stateMachine.Player.Input.PlayerActions.Attack.canceled -= OnAttackCanceled;
}
// Performed : ๋๋ ค์ง๊ณ ์๋ ๋์์๋ true
protected virtual void OnAttackPerformed(InputAction.CallbackContext obj)
{
stateMachine.IsAttacking = true;
}
// Cancel : ๋ผ๋ฉด false
protected virtual void OnAttackCanceled(InputAction.CallbackContext obj)
{
stateMachine.IsAttacking = false;
}
'''์๋ต
protected float GetNormalizedTime(Animator animator, string tag)
{
AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0); // GetCurrentAnimatorStateInfo : ํ์ฌ ์ ๋๋ฉ์ด์
์ ๋ณด
AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0); // GetNextAnimatorStateInfo : ๋ค์ ์ ๋๋ฉ์ด์
์ ๋ณด
// IsInTransition : ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ animator๊ฐ ๋ผ์ธ์ ํ๊ณ ์๋?
// nextInfo.IsTag(tag) : ๋ค์ tag๊ฐ Attack(๋งค๊ฐ๋ณ์ tag)์ธ๊ฐ?
if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
{
// ๋ธ๋์ง๋ ๋์ด์ ์์ด๊ณ ์๊ธฐ ๋๋ฌธ์ ์ด๋ฏธ ํธ๋์ง์
์ ํ์ผ๋ฉด ๋ค์ ์ ๋๋ฉ์ด์
์ ๊ฐ์ ธ์ค๋ ๊ฒ์ด ๋ง๋ค.
return nextInfo.normalizedTime; // ๋ค์๊บผ์ ๋ํ normalizedTime์ ๋๊ฒจ์ค
}
else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag)) // currentInfo.IsTag(tag) : ๋์ ํ๊ทธ๊ฐ Attack์ด๋?
{
return currentInfo.normalizedTime; // ํ์ฌ๋ฅผ ๋๊ฒจ์ค
}
else
{
return 0; // ๋ ๋ค ์๋๋ฉด Attack์ด ์๋๋ผ๋ ๊ฒ. 0์ return;
}
}
public class PlayerComboAttackState : PlayerAttackState
{
private bool alreadyAppliedForce; // ์ฝค๋ณด๋ฅผ ์ ์ฉ ํ๋์ง
private bool alreadyApplyCombo; // ํฌ์ค๋ฅผ ์ ์ฉ ํ๋์ง
AttackInfoData attackInfoData;
public PlayerComboAttackState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.ComboAttackParameterHash);
// ๊ฐ์ง๊ณ ์๋ ๊ฐ์ ์ด๊ธฐํ
alreadyAppliedForce = false;
alreadyApplyCombo = false;
int comboIndex = stateMachine.ComboIndex;
attackInfoData = stateMachine.Player.Data.AttackData.GetAttackInfo(comboIndex); // ํ์ฌ ์ฌ์ฉํด์ผ ํ๋ comboIndex์ ๋ํ ComboAttack์ ์ ๋ณด
stateMachine.Player.Animator.SetInteger("Combo", comboIndex);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.ComboAttackParameterHash);
// ์ฝค๋ณด๋ฅผ ์ ์ฉํ์ง ๋ชปํ ์ํ๋ก ์ด State๊ฐ ๋๋ฌ๋ค๋ฉด, ๊ณต๊ฒฉ์ค์ ์ฝค๋ณด๋ฅผ ์ฑ๊ณตํ์ง ์์๋ค๋ ๊ฒ
// ๊ทธ๋ฌ๋๊น ์ฝค๋ณด์ ๋ณด๋ฅผ 0์ผ๋ก ๋๋ ค๋์ผ ๋ค์์ ๋ค์ ๊ณต๊ฒฉํ ๋, ์ฒซ ์ฝค๋ณด๊ณต๊ฒฉ๋ถํฐ ๋๊ฐ๋ค.
if (!alreadyApplyCombo)
stateMachine.ComboIndex = 0; // ์ฝค๋ณด๋ฅผ ์ ์ฉํ์ง ์์๋ค๋ฉด, 0์ผ๋ก ๋ง๋ ๋ค.
}
private void TryComboAttack()
{
if (alreadyApplyCombo) return; // ์ฝค๋ณด์ดํ์ ์ด๋ฏธ ํ๋ค๋ฉด?(true๋ผ๋ฉด) return;
if (attackInfoData.ComboStateIndex == -1) return; // Index๊ฐ -1 ? ์ฆ ๋ง์ง๋ง ๊ณต๊ฒฉ ๊ทธ๋ผ return;
if (!stateMachine.IsAttacking) return; // !IsAttacking ? ๊ณต๊ฒฉ์ด ๋๋ฌ๋ค๋ ๊ฒ, ๊ทธ๋ผ return;
alreadyApplyCombo = true; // ์์ ๋ชจ๋ ์กฐ๊ฑด์ด ์๋๋ผ ํ๋ฉด? true
}
private void TryApplyForce()
{
if (alreadyAppliedForce) return; // ์ด๋ฏธ ์ ์ฉํ ์ ์ด ์๋ค? return;
alreadyAppliedForce = true; // ์๋๋ฉด true;
stateMachine.Player.ForceReciver.Reset(); // ๊ฐ๊ณ ์๋ ํ. Force๋ฅผ Reset
// AddForce() : ๋ด๊ฐ ๋ฐ๋ผ๋ณด๊ณ ์๋ ์ ๋ฉด์์ ๋ฐ๋ ค๋๊ฒ
stateMachine.Player.ForceReciver.AddForce(stateMachine.Player.transform.forward * attackInfoData.Force);
}
public override void Update()
{
base.Update();
ForceMove();
// animator์ tag๊ฐ์ ๋๊ฒจ ํด๋น ์ ๋๋ฉ์ด์
์ normalizedTime์ ๊ฐ์ ธ์จ๋ค.
float normalizedTime = GetNormalizedTime(stateMachine.Player.Animator, "Attack");
if (normalizedTime < 1f) // 1๋ณด๋ค ์๋ค? ๊ทธ๋ผ ์์ง ์ ๋๋ฉ์ด์
์งํ์ค
{
if (normalizedTime >= attackInfoData.ForceTransitionTime)
TryApplyForce(); // ํ ์ ์ฉ
if (normalizedTime >= attackInfoData.ComboTransitionTime)
TryComboAttack(); // ์ฝค๋ณด ์ ์ฉ
}
else // ์๋๋ค? ์ฒ๋ฆฌ ๋๋ฌ์ ๋
{
if (alreadyApplyCombo) // ์ฝค๋ณด๊ฐ ์งํ์ค? ๋ค์ ์ฝค๋ณด๋ก ๋ณ๊ฒฝ
{
stateMachine.ComboIndex = attackInfoData.ComboStateIndex;
stateMachine.ChangeState(stateMachine.ComboAttackState);
}
else
{
stateMachine.ChangeState(stateMachine.IdleState);
}
}
}
}
''' ์๋ต
public override void Update()
{
base.Update();
if (stateMachine.IsAttacking) // IsAttacking ์ด true๋ผ๋ฉด?
{
OnAttack(); // ์คํ
return;
}
}
Player์ ๊ฑฐ์ ์ ์ฌํ๋ค.
public class EnemyStateMachine : StateMachine
{
public Enemy Enemy { get; }
public Transform Target { get; private set; }
public EnemyIdleState IdlingState { get; }
public EnemyChasingState ChasingState { get; }
public EnemyAttackState AttackState { 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 EnemyStateMachine(Enemy enemy)
{
Enemy = enemy;
Target = GameObject.FindGameObjectWithTag("Player").transform;
IdlingState = new EnemyIdleState(this);
ChasingState = new EnemyChasingState(this);
AttackState = new EnemyAttackState(this);
MovementSpeed = enemy.Data.GroundedData.BaseSpeed;
RotationDamping = enemy.Data.GroundedData.BaseRotationDamping;
}
}
public class Enemy : MonoBehaviour
{
[field: Header("References")]
[field: SerializeField] public EnemySO 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 ForceReciver ForceReceiver { get; private set; }
public CharacterController Controller { get; private set; }
private EnemyStateMachine stateMachine;
void Awake()
{
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Controller = GetComponent<CharacterController>();
ForceReceiver = GetComponent<ForceReciver>();
stateMachine = new EnemyStateMachine(this);
}
private void Start()
{
stateMachine.ChangeState(stateMachine.IdlingState);
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
}
[CreateAssetMenu(fileName = "EnemySO", menuName = "Characters/Enemy")]
public class EnemySO : ScriptableObject
{
[field: SerializeField] public float PlayerChasingRange { get; private set; } = 10f;
[field: SerializeField] public float AttackRange { get; private set; } = 1.5f;
[field: SerializeField][field: Range(0f, 3f)] public float ForceTransitionTime { get; private set; }
[field: SerializeField][field: Range(-10f, 10f)] public float Force { get; private set; }
[field: SerializeField] public int Damage { get; private set; }
[field: SerializeField][field: Range(0f, 1f)] public float Dealing_Start_TransitionTime { get; private set; }
[field: SerializeField][field: Range(0f, 1f)] public float Dealing_End_TransitionTime { get; private set; }
[field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
}
// Player์ ๊ฑฐ์ ๋์ผ
public class EnemyBaseState : IState
{
protected EnemyStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
public EnemyBaseState(EnemyStateMachine ememyStateMachine)
{
stateMachine = ememyStateMachine;
groundData = stateMachine.Enemy.Data.GroundedData;
}
public virtual void Enter()
{
}
public virtual void Exit()
{
}
public virtual void HandleInput()
{
}
public virtual void Update()
{
Move();
}
public virtual void PhysicsUpdate()
{
}
protected void StartAnimation(int animationHash)
{
stateMachine.Enemy.Animator.SetBool(animationHash, true);
}
protected void StopAnimation(int animationHash)
{
stateMachine.Enemy.Animator.SetBool(animationHash, false);
}
private void Move()
{
Vector3 movementDirection = GetMovementDirection();
Rotate(movementDirection);
Move(movementDirection);
}
protected void ForceMove()
{
stateMachine.Enemy.Controller.Move(stateMachine.Enemy.ForceReceiver.Movement * Time.deltaTime);
}
// ํ๊ฒ์ ๋ฐฉํฅ์ผ๋ก ๊ฐ์ ธ์ค๊ฒ ํ๋ค.
private Vector3 GetMovementDirection()
{
return (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).normalized;
}
private void Move(Vector3 direction)
{
float movementSpeed = GetMovementSpeed();
stateMachine.Enemy.Controller.Move(((direction * movementSpeed) + stateMachine.Enemy.ForceReceiver.Movement) * Time.deltaTime);
}
private void Rotate(Vector3 direction)
{
if (direction != Vector3.zero)
{
direction.y = 0;
Quaternion targetRotation = Quaternion.LookRotation(direction);
stateMachine.Enemy.transform.rotation = Quaternion.Slerp(stateMachine.Enemy.transform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
}
}
protected float GetMovementSpeed()
{
float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
return movementSpeed;
}
protected float GetNormalizedTime(Animator animator, string tag)
{
AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);
if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
{
return nextInfo.normalizedTime;
}
else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag))
{
return currentInfo.normalizedTime;
}
else
{
return 0f;
}
}
// Enemy์์์ ์ถ๊ฐ ์ฝ๋
protected bool IsInChaseRange()
{
// if (stateMachine.Target.IsDead) { return false; }
// Enemy์ Target์ ๊ฑฐ๋ฆฌ๋ฅผ ์ฌ๊ณ ๊ทธ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ๋ค.
// sqrMagnitude : (ํผํ๊ณ ๋ผ์ค)์ ๊ณฑ๊ทผ์ ํ์ง ์์ Magnitude, ์ ๊ณฑ๊ทผ์ ํธ๋ ์ฐ์ฐ์ด ๋ฌด๊ฒ๋ค ๋ณด๋
float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
// ์ฌ๊ธฐ์์ ๊ณฑํ๊ธฐ๋ฅผ ํ๋ฒ ๋ ํด์ค๋ค.
return playerDistanceSqr <= stateMachine.Enemy.Data.PlayerChasingRange * stateMachine.Enemy.Data.PlayerChasingRange;
}
}
public class EnemyIdleState : EnemyBaseState
{
public EnemyIdleState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0f;
base.Enter();
// ํ๋ ์ด์ด์ ๊ณต์ ํด์ ์ธ ๊ฒ์ด๋ค ๋ณด๋ ํ๋ฒ์ ๋๊ฐ๋ฅผ ์ก์์ค๋ค.
StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
}
public override void Update()
{
if (IsInChaseRange()) // ๊ทผ๊ฑฐ๋ฆฌ๊น์ง ๋๋ฌ์ ํ๋ค๋ฉด
{
stateMachine.ChangeState(stateMachine.ChasingState);
return;
}
}
}
public class EnemyChasingState : EnemyBaseState
{
public EnemyChasingState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 1;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
}
public override void Update()
{
base.Update();
if (!IsInChaseRange()) // ์ซ์๊ฐ๋ ๊ฑฐ๋ฆฌ ๋ด์ ์๋ค๋ฉด
{
stateMachine.ChangeState(stateMachine.IdlingState); // Idle๋ก ์ ํ
return;
}
else if (IsInAttackRange()) // ๊ณต๊ฒฉ ๊ฑฐ๋ฆฌ ์์ ์๋ค๋ฉด
{
stateMachine.ChangeState(stateMachine.AttackState); // Attack๋ก ์ ํ
return;
}
}
private bool IsInAttackRange()
{
// if (stateMachine.Target.IsDead) { return false; }
// ํ๊ฒ๊ณผ์ ๊ฑฐ๋ฆฌ ๋น๊ตํด์
float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
// AttackRange๋ ๋น๊ต
return playerDistanceSqr <= stateMachine.Enemy.Data.AttackRange * stateMachine.Enemy.Data.AttackRange;
}
}
BaseAttack(Bool) Parameter ์ถ๊ฐ
Transition ์์
'''์๋ต
[SerializeField] private string baseAttackParameterName = "BaseAttack";
'''์๋ต
public int BaseAttackParameterHash { get; private set; }
public class EnemyAttackState : EnemyBaseState
{
private bool alreadyAppliedForce;
public EnemyAttackState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
}
public override void Update()
{
base.Update();
ForceMove();
float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
if (normalizedTime < 1f)
{
if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
TryApplyForce();
}
else
{
if (IsInChaseRange()) // ๊ฑฐ๋ฆฌ ์ฒดํฌ
{
stateMachine.ChangeState(stateMachine.ChasingState);
return;
}
else
{
stateMachine.ChangeState(stateMachine.IdlingState);
return;
}
}
}
private void TryApplyForce()
{
if (alreadyAppliedForce) return;
alreadyAppliedForce = true;
stateMachine.Enemy.ForceReceiver.Reset();
stateMachine.Enemy.ForceReceiver.AddForce(stateMachine.Enemy.transform.forward * stateMachine.Enemy.Data.Force);
}
}
ํ๋ ์ด์ด ๋ณต์ - Enemy๋ก ์์
Player, Player Input โ Remove Component
Enemy ์ถ๊ฐ
EnemySO ์์ฑ ํ ์ฐ๊ฒฐ
public class Health : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
private int health;
public event Action OnDie;
public bool IsDead => health == 0;
private void Start()
{
health = maxHealth;
}
public void TakeDamage(int damage)
{
if (health == 0) return;
health = Math.Max(health - damage, 0); // 0๋ณด๋ค ๋ฐ์ผ๋ก ๋ด๋ ค๊ฐ๋๊ฑฐ ๋ฐฉ์ง
if (health == 0)
OnDie?.Invoke();
Debug.Log(health);
}
}
public class Weapon : MonoBehaviour
{
[SerializeField] private Collider myCollider;
private int damage;
private float knockback;
// <Collider> : ๋ชจ๋ Collider๋ค์ ์์ Class
private List<Collider> alreadyColliderWith = new List<Collider>();
private void OnEnable()
{
alreadyColliderWith.Clear(); // ์ด๊ธฐํ
}
private void OnTriggerEnter(Collider other)
{
if (other == myCollider) return; // ๋ ์์ ?
if (alreadyColliderWith.Contains(other)) return; // ์ด๋ฏธ ํฌํจ์ด ๋์ด์๋?
alreadyColliderWith.Add(other); // ๋ ๋ค ์๋๋ผ๋ฉด ์ถ๊ฐ(.Add)
if(other.TryGetComponent(out Health health))
{
health.TakeDamage(damage);
}
if(other.TryGetComponent(out ForceReciver forceReceiver))
{
Vector3 direction = (other.transform.position - myCollider.transform.position).normalized;
forceReceiver.AddForce(direction * knockback);
}
}
public void SetAttack(int damage, float knockback)
{
this.damage = damage;
this.knockback = knockback;
}
}
'''์๋ต
private bool alreadyAppliedDealing;
'''์๋ต
public override void Enter()
{
alreadyAppliedForce = false;
alreadyAppliedDealing = false;
stateMachine.MovementSpeedModifier = 0;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
'''์๋ต
public override void Update()
{
base.Update();
ForceMove();
float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
if (normalizedTime < 1f)
{
if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
TryApplyForce();
// ๊ณต๊ฒฉ ์ ๋๋ฉ์ด์
์ด ์งํ ์ค์ ์ ๋๋ฉ์ด์
์งํ์๋์ if๋ฌธ์ ๊ฑธ์ด weapon์ SetActive๋ฅผ
// ์ผ์ฃผ์ด ์ด๊ธฐํ(OnEnable)ํ ๋ฐ๋ฏธ์ง ์ฒ๋ฆฌ๋ฅผ ๋ฐ๊ฒ ํ๊ณ (Weapon.cs) ๊ณต๊ฒฉ ์ ๋๋ฉ์ด์
์ด ๋, ๊ณต๊ฒฉ์ด ๋๋๋ฉด
// weapon์ SetActive๋ฅผ ๊บผ์ฃผ์ด ๋ฐ๋ฏธ์ง๋ฅผ ๋ฐ์ง ์๊ฒ ํ๋ค.
// ๋์ ์์ง ์๋ฃ์ ์ํ && normalizedTime ์ด StartTime๋ณด๋ค ์ปค์ก๋ค๋ฉด?
if (!alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_Start_TransitionTime)
{
stateMachine.Enemy.Weapon.SetAttack(stateMachine.Enemy.Data.Damage, stateMachine.Enemy.Data.Force);
stateMachine.Enemy.Weapon.gameObject.SetActive(true);
alreadyAppliedDealing = true;
}
// ๋์ ๋ฃ์ ์ํ && EndTime์ ๋์๋ค๋ฉด?
if (alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_End_TransitionTime)
{
stateMachine.Enemy.Weapon.gameObject.SetActive(false);
}
}
else
{
if (IsInChaseRange()) // ๊ฑฐ๋ฆฌ ์ฒดํฌ
{
stateMachine.ChangeState(stateMachine.ChasingState);
return;
}
else
{
stateMachine.ChangeState(stateMachine.IdlingState);
return;
}
}
}
'''์๋ต
[field: SerializeField] public Weapon Weapon { get; private set; }
public CharacterHealth CharacterHealth { get; private set; }
void Awake()
{
'''์๋ต
CharacterHealth = GetComponent<CharacterHealth>();
stateMachine = new EnemyStateMachine(this);
}
private void Start()
{
stateMachine.ChangeState(stateMachine.IdlingState);
CharacterHealth.OnDie += OnDie;
}
'''์๋ต
void OnDie()
{
Animator.SetTrigger("Die");
enabled = false; // ์ค๋ธ์ ํธ์ ์ปดํฌ๋ํธ๋ฅผ ๋นํ์ฑํ.
}
}
[Player๋ ๋์ผํ๊ฒ ์ค์ (Player.cs, Weapon ์ค๋ธ์ ํธ]