This commit is contained in:
Scove
2026-03-26 20:27:19 +07:00
parent a94ab0e3f6
commit f42ef22a13
129 changed files with 5517 additions and 1134 deletions

View File

@@ -0,0 +1,89 @@
using System;
using UnityEngine;
using UnityEngine.InputSystem;
namespace OnlyScove.Scripts
{
public class InputReader : MonoBehaviour
{
// Continuous Inputs
public virtual Vector2 MoveInput { get; protected set; }
public virtual Vector2 LookInput { get; protected set; }
public virtual Vector2 ScrollInput { get; protected set; }
public virtual bool IsSprintHeld { get; protected set; } // Left Shift
public virtual bool IsAttackHeld { get; protected set; } // Left Mouse Button
// One-shot Events
public event Action OnJumpEvent; // Space
public event Action OnDodgeEvent; // Right Mouse Button (RMB)
public event Action OnAttackEvent; // Left Mouse Button (LMB)
public event Action OnCrouchEvent; // C Key
public event Action OnInteractEvent; // E Key
public event Action OnNextInteractEvent; // R Key
public event Action OnPreviousInteractEvent; // Q Key
public void OnAttack(InputAction.CallbackContext context)
{
if (context.performed)
{
OnAttackEvent?.Invoke();
IsAttackHeld = true;
}
if (context.canceled)
{
IsAttackHeld = false;
}
}
public void OnMove(InputAction.CallbackContext context)
{
MoveInput = context.ReadValue<Vector2>();
}
public void OnLook(InputAction.CallbackContext context)
{
LookInput = context.ReadValue<Vector2>();
}
public void OnScroll(InputAction.CallbackContext context)
{
ScrollInput = context.ReadValue<Vector2>();
}
public void OnSprint(InputAction.CallbackContext context)
{
if (context.performed) IsSprintHeld = true;
if (context.canceled) IsSprintHeld = false;
}
public void OnJump(InputAction.CallbackContext context)
{
if (context.performed) OnJumpEvent?.Invoke();
}
public void OnDodgeOrThrust(InputAction.CallbackContext context)
{
if (context.performed) OnDodgeEvent?.Invoke();
}
public void OnCrouch(InputAction.CallbackContext context)
{
if (context.performed) OnCrouchEvent?.Invoke();
}
public void OnInteract(InputAction.CallbackContext context)
{
if (context.performed) OnInteractEvent?.Invoke();
}
public void OnNext(InputAction.CallbackContext context)
{
if (context.performed) OnNextInteractEvent?.Invoke();
}
public void OnPrevious(InputAction.CallbackContext context)
{
if (context.performed) OnPreviousInteractEvent?.Invoke();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5962d8f2c8e40e240a4a4907c7b539fa

View File

@@ -0,0 +1,12 @@
using UnityEngine;
namespace OnlyScove.Game_Definition
{
[CreateAssetMenu(fileName = "ParkourAction", menuName = "Parkour System/New Parkour Action")]
public class ParkourAction : ScriptableObject
{
[SerializeField] string animationName;
[SerializeField] private float minHeight;
[SerializeField] private float maxHeight;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 985468dedaf32f44191b2cc29c813c8c

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections;
using UnityEngine;
namespace OnlyScove.Scripts
{
public class ParkourController : MonoBehaviour
{
[SerializeField] private InputReader inputReader;
EnvironmentScanner environmentScanner;
Animator animator;
PlayerController playerController;
bool inAction;
private void Awake()
{
inputReader = GetComponent<InputReader>();
environmentScanner = GetComponent<EnvironmentScanner>();
animator = GetComponent<Animator>();
playerController = GetComponent<PlayerController>();
}
private void OnEnable()
{
inputReader.OnJumpEvent += HandleParkour;
}
private void OnDisable()
{
inputReader.OnJumpEvent -= HandleParkour;
}
private void HandleParkour()
{
var hitData = environmentScanner.ObstacleCheck();
if (hitData.forwardHitFound)
{
StartCoroutine(DoParkourAction());
}
}
IEnumerator DoParkourAction()
{
inAction = true;
playerController.SetControl(false);
animator.CrossFade("Step Up", 0.1f);
yield return null;
var animationState = animator.GetNextAnimatorStateInfo(0);
yield return new WaitForSeconds(animationState.length);
playerController.SetControl(true);
inAction = false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0407ea3fbe445ac43b4f1ca3077ce283

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerAirDashState : PlayerBaseState
{
private readonly int airDashHash = Animator.StringToHash("AirDash");
private float dashDuration = 0.2f;
private float dashTimer;
private Vector3 dashDirection;
public PlayerAirDashState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
dashTimer = dashDuration;
stateMachine.Anim.SetTrigger(airDashHash);
// Reset vertical velocity so we don't carry falling momentum into the dash
stateMachine.VelocityY = 0f;
// Determine dash direction
Vector2 input = stateMachine.Input.MoveInput;
if (input != Vector2.zero)
{
dashDirection = new Vector3(input.x, 0f, input.y).normalized;
if (stateMachine.Cam != null)
{
dashDirection = stateMachine.Cam.PlanarRotation * dashDirection;
}
stateMachine.transform.rotation = Quaternion.LookRotation(dashDirection);
}
else
{
dashDirection = stateMachine.transform.forward;
}
}
public override void Tick(float deltaTime)
{
dashTimer -= deltaTime;
// Move horizontally, ignoring gravity for this brief moment
stateMachine.Controller.Move(dashDirection * stateMachine.DashForce * deltaTime);
// When the air dash ends, return to falling
if (dashTimer <= 0f)
{
stateMachine.SwitchState(new PlayerFallState(stateMachine));
}
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit() {}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43c3e5e400fea604fb432f84d5dd9ce1

View File

@@ -0,0 +1,25 @@
namespace OnlyScove.Scripts
{
public abstract class PlayerBaseState
{
protected PlayerStateMachine stateMachine;
// Constructor to pass the state machine reference
public PlayerBaseState(PlayerStateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
// Called once when entering the state
public abstract void Enter();
// Called every frame (equivalent to Update)
public abstract void Tick(float deltaTime);
// Called every physics frame (equivalent to FixedUpdate)
public abstract void PhysicsTick(float fixedDeltaTime);
// Called once before switching to a new state
public abstract void Exit();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: af622221f1750284885366b02b8bfbba

View File

@@ -0,0 +1,134 @@
using System;
using Unity.VisualScripting;
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerController : MonoBehaviour
{
private static readonly int MoveAmount = Animator.StringToHash("moveAmount");
[SerializeField] private InputReader inputReader;
[SerializeField] private float rotationSpeed = 500f;
[SerializeField] private float moveSpeed = 10f;
[SerializeField] private float jumpHeight = 2f;
[SerializeField] private float animationDamping = 0.2f;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private Vector3 groundCheckOffset;
[SerializeField] private LayerMask groundMask;
CameraController cameraController;
Animator animator;
private CharacterController characterController;
Quaternion targetRotation;
private float horizontal;
private float vertical;
bool isGrounded;
private bool wasGrounded;
private bool hasControl = true;
private float ySpeed;
private void Awake()
{
if (Camera.main != null) cameraController = Camera.main.GetComponent<CameraController>();
animator = GetComponent<Animator>();
characterController = GetComponent<CharacterController>();
}
private void OnEnable()
{
inputReader.OnJumpEvent += HandleJump;
}
private void OnDisable()
{
inputReader.OnJumpEvent -= HandleJump;
}
private void HandleJump()
{
if (isGrounded && hasControl)
{
// Công thức tính vận tốc nhảy: v = sqrt(h * -2 * g)
ySpeed = Mathf.Sqrt(jumpHeight * -2f * Physics.gravity.y);
}
}
private void Update()
{
horizontal = inputReader.MoveInput.x;
vertical = inputReader.MoveInput.y;
float moveAmount = Mathf.Clamp01(Math.Abs(horizontal) + Math.Abs(vertical));
var moveInput = (new Vector3(horizontal, 0, vertical)).normalized;
var moveDirection = cameraController.PlanarRotation * moveInput;
if (!hasControl)
return;
wasGrounded = isGrounded;
GroundCheck();
// Phát hiện tiếp đất (Landing)
if (isGrounded && !wasGrounded && ySpeed < -1f)
{
// Rung camera khi tiếp đất mạnh
if (cameraController != null)
{
cameraController.Shake(0.2f, 0.15f);
}
}
if (isGrounded && ySpeed < 0)
{
ySpeed = -2f; // Giữ nhân vật dính xuống mặt đất
}
else
{
ySpeed += Physics.gravity.y * Time.deltaTime;
}
var velocity = moveDirection * moveSpeed;
velocity.y = ySpeed;
characterController.Move(velocity * Time.deltaTime);
if (moveAmount > 0)
{
targetRotation = Quaternion.LookRotation(moveDirection);
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
Time.deltaTime * rotationSpeed);
animator.SetFloat(MoveAmount, moveAmount, animationDamping, Time.deltaTime);
}
void GroundCheck()
{
isGrounded =
Physics.CheckSphere(transform.TransformPoint(groundCheckOffset), groundCheckRadius, groundMask);
}
public void SetControl(bool control)
{
this.hasControl = control;
characterController.enabled = hasControl;
if (!hasControl)
{
animator.SetFloat(MoveAmount, 0f);
targetRotation = transform.rotation;
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(0, 1, 0, 0.5f);
Gizmos.DrawSphere(transform.TransformPoint(groundCheckOffset), groundCheckRadius);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cd897e8bcaabdc8408bb6aaf7c037537

View File

@@ -0,0 +1,97 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerCrouchState : PlayerBaseState
{
private readonly int isCrouchingHash = Animator.StringToHash("IsCrouching");
private readonly int crouchSpeedHash = Animator.StringToHash("CrouchSpeed");
private const float AnimatorDampTime = 0.1f;
public PlayerCrouchState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Anim.SetBool(isCrouchingHash, true);
stateMachine.Input.OnCrouchEvent += OnCrouchToggle;
stateMachine.Input.OnDodgeEvent += OnDodge;
}
public override void Tick(float deltaTime)
{
Vector2 moveInput = stateMachine.Input.MoveInput;
float currentSpeed = 0f;
float animValue = 0f;
if (moveInput == Vector2.zero)
{
animValue = 0f;
}
else
{
if (stateMachine.Input.IsSprintHeld)
{
currentSpeed = stateMachine.SneakSpeed;
animValue = 0.5f;
}
else
{
currentSpeed = stateMachine.WalkSpeed;
animValue = 1.0f;
}
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y).normalized;
if (stateMachine.Cam != null)
{
moveDirection = stateMachine.Cam.PlanarRotation * moveDirection;
}
// Apply horizontal movement
Vector3 movement = moveDirection * currentSpeed;
// Apply gravity
if (stateMachine.Controller.isGrounded)
{
stateMachine.VelocityY = -2f;
}
else
{
stateMachine.VelocityY += stateMachine.Gravity * deltaTime;
}
movement.y = stateMachine.VelocityY;
stateMachine.Controller.Move(movement * deltaTime);
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
stateMachine.transform.rotation = Quaternion.Slerp(
stateMachine.transform.rotation,
targetRotation,
deltaTime * 10f
);
}
stateMachine.Anim.SetFloat(crouchSpeedHash, animValue, AnimatorDampTime, deltaTime);
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Anim.SetBool(isCrouchingHash, false);
stateMachine.Input.OnCrouchEvent -= OnCrouchToggle;
stateMachine.Input.OnDodgeEvent -= OnDodge;
}
private void OnCrouchToggle()
{
if (stateMachine.Input.MoveInput == Vector2.zero)
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
else
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82b04241d720e444a937973caed8eb42

View File

@@ -0,0 +1,86 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerDashState : PlayerBaseState
{
private readonly int dashHash = Animator.StringToHash("Dash"); // Trigger parameter
private float dashDuration = 0.25f; // How long the burst lasts (tweak as needed)
private float dashTimer;
private Vector3 dashDirection;
public PlayerDashState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
dashTimer = dashDuration;
// Fire the Dash animation trigger
stateMachine.Anim.SetTrigger(dashHash);
stateMachine.Input.OnJumpEvent += OnJump;
// Determine dash direction based on input, or default to forward if no input
Vector2 input = stateMachine.Input.MoveInput;
if (input != Vector2.zero)
{
dashDirection = new Vector3(input.x, 0f, input.y).normalized;
if (stateMachine.Cam != null)
{
dashDirection = stateMachine.Cam.PlanarRotation * dashDirection;
}
// Instantly snap rotation to face the dash direction
stateMachine.transform.rotation = Quaternion.LookRotation(dashDirection);
}
else
{
dashDirection = stateMachine.transform.forward;
}
}
public override void Tick(float deltaTime)
{
dashTimer -= deltaTime;
// Apply high speed for the dash (Burst speed)
stateMachine.Controller.Move(dashDirection * stateMachine.SprintSpeed * deltaTime);
// When the dash finishes, decide the next state
if (dashTimer <= 0f)
{
if (stateMachine.Input.IsSprintHeld && stateMachine.Input.MoveInput != Vector2.zero)
{
// Kept holding Shift -> Go to Sprint
stateMachine.SwitchState(new PlayerRunState(stateMachine));
}
else if (stateMachine.Input.MoveInput != Vector2.zero)
{
// Released Shift but still moving -> Go to Walk
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
else
{
// Stopped moving -> Go to Idle
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
}
}
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Input.OnJumpEvent -= OnJump;
}
private void OnJump()
{
if (stateMachine.IsGrounded)
{
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.SprintSpeed));
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 03721c0a319be8c4093b616e2f46afe8

View File

@@ -0,0 +1,65 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerDodgeState : PlayerBaseState
{
private readonly int dodgeHash = Animator.StringToHash("Dodge");
private float dodgeDuration = 0.4f; // Adjust based on your dodge animation length
private float dodgeTimer;
private Vector3 dodgeDirection;
public PlayerDodgeState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
dodgeTimer = dodgeDuration;
stateMachine.Anim.SetTrigger(dodgeHash);
// Calculate dodge direction based on current input
Vector2 input = stateMachine.Input.MoveInput;
if (input != Vector2.zero)
{
// Dodge in the input direction (Left, Right, Back, Forward)
dodgeDirection = new Vector3(input.x, 0f, input.y).normalized;
if (stateMachine.Cam != null)
{
dodgeDirection = stateMachine.Cam.PlanarRotation * dodgeDirection;
}
// Instantly rotate the player to face the dodge direction
stateMachine.transform.rotation = Quaternion.LookRotation(dodgeDirection);
}
else
{
// If no input, just roll straight forward
dodgeDirection = stateMachine.transform.forward;
}
}
public override void Tick(float deltaTime)
{
dodgeTimer -= deltaTime;
// Apply movement force for the dodge (usually slightly slower than a Dash)
stateMachine.Controller.Move(dodgeDirection * (stateMachine.DashForce * 0.8f) * deltaTime);
// Once the roll finishes, transition back to Idle or Move
if (dodgeTimer <= 0f)
{
if (stateMachine.Input.MoveInput != Vector2.zero)
{
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
else
{
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
}
}
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit() {}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 71bce3b0373df774c8b1598487bcd4ee

View File

@@ -0,0 +1,82 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerFallState : PlayerBaseState
{
private readonly int fallHash = Animator.StringToHash("Fall");
private float fallSpeed;
public PlayerFallState(PlayerStateMachine stateMachine, float fallSpeed = -1f) : base(stateMachine)
{
if (fallSpeed < 0)
{
this.fallSpeed = stateMachine.WalkSpeed;
}
else
{
this.fallSpeed = fallSpeed;
}
}
public override void Enter()
{
stateMachine.Anim.SetTrigger(fallHash);
stateMachine.Input.OnDodgeEvent += OnThrustPressed;
}
public override void Tick(float deltaTime)
{
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
Vector2 input = stateMachine.Input.MoveInput;
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
Vector3 velocity = moveDirection * fallSpeed;
velocity.y = stateMachine.VelocityY;
stateMachine.Controller.Move(velocity * deltaTime);
if (stateMachine.Input.IsSprintHeld)
{
stateMachine.SwitchState(new PlayerAirDashState(stateMachine));
return;
}
if (stateMachine.IsGrounded)
{
// Landing Shake from PlayerController.cs
if (!stateMachine.WasGrounded && stateMachine.VelocityY < -1f)
{
if (stateMachine.Cam != null)
{
stateMachine.Cam.Shake(0.2f, 0.15f);
}
}
stateMachine.VelocityY = -2f;
if (input == Vector2.zero)
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
else
{
// Return to the appropriate movement state based on sprint input
if (stateMachine.Input.IsSprintHeld)
stateMachine.SwitchState(new PlayerRunState(stateMachine));
else
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
}
}
private void OnThrustPressed() => stateMachine.SwitchState(new PlayerThrustState(stateMachine));
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Input.OnDodgeEvent -= OnThrustPressed;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e2b239715e32b6a4791f677e0159b862

View File

@@ -0,0 +1,71 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerIdleState : PlayerBaseState
{
private readonly int speedHash = Animator.StringToHash("Speed");
public PlayerIdleState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Input.OnJumpEvent += OnJump;
stateMachine.Input.OnDodgeEvent += OnDodge;
stateMachine.Input.OnCrouchEvent += OnCrouch;
stateMachine.Input.OnInteractEvent += OnInteract;
}
public override void Tick(float deltaTime)
{
stateMachine.Anim.SetFloat(speedHash, 0f, stateMachine.AnimationDamping, deltaTime);
if (stateMachine.Input.MoveInput != Vector2.zero)
{
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
return;
}
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
{
stateMachine.VelocityY = -2f;
}
else
{
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
}
stateMachine.Controller.Move(new Vector3(0, stateMachine.VelocityY, 0) * deltaTime);
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Input.OnJumpEvent -= OnJump;
stateMachine.Input.OnDodgeEvent -= OnDodge;
stateMachine.Input.OnCrouchEvent -= OnCrouch;
stateMachine.Input.OnInteractEvent -= OnInteract;
}
private void OnJump()
{
if (stateMachine.IsGrounded)
{
if (stateMachine.Scanner != null)
{
var hitData = stateMachine.Scanner.ObstacleCheck();
if (hitData.forwardHitFound)
{
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
return;
}
}
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.WalkSpeed));
}
}
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
private void OnInteract() => stateMachine.SwitchState(new PlayerInteractState(stateMachine));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fd83e34f11c7aaa4ab144ee59db04e8d

View File

@@ -0,0 +1,38 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerInteractState : PlayerBaseState
{
public PlayerInteractState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
// Lấy vật thể đang được chọn (Index hiện tại)
IInteractable interactable = stateMachine.GetInteractable();
if (interactable != null)
{
Debug.Log($"[Interaction] Interacting with: {interactable.InteractionPrompt}");
interactable.OnInteract(stateMachine);
// Bạn có thể phát animation tương tác ở đây
// stateMachine.Anim.CrossFadeInFixedTime("Interact", 0.1f);
}
// Chuyển về trạng thái di chuyển hoặc đứng yên ngay lập tức
if (stateMachine.Input.MoveInput == Vector2.zero)
{
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
}
else
{
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
}
public override void Tick(float deltaTime) { }
public override void PhysicsTick(float fixedDeltaTime) { }
public override void Exit() { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0b772d1c0c26c634fad5e71b43db9385

View File

@@ -0,0 +1,52 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerJumpState : PlayerBaseState
{
private readonly int jumpHash = Animator.StringToHash("Jump");
private float jumpSpeed;
public PlayerJumpState(PlayerStateMachine stateMachine, float jumpSpeed = -1f) : base(stateMachine)
{
if (jumpSpeed < 0)
{
this.jumpSpeed = stateMachine.WalkSpeed;
}
else
{
this.jumpSpeed = jumpSpeed;
}
}
public override void Enter()
{
stateMachine.Anim.SetTrigger(jumpHash);
// Physic formula: v = sqrt(h * -2 * g)
stateMachine.VelocityY = Mathf.Sqrt(stateMachine.JumpHeight * -2f * Physics.gravity.y);
}
public override void Tick(float deltaTime)
{
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
Vector2 input = stateMachine.Input.MoveInput;
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
Vector3 velocity = moveDirection * jumpSpeed;
velocity.y = stateMachine.VelocityY;
stateMachine.Controller.Move(velocity * deltaTime);
if (stateMachine.VelocityY <= 0f)
{
stateMachine.SwitchState(new PlayerFallState(stateMachine, jumpSpeed));
}
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit() {}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ab5bb519df10fe45a5af5f45111ed4d

View File

@@ -0,0 +1,97 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerMoveState : PlayerBaseState
{
private readonly int speedHash = Animator.StringToHash("Speed");
public PlayerMoveState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Input.OnJumpEvent += OnJump;
stateMachine.Input.OnDodgeEvent += OnDodge;
stateMachine.Input.OnCrouchEvent += OnCrouch;
stateMachine.Input.OnInteractEvent += OnInteract;
}
public override void Tick(float deltaTime)
{
Vector2 input = stateMachine.Input.MoveInput;
float moveAmount = Mathf.Clamp01(Mathf.Abs(input.x) + Mathf.Abs(input.y));
if (moveAmount <= 0.01f)
{
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
return;
}
if (stateMachine.Input.IsSprintHeld)
{
stateMachine.SwitchState(new PlayerDashState(stateMachine));
return;
}
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
Vector3 velocity = moveDirection * stateMachine.RunSpeed;
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
{
stateMachine.VelocityY = -2f;
}
else
{
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
}
velocity.y = stateMachine.VelocityY;
stateMachine.Controller.Move(velocity * deltaTime);
if (moveDirection != Vector3.zero)
{
Quaternion targetRot = Quaternion.LookRotation(moveDirection);
stateMachine.transform.rotation = Quaternion.RotateTowards(
stateMachine.transform.rotation,
targetRot,
stateMachine.RotationSpeed * deltaTime
);
}
stateMachine.Anim.SetFloat(speedHash, 0.7f, stateMachine.AnimationDamping, deltaTime);
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Input.OnJumpEvent -= OnJump;
stateMachine.Input.OnDodgeEvent -= OnDodge;
stateMachine.Input.OnCrouchEvent -= OnCrouch;
stateMachine.Input.OnInteractEvent -= OnInteract;
}
private void OnJump()
{
if (stateMachine.IsGrounded)
{
if (stateMachine.Scanner != null)
{
var hitData = stateMachine.Scanner.ObstacleCheck();
if (hitData.forwardHitFound)
{
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
return;
}
}
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.RunSpeed));
}
}
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
private void OnInteract() => stateMachine.SwitchState(new PlayerInteractState(stateMachine));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a11b0f0549f3dbd45bbdfd8114586a43

View File

@@ -0,0 +1,53 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerParkourState : PlayerBaseState
{
private readonly int parkourHash = Animator.StringToHash("Step Up");
private float animationDuration;
private float timer;
public PlayerParkourState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
// Play the Parkour animation (Step Up)
stateMachine.Anim.CrossFadeInFixedTime(parkourHash, 0.1f);
// We'll wait for the animation to finish.
// In a real project, you might get the exact duration from the Animator.
// For now, we'll assume a fixed duration or check state.
timer = 0f;
}
public override void Tick(float deltaTime)
{
timer += deltaTime;
// Simple way to wait for animation: check normalized time of the current state
var stateInfo = stateMachine.Anim.GetCurrentAnimatorStateInfo(0);
if (stateInfo.shortNameHash == parkourHash && stateInfo.normalizedTime >= 1f)
{
if (stateMachine.Input.MoveInput == Vector2.zero)
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
else
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
}
// Safety timeout if animation doesn't play or something
if (timer > 2f)
{
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
}
}
public override void PhysicsTick(float fixedDeltaTime)
{
// Usually during parkour we disable gravity or handle it specially
stateMachine.VelocityY = 0;
}
public override void Exit() {}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d9b73ee252f157e49a08f8f7566dc6ac

View File

@@ -0,0 +1,94 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerRunState : PlayerBaseState
{
private readonly int speedHash = Animator.StringToHash("Speed");
public PlayerRunState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Input.OnJumpEvent += OnJump;
stateMachine.Input.OnDodgeEvent += OnDodge;
stateMachine.Input.OnCrouchEvent += OnCrouch;
}
public override void Tick(float deltaTime)
{
Vector2 input = stateMachine.Input.MoveInput;
float moveAmount = Mathf.Clamp01(Mathf.Abs(input.x) + Mathf.Abs(input.y));
if (moveAmount <= 0.01f)
{
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
return;
}
if (!stateMachine.Input.IsSprintHeld)
{
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
return;
}
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
Vector3 velocity = moveDirection * stateMachine.SprintSpeed;
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
{
stateMachine.VelocityY = -2f;
}
else
{
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
}
velocity.y = stateMachine.VelocityY;
stateMachine.Controller.Move(velocity * deltaTime);
if (moveDirection != Vector3.zero)
{
Quaternion targetRot = Quaternion.LookRotation(moveDirection);
stateMachine.transform.rotation = Quaternion.RotateTowards(
stateMachine.transform.rotation,
targetRot,
stateMachine.RotationSpeed * deltaTime
);
}
stateMachine.Anim.SetFloat(speedHash, 1f, stateMachine.AnimationDamping, deltaTime);
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit()
{
stateMachine.Input.OnJumpEvent -= OnJump;
stateMachine.Input.OnDodgeEvent -= OnDodge;
stateMachine.Input.OnCrouchEvent -= OnCrouch;
}
private void OnJump()
{
if (stateMachine.IsGrounded)
{
if (stateMachine.Scanner != null)
{
var hitData = stateMachine.Scanner.ObstacleCheck();
if (hitData.forwardHitFound)
{
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
return;
}
}
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.SprintSpeed));
}
}
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ef096feea261a8d449384a68f71470b3

View File

@@ -0,0 +1,177 @@
using System.Collections.Generic;
using UnityEngine;
namespace OnlyScove.Scripts
{
[RequireComponent(typeof(CharacterController), typeof(InputReader), typeof(Animator))]
public class PlayerStateMachine : MonoBehaviour
{
[field: Header("References")]
[field: SerializeField] public CharacterController Controller { get; private set; }
[field: SerializeField] public virtual InputReader Input { get; private set; }
[field: SerializeField] public Animator Anim { get; private set; }
[field: SerializeField] public EnvironmentScanner Scanner { get; private set; }
public CameraController Cam { get; private set; }
[field: Header("Movement Settings")]
[field: SerializeField] public float WalkSpeed { get; private set; } = 3f;
[field: SerializeField] public float RunSpeed { get; private set; } = 6f;
[field: SerializeField] public float SprintSpeed { get; private set; } = 9f; // 150% of RunSpeed
[field: SerializeField] public float SneakSpeed { get; private set; } = 1.5f;
[field: SerializeField] public float DashForce { get; private set; } = 10f;
[field: SerializeField] public float RotationSpeed { get; private set; } = 500f;
[field: SerializeField] public float AnimationDamping { get; private set; } = 0.2f;
[field: Header("Airborne Settings")]
[field: SerializeField] public float JumpHeight { get; private set; } = 2f;
[field: SerializeField] public float Gravity { get; private set; } = -9.81f;
[field: SerializeField] public float ThrustDownwardForce { get; private set; } = -20f;
[field: Header("Ground Check")]
[field: SerializeField] public float GroundCheckRadius { get; private set; } = 0.2f;
[field: SerializeField] public Vector3 GroundCheckOffset { get; private set; }
[field: SerializeField] public LayerMask GroundMask { get; private set; }
[field: Header("Interaction")]
[field: SerializeField] public float InteractionRange { get; private set; } = 2f;
[field: SerializeField] public LayerMask InteractionMask { get; private set; }
public float VelocityY { get; set; }
public bool IsGrounded { get; private set; }
public bool WasGrounded { get; private set; }
// Interaction system variables
private List<IInteractable> interactablesNearby = new List<IInteractable>();
private int currentInteractableIndex = 0;
public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None";
private PlayerBaseState currentState;
private bool hasControl = true;
protected virtual void Awake()
{
Controller = GetComponent<CharacterController>();
Input = GetComponent<InputReader>();
Anim = GetComponentInChildren<Animator>();
Scanner = GetComponent<EnvironmentScanner>();
Cam = Camera.main?.GetComponent<CameraController>();
}
private void Start()
{
SwitchState(new PlayerIdleState(this));
// Subscribe to cycle events
Input.OnNextInteractEvent += OnNextInteract;
Input.OnPreviousInteractEvent += OnPreviousInteract;
}
private void OnDestroy()
{
if (Input != null)
{
Input.OnNextInteractEvent -= OnNextInteract;
Input.OnPreviousInteractEvent -= OnPreviousInteract;
}
}
protected virtual void Update()
{
if (!hasControl) return;
WasGrounded = IsGrounded;
CheckGround();
UpdateInteractablesList();
currentState?.Tick(Time.deltaTime);
}
private void FixedUpdate()
{
if (!hasControl) return;
currentState?.PhysicsTick(Time.fixedDeltaTime);
}
private void CheckGround()
{
IsGrounded = Physics.CheckSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius, GroundMask);
}
private void UpdateInteractablesList()
{
interactablesNearby.Clear();
Collider[] colliders = Physics.OverlapSphere(transform.position + transform.forward * (InteractionRange / 2), InteractionRange, InteractionMask);
foreach (var col in colliders)
{
if (col.TryGetComponent(out IInteractable interactable))
{
if (!interactablesNearby.Contains(interactable))
interactablesNearby.Add(interactable);
}
}
if (interactablesNearby.Count == 0)
{
currentInteractableIndex = 0;
}
else if (currentInteractableIndex >= interactablesNearby.Count)
{
currentInteractableIndex = interactablesNearby.Count - 1;
}
}
private void OnNextInteract()
{
if (interactablesNearby.Count <= 1) return;
currentInteractableIndex = (currentInteractableIndex + 1) % interactablesNearby.Count;
Debug.Log($"[Interaction] Switched to: {interactablesNearby[currentInteractableIndex].InteractionPrompt}");
}
private void OnPreviousInteract()
{
if (interactablesNearby.Count <= 1) return;
currentInteractableIndex--;
if (currentInteractableIndex < 0) currentInteractableIndex = interactablesNearby.Count - 1;
Debug.Log($"[Interaction] Switched to: {interactablesNearby[currentInteractableIndex].InteractionPrompt}");
}
public IInteractable GetInteractable()
{
if (interactablesNearby.Count == 0) return null;
return interactablesNearby[currentInteractableIndex];
}
public void SetGroundCheck(float radius, Vector3 offset)
{
GroundCheckRadius = radius;
GroundCheckOffset = offset;
}
public void SwitchState(PlayerBaseState newState)
{
currentState?.Exit();
currentState = newState;
currentState?.Enter();
}
public void SetControl(bool control)
{
hasControl = control;
Controller.enabled = control;
if (!control)
{
Anim.SetFloat("Speed", 0f);
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(0, 1, 0, 0.5f);
Gizmos.DrawSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius);
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position + transform.forward * (InteractionRange / 2), InteractionRange);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 848ad6fdeb60b254497391392419b063

View File

@@ -0,0 +1,43 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class PlayerThrustState : PlayerBaseState
{
private readonly int thrustHash = Animator.StringToHash("Thrust");
public PlayerThrustState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Anim.SetTrigger(thrustHash);
// Immediately set a massive downward velocity
stateMachine.VelocityY = stateMachine.ThrustDownwardForce;
}
public override void Tick(float deltaTime)
{
// Keep applying heavy gravity just in case
stateMachine.VelocityY += (stateMachine.Gravity * 2f) * deltaTime;
// Move the player straight down (no horizontal movement allowed during thrust)
Vector3 fallMovement = new Vector3(0f, stateMachine.VelocityY, 0f);
stateMachine.Controller.Move(fallMovement * deltaTime);
// When we smash into the ground...
if (stateMachine.Controller.isGrounded)
{
stateMachine.VelocityY = -2f;
// TODO: Add impact effects, screen shake, or damage area here!
// Return to idle after landing
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
}
}
public override void PhysicsTick(float fixedDeltaTime) {}
public override void Exit() {}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4e76cfd208cc6f44b8b954147ee1defb