2026-04-22 15:58:44 +00:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using Platformer.Gameplay;
|
|
|
|
|
|
using static Platformer.Core.Simulation;
|
|
|
|
|
|
using Platformer.Model;
|
|
|
|
|
|
using Platformer.Core;
|
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Platformer.Mechanics
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// This is the main class used to implement control of the player.
|
|
|
|
|
|
/// It is a superset of the AnimationController class, but is inlined to allow for any kind of customisation.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class PlayerController : KinematicObject
|
|
|
|
|
|
{
|
|
|
|
|
|
public AudioClip jumpAudio;
|
|
|
|
|
|
public AudioClip respawnAudio;
|
|
|
|
|
|
public AudioClip ouchAudio;
|
2026-04-23 14:47:51 +00:00
|
|
|
|
/// <summary>
|
2026-04-24 07:22:13 +00:00
|
|
|
|
/// Attack sound effect. 자동 발동 시 PlayerAttack.Execute가 PlayOneShot으로 재생.
|
|
|
|
|
|
/// BT7-Plan 2026-04-24 — VS 순수형 자동 발동 전환 후에도 audio hook 유지.
|
2026-04-23 14:47:51 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public AudioClip attackAudio;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Max horizontal speed of the player.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float maxSpeed = 7;
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Initial jump velocity at the start of a jump.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float jumpTakeOffSpeed = 7;
|
|
|
|
|
|
|
2026-04-23 14:47:51 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크).
|
2026-04-24 07:22:13 +00:00
|
|
|
|
/// 공격은 PlayerAttackTicker가 주기적으로 Schedule<PlayerAttack>을 발화하여 실행.
|
2026-04-23 14:47:51 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public AttackHitbox attackHitbox;
|
|
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
public JumpState jumpState = JumpState.Grounded;
|
|
|
|
|
|
private bool stopJump;
|
2026-05-07 15:26:12 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Drop-Through (Down + Jump) — 발판 위에서 Down 유지 + 점프 시 발판 통과 + 점프 모션 + 자연 낙하
|
|
|
|
|
|
private float dropThroughTimer = 0f;
|
|
|
|
|
|
private const float DROP_THROUGH_DURATION = 0.3f;
|
|
|
|
|
|
private bool dropThroughJump = false;
|
2026-05-07 15:35:02 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Jump Ascent — 점프 ascending·정점 영역 mask 강제 OFF 보장 (정점 jitter 차단)
|
2026-05-07 15:35:02 +00:00
|
|
|
|
private float jumpAscentTimer = 0f;
|
2026-05-07 15:49:26 +00:00
|
|
|
|
private const float JUMP_ASCENT_DURATION = 0.4f;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
/*internal new*/ public Collider2D collider2d;
|
|
|
|
|
|
/*internal new*/ public AudioSource audioSource;
|
|
|
|
|
|
public Health health;
|
|
|
|
|
|
public bool controlEnabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
bool jump;
|
|
|
|
|
|
Vector2 move;
|
|
|
|
|
|
SpriteRenderer spriteRenderer;
|
|
|
|
|
|
internal Animator animator;
|
|
|
|
|
|
readonly PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
|
|
|
|
|
|
|
|
|
|
|
private InputAction m_MoveAction;
|
|
|
|
|
|
private InputAction m_JumpAction;
|
2026-04-23 14:47:51 +00:00
|
|
|
|
|
2026-04-24 07:22:13 +00:00
|
|
|
|
// 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지).
|
|
|
|
|
|
// BT7-Plan 2026-04-24 — PlayerAttackTicker가 자동 발동 시 참조하므로 public 노출.
|
2026-04-23 14:47:51 +00:00
|
|
|
|
Vector2 facing = Vector2.right;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
2026-04-24 07:22:13 +00:00
|
|
|
|
/// <summary>현재 플레이어 facing 방향. PlayerAttackTicker가 Schedule 시점에 참조.</summary>
|
|
|
|
|
|
public Vector2 Facing => facing;
|
|
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
public Bounds Bounds => collider2d.bounds;
|
|
|
|
|
|
|
2026-05-07 06:29:34 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 마지막 grounded 위치 — 낙사 시 안전 복귀 영역 (PD 지시 2026-05-07).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public Vector3 LastGroundedPosition { get; private set; }
|
|
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
health = GetComponent<Health>();
|
|
|
|
|
|
audioSource = GetComponent<AudioSource>();
|
|
|
|
|
|
collider2d = GetComponent<Collider2D>();
|
|
|
|
|
|
spriteRenderer = GetComponent<SpriteRenderer>();
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (spriteRenderer == null) spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
2026-04-22 15:58:44 +00:00
|
|
|
|
animator = GetComponent<Animator>();
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (animator == null) animator = GetComponentInChildren<Animator>();
|
|
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 동반 컴포넌트 자동 부착 (Inspector 부착 불요)
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (GetComponent<PlayerInvulnerabilityFlash>() == null) gameObject.AddComponent<PlayerInvulnerabilityFlash>();
|
|
|
|
|
|
if (GetComponent<Platformer.UI.ResurrectPromptUI>() == null) gameObject.AddComponent<Platformer.UI.ResurrectPromptUI>();
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 구 PlatformDropThrough 컴포넌트 자동 제거 (Drop-Through는 ContactFilter mask 동적 갱신으로 처리)
|
2026-05-07 08:49:58 +00:00
|
|
|
|
var oldDrop = GetComponent<PlatformDropThrough>();
|
|
|
|
|
|
if (oldDrop != null) Destroy(oldDrop);
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 사망 시 입력 차단 / 부활 시 입력 복원
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (health != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
health.OnDeathEvent += OnHealthDeath;
|
|
|
|
|
|
health.OnResurrectEvent += OnHealthResurrect;
|
|
|
|
|
|
}
|
2026-04-23 14:47:51 +00:00
|
|
|
|
if (attackHitbox == null) attackHitbox = GetComponent<AttackHitbox>();
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
|
|
|
|
|
m_MoveAction = InputSystem.actions.FindAction("Player/Move");
|
|
|
|
|
|
m_JumpAction = InputSystem.actions.FindAction("Player/Jump");
|
2026-04-23 14:47:51 +00:00
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
m_MoveAction.Enable();
|
|
|
|
|
|
m_JumpAction.Enable();
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
|
|
|
|
|
LastGroundedPosition = transform.position;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (health != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
health.OnDeathEvent -= OnHealthDeath;
|
|
|
|
|
|
health.OnResurrectEvent -= OnHealthResurrect;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnHealthDeath()
|
|
|
|
|
|
{
|
|
|
|
|
|
controlEnabled = false;
|
|
|
|
|
|
move = Vector2.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnHealthResurrect()
|
|
|
|
|
|
{
|
|
|
|
|
|
controlEnabled = true;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (controlEnabled)
|
|
|
|
|
|
{
|
2026-05-07 15:26:12 +00:00
|
|
|
|
Vector2 moveInput = m_MoveAction.ReadValue<Vector2>();
|
|
|
|
|
|
move.x = moveInput.x;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
if (jumpState == JumpState.Grounded && m_JumpAction.WasPressedThisFrame())
|
2026-05-07 15:26:12 +00:00
|
|
|
|
{
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Down + Jump 발판 위 = Drop-Through 발동 / 지면 위 = 일반 점프
|
2026-05-07 15:26:12 +00:00
|
|
|
|
bool downHeld = moveInput.y < -0.5f;
|
2026-05-07 15:28:29 +00:00
|
|
|
|
bool onJumpThroughPlatform = false;
|
|
|
|
|
|
if (downHeld && collider2d != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector2 dropFootPos = new Vector2(collider2d.bounds.center.x, collider2d.bounds.min.y + 0.02f);
|
|
|
|
|
|
int jumpThroughMask = 1 << JUMP_THROUGH_LAYER;
|
|
|
|
|
|
RaycastHit2D dropFootHit = Physics2D.Raycast(dropFootPos, Vector2.down, 0.1f, jumpThroughMask);
|
|
|
|
|
|
onJumpThroughPlatform = dropFootHit.collider != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (downHeld && onJumpThroughPlatform)
|
2026-05-07 15:26:12 +00:00
|
|
|
|
{
|
|
|
|
|
|
dropThroughTimer = DROP_THROUGH_DURATION;
|
|
|
|
|
|
dropThroughJump = true;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
jumpState = JumpState.PrepareToJump;
|
2026-05-07 15:26:12 +00:00
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
else if (m_JumpAction.WasReleasedThisFrame())
|
|
|
|
|
|
{
|
|
|
|
|
|
stopJump = true;
|
|
|
|
|
|
Schedule<PlayerStopJump>().player = this;
|
|
|
|
|
|
}
|
2026-04-24 07:22:13 +00:00
|
|
|
|
// 공격은 PlayerAttackTicker가 자동 주기로 Schedule<PlayerAttack>을 발화 (BT7-Plan VS 순수형).
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
move.x = 0;
|
|
|
|
|
|
}
|
2026-05-07 15:26:12 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Drop-Through·Jump Ascent Timer 감소 (mask 강제 OFF 지속 시간)
|
2026-05-07 15:26:12 +00:00
|
|
|
|
if (dropThroughTimer > 0f) dropThroughTimer -= Time.deltaTime;
|
2026-05-07 15:35:02 +00:00
|
|
|
|
if (jumpAscentTimer > 0f) jumpAscentTimer -= Time.deltaTime;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
UpdateJumpState();
|
|
|
|
|
|
base.Update();
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 낙사 시 복귀할 안전 위치 추적
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (IsGrounded) LastGroundedPosition = transform.position;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Drop-Through Layer (Foreground·AutoForeground GameObject Layer 16).
|
|
|
|
|
|
// KinematicObject body.Cast의 contactFilter는 Start() 시점 캐싱되므로 Drop-Through는 SetLayerMask 동적 갱신으로 처리.
|
2026-05-07 09:11:09 +00:00
|
|
|
|
const int JUMP_THROUGH_LAYER = 16;
|
|
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
void UpdateJumpState()
|
|
|
|
|
|
{
|
|
|
|
|
|
jump = false;
|
|
|
|
|
|
switch (jumpState)
|
|
|
|
|
|
{
|
|
|
|
|
|
case JumpState.PrepareToJump:
|
|
|
|
|
|
jumpState = JumpState.Jumping;
|
|
|
|
|
|
jump = true;
|
|
|
|
|
|
stopJump = false;
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 일반 점프만 ascending 통과 보장 Timer 활성 (Drop-Through는 dropThroughTimer로 처리)
|
2026-05-07 15:35:02 +00:00
|
|
|
|
if (!dropThroughJump) jumpAscentTimer = JUMP_ASCENT_DURATION;
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// PrepareToJump frame 즉시 mask OFF — 다음 frame ComputeVelocity의 velocity.y 적용 직전 발판 충돌 차단
|
2026-05-07 09:23:46 +00:00
|
|
|
|
{
|
|
|
|
|
|
int baseMaskJump = Physics2D.GetLayerCollisionMask(gameObject.layer);
|
|
|
|
|
|
contactFilter.SetLayerMask(baseMaskJump & ~(1 << JUMP_THROUGH_LAYER));
|
|
|
|
|
|
contactFilter.useLayerMask = true;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
break;
|
|
|
|
|
|
case JumpState.Jumping:
|
|
|
|
|
|
if (!IsGrounded)
|
|
|
|
|
|
{
|
|
|
|
|
|
Schedule<PlayerJumped>().player = this;
|
|
|
|
|
|
jumpState = JumpState.InFlight;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case JumpState.InFlight:
|
|
|
|
|
|
if (IsGrounded)
|
|
|
|
|
|
{
|
|
|
|
|
|
Schedule<PlayerLanded>().player = this;
|
|
|
|
|
|
jumpState = JumpState.Landed;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case JumpState.Landed:
|
|
|
|
|
|
jumpState = JumpState.Grounded;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-05-07 09:16:01 +00:00
|
|
|
|
|
|
|
|
|
|
UpdateContactFilterForDropThrough();
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Drop-Through 패턴: ascending·정점·Drop-Through Timer 활성 시 Layer 16 mask OFF (통과).
|
|
|
|
|
|
/// 그 외 = footHit 3점 Raycast (Layer 16) — 발판 가장자리 안정 검출.
|
|
|
|
|
|
/// 점프·낙하 중 정점 영역 + 수평 입력 + 발판 가장자리 일시 검출 = 밀림 상태 → 강제 Drop-Through 발동.
|
|
|
|
|
|
/// </summary>
|
2026-05-07 09:16:01 +00:00
|
|
|
|
void UpdateContactFilterForDropThrough()
|
2026-05-07 09:11:09 +00:00
|
|
|
|
{
|
2026-05-07 09:16:01 +00:00
|
|
|
|
int baseMask = Physics2D.GetLayerCollisionMask(gameObject.layer);
|
2026-05-07 09:26:54 +00:00
|
|
|
|
|
2026-05-07 09:28:29 +00:00
|
|
|
|
bool isJumpingThrough = jumpState == JumpState.Jumping
|
2026-05-07 15:26:12 +00:00
|
|
|
|
|| (jumpState == JumpState.InFlight && velocity.y > 0.01f)
|
2026-05-07 15:35:02 +00:00
|
|
|
|
|| dropThroughTimer > 0f
|
|
|
|
|
|
|| jumpAscentTimer > 0f;
|
2026-05-07 09:28:29 +00:00
|
|
|
|
|
2026-05-07 09:26:54 +00:00
|
|
|
|
bool standingOnPlatform = false;
|
2026-05-07 09:28:29 +00:00
|
|
|
|
if (collider2d != null && !isJumpingThrough)
|
2026-05-07 09:26:54 +00:00
|
|
|
|
{
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// footHit Raycast 3점 (좌·중·우) — 가장자리 jitter 차단
|
2026-05-07 15:37:58 +00:00
|
|
|
|
float footY = collider2d.bounds.min.y + 0.02f;
|
|
|
|
|
|
float boundsLeft = collider2d.bounds.min.x + 0.02f;
|
|
|
|
|
|
float boundsCenter = collider2d.bounds.center.x;
|
|
|
|
|
|
float boundsRight = collider2d.bounds.max.x - 0.02f;
|
2026-05-07 09:26:54 +00:00
|
|
|
|
int jumpThroughMask = 1 << JUMP_THROUGH_LAYER;
|
2026-05-07 15:37:58 +00:00
|
|
|
|
RaycastHit2D footHitC = Physics2D.Raycast(new Vector2(boundsCenter, footY), Vector2.down, 0.1f, jumpThroughMask);
|
|
|
|
|
|
RaycastHit2D footHitL = Physics2D.Raycast(new Vector2(boundsLeft, footY), Vector2.down, 0.1f, jumpThroughMask);
|
|
|
|
|
|
RaycastHit2D footHitR = Physics2D.Raycast(new Vector2(boundsRight, footY), Vector2.down, 0.1f, jumpThroughMask);
|
|
|
|
|
|
standingOnPlatform = footHitC.collider != null || footHitL.collider != null || footHitR.collider != null;
|
2026-05-07 15:43:25 +00:00
|
|
|
|
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// 밀림 상태 강제 Drop-Through (점프·낙하 중 정점 + 수평 입력 + 발판 가장자리 일시 검출)
|
2026-05-07 15:43:25 +00:00
|
|
|
|
bool inAir = jumpState == JumpState.Jumping || jumpState == JumpState.InFlight;
|
|
|
|
|
|
bool nearApex = velocity.y > -1.5f;
|
|
|
|
|
|
bool horizontalIntent = Mathf.Abs(move.x) > 0.1f;
|
|
|
|
|
|
if (standingOnPlatform && inAir && nearApex && horizontalIntent)
|
|
|
|
|
|
{
|
|
|
|
|
|
dropThroughTimer = DROP_THROUGH_DURATION;
|
|
|
|
|
|
standingOnPlatform = false;
|
|
|
|
|
|
}
|
2026-05-07 09:26:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int mask = standingOnPlatform ? baseMask : (baseMask & ~(1 << JUMP_THROUGH_LAYER));
|
2026-05-07 09:16:01 +00:00
|
|
|
|
contactFilter.SetLayerMask(mask);
|
|
|
|
|
|
contactFilter.useLayerMask = true;
|
2026-05-07 09:11:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
protected override void ComputeVelocity()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (jump && IsGrounded)
|
|
|
|
|
|
{
|
2026-05-07 15:49:26 +00:00
|
|
|
|
// Drop-Through 점프: 위로 점프 X (음수 velocity.y로 즉시 낙하 시작 + 점프 애니메이션만 유지)
|
2026-05-07 15:26:12 +00:00
|
|
|
|
if (dropThroughJump)
|
|
|
|
|
|
{
|
2026-05-07 15:49:26 +00:00
|
|
|
|
velocity.y = -0.5f;
|
2026-05-07 15:26:12 +00:00
|
|
|
|
dropThroughJump = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
velocity.y = jumpTakeOffSpeed * model.jumpModifier;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
jump = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (stopJump)
|
|
|
|
|
|
{
|
|
|
|
|
|
stopJump = false;
|
|
|
|
|
|
if (velocity.y > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
velocity.y = velocity.y * model.jumpDeceleration;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (move.x > 0.01f)
|
2026-04-23 14:47:51 +00:00
|
|
|
|
{
|
2026-05-07 06:29:34 +00:00
|
|
|
|
spriteRenderer.flipX = true;
|
2026-04-23 14:47:51 +00:00
|
|
|
|
facing = Vector2.right;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
else if (move.x < -0.01f)
|
2026-04-23 14:47:51 +00:00
|
|
|
|
{
|
2026-05-07 06:29:34 +00:00
|
|
|
|
spriteRenderer.flipX = false;
|
2026-04-23 14:47:51 +00:00
|
|
|
|
facing = Vector2.left;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
|
|
|
|
|
animator.SetBool("grounded", IsGrounded);
|
|
|
|
|
|
animator.SetFloat("velocityX", Mathf.Abs(velocity.x) / maxSpeed);
|
|
|
|
|
|
|
|
|
|
|
|
targetVelocity = move * maxSpeed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public enum JumpState
|
|
|
|
|
|
{
|
|
|
|
|
|
Grounded,
|
|
|
|
|
|
PrepareToJump,
|
|
|
|
|
|
Jumping,
|
|
|
|
|
|
InFlight,
|
|
|
|
|
|
Landed
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|