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,8 @@
fileFormatVersion: 2
guid: 962e9e0d2b8d78d4fbb25fb03224f618
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,287 @@
using System;
using UnityEngine;
namespace OnlyScove.Scripts
{
public class CameraController : MonoBehaviour
{
[SerializeField] InputReader inputReader; // Kéo thả Object chứa InputReader vào đây
[SerializeField] Transform followTarget;
[SerializeField] float distance = 5;
[SerializeField] float minDistance = 2f;
[SerializeField] float maxDistance = 15f;
[SerializeField] float zoomSensitivity = 1f;
[SerializeField] float sensitivity = 0.1f; // Độ nhạy (chỉnh trong Inspector)
[SerializeField] LayerMask collisionLayers;
[SerializeField] float cameraRadius = 0.2f;
[SerializeField] float positionSmoothTime = 0.12f; // Độ trễ đuổi theo nhân vật
[SerializeField] float rotationSmoothTime = 5f; // Tốc độ làm mượt vòng xoay chuột
[Header("Auto Rotation")]
[SerializeField] bool useAutoRotation = true;
[SerializeField] float autoRotateDelay = 2.5f; // Sau bao lâu không chạm chuột thì xoay
[SerializeField] float autoRotateSpeed = 2f; // Tốc độ xoay về sau lưng
[Header("Occlusion Transparency")]
[SerializeField] bool useTransparency = true;
[SerializeField] LayerMask transparencyLayers;
[SerializeField] float fadeAlpha = 0.3f; // Độ trong suốt (0 là biến mất, 1 là hiện rõ)
[Header("Dynamic FOV")]
[SerializeField] bool useDynamicFOV = true;
[SerializeField] float baseFOV = 60f;
[SerializeField] float sprintFOV = 70f;
[SerializeField] float fovSmoothTime = 5f;
[Header("Character Fading")]
[SerializeField] bool useCharacterFading = true;
[SerializeField] float minVisibleDistance = 1.2f; // Khoảng cách bắt đầu mờ
[SerializeField] float fullyHiddenDistance = 0.6f; // Khoảng cách biến mất hẳn
[SerializeField] Renderer[] characterRenderers; // Kéo các Mesh của nhân vật vào đây
[Header("Side Bias")]
[SerializeField] bool useSideBias = true;
[SerializeField] float horizontalBiasAmount = 0.5f; // Độ lệch sang trái/phải
[SerializeField] float biasSmoothTime = 3f; // Tốc độ chuyển đổi độ lệch
[Header("Camera Shake")]
[SerializeField] bool useShake = true;
private float shakeIntensity = 0f;
private float shakeDuration = 0f;
private float shakeTimer = 0f;
private Vector3 shakeOffset;
[SerializeField] float minVerticalAngle = -45f;
[SerializeField] float maxVerticalAngle = 45f;
[SerializeField] Vector2 framingOffset;
[SerializeField] private bool invertX;
[SerializeField] private bool invertY;
private float rotationX;
private float rotationY;
private float invertXVal;
private float invertYVal;
private float lastInputTime;
private Vector3 currentVelocity;
private Quaternion currentRotation;
private Camera cam;
private Renderer lastFadedRenderer;
private Color originalColor;
private float currentSideBias;
private void Start()
{
cam = GetComponent<Camera>();
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
// Khởi tạo vòng xoay hiện tại
rotationX = transform.eulerAngles.x;
rotationY = transform.eulerAngles.y;
currentRotation = transform.rotation;
lastInputTime = Time.time;
}
private void Update()
{
if (inputReader != null)
{
// Kiểm tra xem có input xoay chuột không
if (inputReader.LookInput.magnitude > 0.01f)
{
lastInputTime = Time.time;
}
invertXVal = (invertX) ? -1 : 1;
invertYVal = (invertY) ? -1 : 1;
rotationX -= inputReader.LookInput.y * invertYVal * sensitivity * Time.deltaTime;
rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);
rotationY += inputReader.LookInput.x * invertXVal * sensitivity * Time.deltaTime;
// Logic Side Bias (Lệch khung hình khi di chuyển)
if (useSideBias)
{
float targetBias = -inputReader.MoveInput.x * horizontalBiasAmount;
currentSideBias = Mathf.Lerp(currentSideBias, targetBias, biasSmoothTime * Time.deltaTime);
}
else
{
currentSideBias = 0;
}
// Logic Tự động xoay sau lưng (Auto-Correction)
if (useAutoRotation && Time.time - lastInputTime > autoRotateDelay)
{
// Chỉ xoay khi nhân vật đang di chuyển
if (inputReader.MoveInput.magnitude > 0.1f)
{
// Lấy hướng nhân vật đang nhìn (Yaw)
float targetYaw = followTarget.eulerAngles.y;
// Dùng LerpAngle để xoay mượt mà về hướng đó
rotationY = Mathf.LerpAngle(rotationY, targetYaw, autoRotateSpeed * Time.deltaTime);
}
}
float scrollDelta = inputReader.ScrollInput.y;
if (Mathf.Abs(scrollDelta) > 0.1f)
{
distance -= scrollDelta * zoomSensitivity * Time.deltaTime;
distance = Mathf.Clamp(distance, minDistance, maxDistance);
}
}
// Xoay chuột: Làm mượt bằng Slerp
Quaternion targetRotation = Quaternion.Euler(rotationX, rotationY, 0f);
currentRotation = Quaternion.Slerp(currentRotation, targetRotation, rotationSmoothTime * Time.deltaTime);
// Vị trí mục tiêu: Áp dụng offset + Side Bias
Vector3 focusPosition = followTarget.position + currentRotation * new Vector3(framingOffset.x + currentSideBias, framingOffset.y, 0);
// Collision Logic (Dùng currentRotation đã được làm mượt)
float targetDistance = distance;
RaycastHit hit;
Vector3 rayStart = focusPosition;
Vector3 rayDirection = currentRotation * Vector3.back;
if (Physics.SphereCast(rayStart, cameraRadius, rayDirection, out hit, distance, collisionLayers))
{
targetDistance = Mathf.Max(minDistance, hit.distance - 0.1f);
}
// Logic Làm mờ nhân vật (Character Fading)
if (useCharacterFading && characterRenderers != null && characterRenderers.Length > 0)
{
HandleCharacterFading(targetDistance);
}
// Vị trí cuối cùng: Làm mượt bằng SmoothDamp để tạo độ trễ đuổi theo
Vector3 targetPosition = focusPosition - currentRotation * new Vector3(0, 0, targetDistance);
// Xử lý Camera Shake
if (useShake && shakeTimer > 0)
{
HandleShake();
}
else
{
shakeOffset = Vector3.zero;
}
transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref currentVelocity, positionSmoothTime) + shakeOffset;
transform.rotation = currentRotation;
// Logic Làm trong suốt vật cản (Occlusion Transparency)
if (useTransparency)
{
HandleTransparency(focusPosition);
}
// Logic FOV linh hoạt
if (useDynamicFOV && cam != null)
{
HandleDynamicFOV();
}
}
private void HandleDynamicFOV()
{
float targetFOV = baseFOV;
// Nếu đang di chuyển và nhấn giữ nút Sprint
if (inputReader.MoveInput.magnitude > 0.1f && inputReader.IsSprintHeld)
{
targetFOV = sprintFOV;
}
// Làm mượt quá trình thay đổi FOV
cam.fieldOfView = Mathf.Lerp(cam.fieldOfView, targetFOV, fovSmoothTime * Time.deltaTime);
}
private void HandleShake()
{
shakeTimer -= Time.deltaTime;
// Cường độ rung giảm dần theo thời gian
float currentIntensity = (shakeTimer / shakeDuration) * shakeIntensity;
// Dùng Perlin Noise để tạo rung động mượt mà
float shakeX = (Mathf.PerlinNoise(Time.time * 25f, 0f) - 0.5f) * 2f;
float shakeY = (Mathf.PerlinNoise(0f, Time.time * 25f) - 0.5f) * 2f;
float shakeZ = (Mathf.PerlinNoise(Time.time * 25f, Time.time * 25f) - 0.5f) * 2f;
shakeOffset = new Vector3(shakeX, shakeY, shakeZ) * currentIntensity;
}
public void Shake(float intensity, float duration)
{
shakeIntensity = intensity;
shakeDuration = duration;
shakeTimer = duration;
}
private void HandleCharacterFading(float currentDistance)
{
// Tính độ mờ dựa trên khoảng cách
float alpha = Mathf.InverseLerp(fullyHiddenDistance, minVisibleDistance, currentDistance);
foreach (var renderer in characterRenderers)
{
if (renderer != null)
{
Color color = renderer.material.color;
color.a = alpha;
renderer.material.color = color;
}
}
}
private void HandleTransparency(Vector3 focusPosition)
{
Vector3 direction = focusPosition - transform.position;
float distanceToPlayer = direction.magnitude;
RaycastHit hit;
// Bắn một tia từ Camera đến Nhân vật
if (Physics.Raycast(transform.position, direction.normalized, out hit, distanceToPlayer, transparencyLayers))
{
Renderer renderer = hit.collider.GetComponent<Renderer>();
if (renderer != null && renderer != lastFadedRenderer)
{
// Nếu chạm vật mới, khôi phục vật cũ
ResetLastRenderer();
// Lưu thông tin vật mới và làm mờ
lastFadedRenderer = renderer;
originalColor = renderer.material.color;
Color fadedColor = originalColor;
fadedColor.a = fadeAlpha;
// Lưu ý: Material cần hỗ trợ Transparency (Surface Type: Transparent trong URP)
renderer.material.color = fadedColor;
}
}
else
{
// Nếu không chạm gì, khôi phục vật cũ
ResetLastRenderer();
}
}
private void ResetLastRenderer()
{
if (lastFadedRenderer != null)
{
lastFadedRenderer.material.color = originalColor;
lastFadedRenderer = null;
}
}
public Quaternion PlanarRotation => Quaternion.Euler(0f, rotationY, 0f);
}
}

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9477ecbb64ef4d9c8863fb16d2c4bc96
timeCreated: 1773383891

View File

@@ -0,0 +1,76 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace OnlyScove.Scripts
{
public class PlayerDebugProvider : MonoBehaviour
{
[Header("References")]
[SerializeField] private PlayerStateMachine stateMachine;
[Tooltip("Kéo cái Canvas (World Space) bạn đã thiết kế vào đây")]
[SerializeField] private GameObject debugCanvas;
[Header("Follow Settings")]
[SerializeField] private Vector3 followOffset = new Vector3(1.5f, 2f, 0f);
[SerializeField] private float smoothTime = 0.15f;
[SerializeField] private bool lookAtCamera = true;
// Các thuộc tính Public để bạn truy cập từ Script UI của bạn
public string CurrentState => stateMachine != null ? stateMachine.CurrentStateName : "N/A";
public string GroundedStatus => (stateMachine != null && stateMachine.IsGrounded) ? "YES" : "NO";
public float HorizontalSpeed => stateMachine != null ? new Vector3(stateMachine.Controller.velocity.x, 0, stateMachine.Controller.velocity.z).magnitude : 0f;
public float VerticalSpeed => stateMachine != null ? stateMachine.VelocityY : 0f;
public Vector2 MoveInput => stateMachine != null ? stateMachine.Input.MoveInput : Vector2.zero;
public bool IsSprinting => stateMachine != null ? stateMachine.Input.IsSprintHeld : false;
public string TargetInteractable => stateMachine != null ? (stateMachine.GetInteractable()?.InteractionPrompt ?? "None") : "N/A";
private Vector3 currentVelocity;
private Transform cameraTransform;
private bool isVisible = true;
private void Awake()
{
if (stateMachine == null) stateMachine = GetComponent<PlayerStateMachine>();
cameraTransform = Camera.main?.transform;
if (debugCanvas != null) debugCanvas.SetActive(isVisible);
}
private void Update()
{
// Toggle Visibility (Ctrl + Shift + B) sử dụng New Input System
if (Keyboard.current != null)
{
bool ctrl = Keyboard.current.leftCtrlKey.isPressed || Keyboard.current.rightCtrlKey.isPressed;
bool shift = Keyboard.current.leftShiftKey.isPressed || Keyboard.current.rightShiftKey.isPressed;
bool bDown = Keyboard.current.bKey.wasPressedThisFrame;
if (ctrl && shift && bDown)
{
isVisible = !isVisible;
if (debugCanvas != null) debugCanvas.SetActive(isVisible);
}
}
}
private void LateUpdate()
{
if (!isVisible || debugCanvas == null) return;
// 1. Damping Follow: UI đuổi theo Player
Vector3 targetPos = transform.position + followOffset;
debugCanvas.transform.position = Vector3.SmoothDamp(
debugCanvas.transform.position,
targetPos,
ref currentVelocity,
smoothTime
);
// 2. Billboard: Luôn nhìn về Camera
if (lookAtCamera && cameraTransform != null)
{
debugCanvas.transform.LookAt(debugCanvas.transform.position + cameraTransform.forward);
}
}
}
}

View File

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

View File

@@ -0,0 +1,48 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
public class EnvironmentScanner : MonoBehaviour
{
[SerializeField] private Vector3 forwardRayOffset = new Vector3(0, 2.5f, 0);
[SerializeField] float forwardRayLength = 10f;
[SerializeField] LayerMask obstacleLayer;
[SerializeField] float heightRayLength;
public ObstacleHitInfo ObstacleCheck()
{
var hitData = new ObstacleHitInfo();
var forwardOrigin = transform.position + forwardRayOffset;
hitData.forwardHitFound = Physics.Raycast(transform.position + forwardRayOffset,
transform.forward,
out hitData.forwardHit,
forwardRayLength, obstacleLayer
);
Debug.DrawRay(forwardOrigin, transform.forward * forwardRayLength, (hitData.forwardHitFound) ? Color.red : Color.green);
if (hitData.forwardHitFound)
{
var heightOrigin = hitData.forwardHit.point + Vector3.up * heightRayLength;
hitData.heightHitFound = Physics.Raycast(heightOrigin, Vector3.down,
out hitData.heightHit,
heightRayLength, obstacleLayer);
Debug.DrawRay(heightOrigin, Vector3.down * heightRayLength, (hitData.heightHitFound) ? Color.red : Color.green);
}
return hitData;
}
}
}
public struct ObstacleHitInfo
{
public RaycastHit forwardHit;
public RaycastHit heightHit;
public bool forwardHitFound;
public bool heightHitFound;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 169e37d35fede30409266070c88b118f

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b562adae77c550e4db1edcabf68b0530
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,170 @@
using UnityEngine;
using System.Text;
namespace OnlyScove.Scripts.GameSetup
{
[RequireComponent(typeof(CharacterController))]
public class CharacterAutoSetup : MonoBehaviour
{
[Header("Manual Overrides (If Detection Fails)")]
[SerializeField] private float defaultHeight = 1.8f;
[SerializeField] private float defaultShoulderWidth = 0.4f;
[Header("Settings")]
[SerializeField] private Transform modelRoot;
[SerializeField] private bool autoDetectOnStart = true;
[SerializeField] private float zCenterOffset = 0.05f;
private void Start()
{
if (autoDetectOnStart)
{
ApplyAutoSetup();
}
}
[ContextMenu("Apply Auto Setup")]
public void ApplyAutoSetup()
{
CharacterController controller = GetComponent<CharacterController>();
PlayerStateMachine stateMachine = GetComponent<PlayerStateMachine>();
Animator animator = GetComponentInChildren<Animator>();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"<color=#4FC3F7><b>[AUTO-SETUP REPORT]</b></color> Character: <b>{gameObject.name}</b>");
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
// 1. HEIGHT DETECTION
float finalHeight = defaultHeight;
string heightMethod = "Default Fallback";
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Head) != null)
{
heightMethod = "Humanoid Bones (Feet to Head)";
Transform head = animator.GetBoneTransform(HumanBodyBones.Head);
// We measure from the local Y=0 (feet) to the head bone and add 10% for the skull/hair
float headHeight = transform.InverseTransformPoint(head.position).y;
finalHeight = headHeight * 1.12f; // 12% extra for the top of the skull
}
else
{
heightMethod = "Local Mesh Bounds";
Bounds localBounds = GetRelativeBounds();
if (localBounds.size.y > 0) finalHeight = localBounds.size.y;
}
sb.AppendLine(string.Format("<b>1. HEIGHT:</b> {0:F3}m ({1}) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, heightMethod, finalHeight));
// 2. RADIUS DETECTION
float shoulderWidth = defaultShoulderWidth;
string radiusMethod = "Default Fallback";
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Hips) != null)
{
radiusMethod = "Humanoid Bones (Shoulders)";
Transform leftArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
Transform rightArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
if (leftArm != null && rightArm != null)
{
float distance = Vector3.Distance(leftArm.position, rightArm.position);
shoulderWidth = distance * 1.2f; // Add 20% for arm/shoulder thickness
}
}
else
{
radiusMethod = "Bounds Width Fallback";
Bounds b = GetRelativeBounds();
shoulderWidth = b.size.x * 0.25f;
}
float finalRadius = shoulderWidth / 2f;
sb.AppendLine(string.Format("<b>2. RADIUS:</b> {0:F3}m ({1}) [/ 2] ➔ <color=#81C784>{2:F3}m</color>", shoulderWidth, radiusMethod, finalRadius));
// 3. COLLISION PRECISION
float skinWidth = finalRadius * 0.10f;
sb.AppendLine(string.Format("<b>3. SKIN WIDTH:</b> {0:F3}m (Radius) [x 0.10] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, skinWidth));
// 4. CAPSULE CENTER
// Center Y = (Height / 2) + SkinWidth
float centerY = (finalHeight / 2f) + skinWidth;
sb.AppendLine(string.Format("<b>4. CENTER Y:</b> ({0:F3}m / 2) + {1:F3}m (Skin) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, skinWidth, centerY));
sb.AppendLine(string.Format("<b>5. CENTER Z:</b> [Fixed Offset] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
// 5. MOVEMENT CONSTRAINTS
float stepOffset = finalHeight * 0.15f;
sb.AppendLine(string.Format("<b>6. STEP OFFSET:</b> {0:F3}m (Height) [x 0.15] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, stepOffset));
// 6. GROUND CHECK (STATE MACHINE)
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
if (stateMachine != null)
{
float groundCheckRadius = finalRadius * 0.6f;
float groundCheckOffsetY = finalHeight / 24f;
Vector3 groundCheckOffset = new Vector3(0, groundCheckOffsetY, zCenterOffset);
stateMachine.SetGroundCheck(groundCheckRadius, groundCheckOffset);
sb.AppendLine("<b>7. GROUND CHECK SETUP:</b>");
sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, groundCheckRadius));
sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, groundCheckOffsetY));
sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
}
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
// Apply to Controller
controller.height = finalHeight;
controller.radius = finalRadius;
controller.skinWidth = skinWidth;
controller.center = new Vector3(0, centerY, zCenterOffset);
controller.slopeLimit = 45f;
controller.stepOffset = stepOffset;
controller.minMoveDistance = 0.001f;
Debug.Log(sb.ToString());
}
private Bounds GetRelativeBounds()
{
Transform targetRoot = modelRoot != null ? modelRoot : transform;
Renderer[] renderers = targetRoot.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return new Bounds(Vector3.zero, Vector3.zero);
// Using local bounds of SkinnedMeshRenderers for better accuracy on animated characters
Bounds combinedLocalBounds = new Bounds();
bool first = true;
foreach (Renderer renderer in renderers)
{
if (renderer is ParticleSystemRenderer) continue;
Bounds localB;
if (renderer is SkinnedMeshRenderer smr)
{
localB = smr.localBounds;
}
else
{
// For static meshes, convert world bounds back to local root space
Vector3 min = transform.InverseTransformPoint(renderer.bounds.min);
Vector3 max = transform.InverseTransformPoint(renderer.bounds.max);
localB = new Bounds((min + max) / 2f, max - min);
}
if (first)
{
combinedLocalBounds = localB;
first = false;
}
else
{
combinedLocalBounds.Encapsulate(localB);
}
}
return combinedLocalBounds;
}
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using UnityEngine;
namespace OnlyScove.Scripts.GameSetup
{
[CreateAssetMenu(fileName = "CharacterSetupSettings", menuName = "Setup/Character Setup Settings")]
public class CharacterSetupSettings : ScriptableObject
{
[Header("Movement Constraints")]
public float slopeLimit = 45f;
[Range(0.01f, 0.5f)] public float stepHeightRatio = 0.15f; // Step offset as % of height
[Header("Precision & Collision")]
public float skinWidthRatio = 0.1f; // Skin width as % of radius
public float minMoveDistance = 0.001f;
[Header("Dimension Multipliers")]
[Tooltip("Multiplies the detected shoulder width to define Radius.")]
public float radiusMultiplier = 0.8f;
[Tooltip("Multiplies the detected bounding box height.")]
public float heightMultiplier = 1.0f;
[Header("Center Offset")]
[Tooltip("Y-axis offset for the center of the capsule (0.5 means exact middle).")]
public float centerYRatio = 0.5f;
}
}

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1398e4608a9406cb68d769d6bc9a3ec
timeCreated: 1773397974

View File

@@ -0,0 +1,8 @@
namespace OnlyScove.Scripts
{
public interface IInteractable
{
string InteractionPrompt { get; }
void OnInteract(PlayerStateMachine player);
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e6d134b82fd19fc4aa38896d5e45efed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
using UnityEngine;
namespace OnlyScove.Scripts
{
/// <summary>
/// A version of PlayerStateMachine that simulates constant forward or backward input.
/// </summary>
public class AutoPlayerStateMachine : PlayerStateMachine
{
[Header("Auto Pilot Settings")]
public bool alwaysMoveForward = true;
public bool alwaysRun = true;
public bool isRed = false; // New property to differentiate movement direction
private class FakeInputReader : InputReader
{
public Vector2 ForcedMove { get; set; }
public bool ForcedSprint { get; set; }
public override Vector2 MoveInput => ForcedMove;
public override bool IsSprintHeld => ForcedSprint;
}
private FakeInputReader fakeInput;
public override InputReader Input => fakeInput;
protected override void Awake()
{
fakeInput = gameObject.AddComponent<FakeInputReader>();
base.Awake();
}
protected override void Update()
{
if (fakeInput != null)
{
// Logic updated: isRed moves backward (Vector2.down), others move forward (Vector2.up)
fakeInput.ForcedMove = (isRed)
? (alwaysMoveForward ? Vector2.down : Vector2.zero)
: (alwaysMoveForward ? Vector2.up : Vector2.zero);
fakeInput.ForcedSprint = alwaysRun;
}
base.Update();
}
}
}

View File

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

View File

@@ -0,0 +1,129 @@
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine.Jobs;
using Unity.Burst;
namespace Elbyss.Optimization
{
/// <summary>
/// Manages 10,000+ objects using C# Job System and Burst Compiler.
/// This avoids the overhead of 10,000 individual Update() calls.
/// </summary>
public class JobsMovementManager : MonoBehaviour
{
[Header("Spawn Settings")]
public GameObject prefab;
public int objectCount = 10000;
public float spacing = 1.5f;
[Header("Movement Settings")]
public float speed = 5f;
private TransformAccessArray transformAccessArray;
private NativeArray<Vector3> directions;
private bool isInitialized = false;
private void Start()
{
// Optional: Start automatically or via Context Menu
// Setup(objectCount);
}
[ContextMenu("Setup 10k Objects")]
public void InitialSetup()
{
Setup(objectCount);
}
public void Setup(int count)
{
if (isInitialized) Cleanup();
objectCount = count;
Transform[] transforms = new Transform[objectCount];
directions = new NativeArray<Vector3>(objectCount, Allocator.Persistent);
int rowSize = Mathf.CeilToInt(Mathf.Sqrt(objectCount));
for (int i = 0; i < objectCount; i++)
{
float x = (i % rowSize) * spacing;
float z = (i / rowSize) * spacing;
Vector3 pos = transform.position + new Vector3(x, 0, z);
GameObject go = Instantiate(prefab, pos, Quaternion.identity, this.transform);
transforms[i] = go.transform;
// Set alternating directions
directions[i] = (i % 2 == 0) ? Vector3.forward : Vector3.back;
// CRITICAL OPTIMIZATION: Disable components that are too heavy for 10k objects
if (go.TryGetComponent<Animator>(out var anim)) anim.enabled = false;
if (go.TryGetComponent<CharacterController>(out var cc)) cc.enabled = false;
// Disable all other custom scripts
MonoBehaviour[] scripts = go.GetComponents<MonoBehaviour>();
foreach (var s in scripts)
{
if (s != this) s.enabled = false;
}
}
transformAccessArray = new TransformAccessArray(transforms);
isInitialized = true;
Debug.Log($"Initialized {objectCount} objects with Job System.");
}
private void Update()
{
if (!isInitialized) return;
// Create the movement job
var job = new MovementJob
{
DeltaTime = Time.deltaTime,
Speed = speed,
Directions = directions
};
// Schedule the job to run in parallel on all available CPU cores
// transformAccessArray allows the job to modify Transform data directly
JobHandle handle = job.Schedule(transformAccessArray);
// This ensures the job is finished before the frame ends
// In a real scenario, you might want to call Complete() in LateUpdate or next frame
// but for simple movement, scheduling and completing in Update is fine.
handle.Complete();
}
private void OnDestroy()
{
Cleanup();
}
private void Cleanup()
{
if (isInitialized)
{
if (transformAccessArray.isCreated) transformAccessArray.Dispose();
if (directions.IsCreated) directions.Dispose();
isInitialized = false;
}
}
[BurstCompile] // This attribute tells the Burst compiler to optimize this job into machine code
struct MovementJob : IJobParallelForTransform
{
public float DeltaTime;
public float Speed;
[ReadOnly] public NativeArray<Vector3> Directions;
public void Execute(int index, TransformAccess transform)
{
// Directly modify the transform position in parallel
transform.position += Directions[index] * Speed * DeltaTime;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 41899df442467714dbee462ae451773a

View File

@@ -0,0 +1,153 @@
// using GPUInstancerPro;
// using Unity.Burst;
// using Unity.Collections;
// using Unity.Jobs;
// using Unity.Mathematics;
// using UnityEngine;
//
// namespace Elbyss.Optimization
// {
// public class MassiveSpawner : MonoBehaviour
// {
// [Header("Spawn Settings")]
// public GameObject prefab;
// public GPUIProfile profile;
// public int instanceCount = 100000;
// public float spacing = 1.5f;
//
// [Header("Update Settings")]
// public bool runUpdate = true;
// public float movementSpeed = 1.0f;
// public float amplitude = 2.0f;
//
// private int _rendererKey;
// private NativeArray<Matrix4x4> _matrices;
// private JobHandle _jobHandle;
// private bool _isInitialized;
//
// private void OnEnable()
// {
// Initialize();
// }
//
// private void OnDisable()
// {
// Dispose();
// }
//
// private void OnValidate()
// {
// if (Application.isPlaying && _isInitialized)
// {
// // Re-initialize if count changes during play (optional, but good for testing)
// Initialize();
// }
// }
//
// public void Initialize()
// {
// Dispose();
// if (prefab == null) return;
//
// if (GPUICoreAPI.RegisterRenderer(this, prefab, profile, out _rendererKey))
// {
// _matrices = new NativeArray<Matrix4x4>(instanceCount, Allocator.Persistent);
//
// // Initial generation
// GenerateMatrices(0);
// _jobHandle.Complete();
// GPUICoreAPI.SetTransformBufferData(_rendererKey, _matrices);
//
// _isInitialized = true;
// Debug.Log($"[MassiveSpawner] Registered {instanceCount} instances with key {_rendererKey}");
// }
// else
// {
// Debug.LogError("[MassiveSpawner] Failed to register renderer!");
// }
// }
//
// private void Update()
// {
// if (!_isInitialized || _rendererKey == 0) return;
//
// if (runUpdate)
// {
// // Complete previous frame's work if any
// _jobHandle.Complete();
//
// // Apply updated matrices to GPUI
// GPUICoreAPI.SetTransformBufferData(_rendererKey, _matrices);
//
// // Schedule next frame's work
// GenerateMatrices(Time.time);
// }
// }
//
// private void GenerateMatrices(float time)
// {
// int side = Mathf.CeilToInt(Mathf.Sqrt(instanceCount));
//
// var job = new MatrixUpdateJob
// {
// matrices = _matrices,
// side = side,
// spacing = spacing,
// time = time,
// speed = movementSpeed,
// amplitude = amplitude,
// origin = transform.position
// };
//
// _jobHandle = job.Schedule(instanceCount, 64);
// }
//
// public void Dispose()
// {
// _jobHandle.Complete();
// if (_rendererKey != 0)
// {
// GPUICoreAPI.DisposeRenderer(_rendererKey);
// _rendererKey = 0;
// }
//
// if (_matrices.IsCreated)
// {
// _matrices.Dispose();
// }
// _isInitialized = false;
// }
//
// [BurstCompile]
// struct MatrixUpdateJob : IJobParallelFor
// {
// public NativeArray<Matrix4x4> matrices;
// public int side;
// public float spacing;
// public float time;
// public float speed;
// public float amplitude;
// public Vector3 origin;
//
// public void Execute(int index)
// {
// int x = index % side;
// int z = index / side;
//
// float xPos = x * spacing;
// float zPos = z * spacing;
//
// // Add some animation to prove it's updating
// float yPos = math.sin(time * speed + (xPos + zPos) * 0.1f) * amplitude;
//
// Vector3 pos = origin + new Vector3(xPos, yPos, zPos);
//
// // Simple rotation based on time
// float angle = (time * speed * 10f + index) % 360f;
// Quaternion rot = Quaternion.Euler(0, angle, 0);
//
// matrices[index] = Matrix4x4.TRS(pos, rot, Vector3.one);
// }
// }
// }
// }

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50831f537cbccac4c9bf4067b6b158c7

View File

@@ -0,0 +1,34 @@
using UnityEngine;
using TMPro;
namespace Elbyss.Optimization
{
public class PerformanceHUD : MonoBehaviour
{
private float deltaTime = 0.0f;
private string displayFormat = "{0:0.0} ms ({1:0.} fps)";
private void Update()
{
deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
}
private void OnGUI()
{
int w = Screen.width, h = Screen.height;
GUIStyle style = new GUIStyle();
Rect rect = new Rect(10, 10, w, h * 2 / 100);
style.alignment = TextAnchor.UpperLeft;
style.fontSize = h * 2 / 50;
style.normal.textColor = new Color(0.0f, 1.0f, 0.5f, 1.0f);
float msec = deltaTime * 1000.0f;
float fps = 1.0f / deltaTime;
string text = string.Format(displayFormat, msec, fps);
GUI.Label(rect, text, style);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9c9a374a3ab089d41a6784b1ffad3b6f

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
using UnityEngine;
using OnlyScove.Scripts;
namespace Elbyss.Optimization
{
public class StressTestSpawner : MonoBehaviour
{
[Header("Spawn Settings")]
public GameObject prefabToSpawn;
public int spawnLimit = 1000;
public int spawnsPerFrame = 10;
public float spacing = 2.0f;
[Header("Testing Mode")]
public bool useAutoStateMachine = true;
[Header("Optimization Options")]
public bool stripHeavyComponents = false;
private List<GameObject> spawnedObjects = new List<GameObject>();
private int currentSpawnCount = 0;
private bool isSpawning = false;
private void Update()
{
if (isSpawning && currentSpawnCount < spawnLimit)
{
for (int i = 0; i < spawnsPerFrame && currentSpawnCount < spawnLimit; i++)
{
SpawnObject();
}
}
}
[ContextMenu("Start Stress Test")]
public void StartStressTest() => isSpawning = true;
[ContextMenu("Clear All")]
public void ClearAll()
{
foreach (var obj in spawnedObjects) if (obj != null) Destroy(obj);
spawnedObjects.Clear();
currentSpawnCount = 0;
isSpawning = false;
}
private void SpawnObject()
{
if (prefabToSpawn == null) return;
int rowSize = Mathf.CeilToInt(Mathf.Sqrt(spawnLimit));
float x = (currentSpawnCount % rowSize) * spacing;
float z = (currentSpawnCount / rowSize) * spacing;
Vector3 spawnPos = transform.position + new Vector3(x, 0, z);
GameObject newObj = Instantiate(prefabToSpawn, spawnPos, transform.rotation);
if (useAutoStateMachine)
{
var realInput = newObj.GetComponent<InputReader>();
if (realInput != null) realInput.enabled = false;
var originalSM = newObj.GetComponent<PlayerStateMachine>();
if (originalSM != null)
{
DestroyImmediate(originalSM);
var autoSM = newObj.AddComponent<AutoPlayerStateMachine>();
autoSM.alwaysMoveForward = true;
autoSM.alwaysRun = true;
}
}
if (stripHeavyComponents)
{
if (newObj.TryGetComponent<Animator>(out var anim)) anim.enabled = false;
if (newObj.TryGetComponent<CharacterController>(out var cc)) cc.enabled = false;
if (newObj.TryGetComponent<PlayerStateMachine>(out var sm)) sm.enabled = false;
}
spawnedObjects.Add(newObj);
currentSpawnCount++;
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e0fd09e39d1b90458b7097e555a9f3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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

View File

@@ -0,0 +1,65 @@
// -- SPINE PROXY 1.0 | Kevin Iglesias --
// This script ensures correct animation display when mixing upper and lower body animations using Unity Avatar Masks.
// Attach this script to the 'B-spineProxy' transform, which is a sibling of the 'B-hips' bone.
// In the 'originalSpine' field, assign the 'B-spine' bone (child of 'B-hips' and parent of 'B-chest').
// By default it will automatically find the 'B-spine' and assign it to the 'originalSpine' field (OnValidate).
// When using a different character rig, manually assign the corresponding spine bone to the 'originalSpine' field and recreate
// 'Rig > B-root > B-spine' structure in your character hierarchy with empty GameObjects.
// More information: https://www.keviniglesias.com/spine-proxy.html
// Contact Support: support@keviniglesias.com
using UnityEngine;
namespace KevinIglesias
{
public class SpineProxy : MonoBehaviour
{
//Assign 'B-spine' (or equivalent) here:
[SerializeField] private Transform originalSpine;
private Quaternion rotationOffset = Quaternion.identity;
#if UNITY_EDITOR
//Attempting to find the original spine bone.
void OnValidate()
{
if(originalSpine == null)
{
Transform parent = transform.parent;
if(parent != null)
{
Transform hips = parent.Find("B-hips");
if(hips != null)
{
Transform spine = hips.Find("B-spine");
if(spine != null)
{
originalSpine = spine;
}
}
}
}
}
#endif
//Match correct orientation on different character rigs
void Awake()
{
if(originalSpine != null)
{//originalSpine.rotation must be the default rotation in your character T-pose when this happens:
rotationOffset = Quaternion.Inverse(transform.rotation) * originalSpine.rotation;
}
}
//Copy rotations from spine proxy bone to the original spine bone.
void LateUpdate()
{
if(originalSpine == null)
{
return;
}
originalSpine.rotation = transform.rotation * rotationOffset;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 682245d9ac89ba4409aa3a92f17f5c6c

View File

@@ -0,0 +1,8 @@
using UnityEngine;
public class StickyNote : MonoBehaviour
{
[TextArea] public string noteText = "Enter note here...";
public Color noteColor = Color.yellow;
public bool showAlways = true; // Show even when not selected
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 20733ff6cdef89e408acddf8ce51503d

3
Assets/Scripts/UI.meta Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91b95b0bf23143b68483f912b558e6f0
timeCreated: 1773383929

View File

@@ -0,0 +1,42 @@
using OnlyScove.Scripts;
using UnityEngine;
using TMPro;
namespace UI
{
public class MyUIDisplay : MonoBehaviour
{
public PlayerDebugProvider playerDebugProvider;
[Header("Text Fields")]
public TextMeshProUGUI stateText;
public TextMeshProUGUI groundedStatusText;
public TextMeshProUGUI horizontalSpeedText;
public TextMeshProUGUI verticaSpeedText;
public TextMeshProUGUI moveInputText;
public TextMeshProUGUI isSprintingText;
private void Update()
{
if (playerDebugProvider == null) return;
if (stateText != null)
stateText.text = "State: " + playerDebugProvider.CurrentState;
if (groundedStatusText != null)
groundedStatusText.text = "Grounded: " + playerDebugProvider.GroundedStatus;
if (horizontalSpeedText != null)
horizontalSpeedText.text = "Speed (H): " + playerDebugProvider.HorizontalSpeed.ToString("F2") + " m/s";
if (verticaSpeedText != null)
verticaSpeedText.text = "Speed (V): " + playerDebugProvider.VerticalSpeed.ToString("F2") + " m/s";
if (moveInputText != null)
moveInputText.text = "Input: " + playerDebugProvider.MoveInput.ToString();
if (isSprintingText != null)
isSprintingText.text = "Sprinting: " + (playerDebugProvider.IsSprinting ? "YES" : "NO");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cd13c5c96000414397dd7d41a73edd62
timeCreated: 1773383951

8
Assets/Scripts/VFX.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 793d95c58fcd4034f8b3152f2317a9e5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SlashMeshGenerator : MonoBehaviour
{
[Header("Mesh Settings")]
public int segments = 10; // Số lượng phân đoạn (càng cao càng mượt)
public float length = 5f; // Chiều dài vệt chém
public float width = 0.5f; // Chiều rộng ở giữa
public float curviness = 1f; // Độ cong của nhát chém
[ContextMenu("Generate Slash Mesh")]
public void GenerateMesh()
{
Mesh mesh = new Mesh();
mesh.name = "SukunaSlashMesh";
int vertexCount = (segments + 1) * 2;
Vector3[] vertices = new Vector3[vertexCount];
Vector2[] uvs = new Vector2[vertexCount];
int[] triangles = new int[segments * 6];
for (int i = 0; i <= segments; i++)
{
float t = (float)i / segments; // Tiến trình từ 0 đến 1
// Tính toán vị trí X (chiều dài)
float x = (t - 0.5f) * length;
// Tính toán độ nhọn (Width Taper): Nhỏ ở 2 đầu, to ở giữa
// Dùng hàm Sin để tạo độ mượt hoặc (1 - |2t-1|)
float currentWidth = Mathf.Sin(t * Mathf.PI) * width;
// Tính toán độ cong (Y Offset)
float yOffset = Mathf.Pow((t - 0.5f) * 2f, 2f) * curviness;
// Tạo 2 đỉnh (trên và dưới) cho mỗi phân đoạn
vertices[i * 2] = new Vector3(x, yOffset + currentWidth / 2f, 0);
vertices[i * 2 + 1] = new Vector3(x, yOffset - currentWidth / 2f, 0);
// Gán UV (để Shader chạy đúng)
uvs[i * 2] = new Vector2(t, 1);
uvs[i * 2 + 1] = new Vector2(t, 0);
// Tạo tam giác (trừ phân đoạn cuối)
if (i < segments)
{
int start = i * 2;
triangles[i * 6] = start;
triangles[i * 6 + 1] = start + 2;
triangles[i * 6 + 2] = start + 1;
triangles[i * 6 + 3] = start + 1;
triangles[i * 6 + 4] = start + 2;
triangles[i * 6 + 5] = start + 3;
}
}
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
GetComponent<MeshFilter>().mesh = mesh;
}
void Awake()
{
GenerateMesh();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 78fcc45270373164cadbaa6681bab73b

View File

@@ -0,0 +1,69 @@
using UnityEngine;
using OnlyScove.Scripts;
public class SukunaAbilityController : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField] private InputReader inputReader;
[Header("VFX Projectiles")]
public GameObject blackProjectilePrefab;
public GameObject redProjectilePrefab;
[Header("Settings")]
public float attackRate = 0.15f;
public float forwardOffset = 1.5f;
public float verticalOffset = 1.0f;
[Header("Random Rotation Ranges")]
public Vector2 rangeX = new Vector2(-360f, 360f);
public Vector2 rangeY = new Vector2(-10f, 10f);
public Vector2 rangeZ = new Vector2(50f, 120f);
private float lastAttackTime = 0f;
private void Update()
{
if (inputReader != null && inputReader.IsAttackHeld)
{
if (Time.time - lastAttackTime >= attackRate)
{
PerformDismantle();
lastAttackTime = Time.time;
}
}
}
private void PerformDismantle()
{
GameObject selectedPrefab = GetRandomSlashVariant();
if (selectedPrefab == null) return;
// Vị trí spawn trước mặt Player
Vector3 spawnPos = transform.position + transform.forward * forwardOffset + Vector3.up * verticalOffset;
// Tạo góc xoay ngẫu nhiên theo yêu cầu của bạn
float randX = Random.Range(rangeX.x, rangeX.y);
float randY = Random.Range(rangeY.x, rangeY.y);
float randZ = Random.Range(rangeZ.x, rangeZ.y);
// Kết hợp với hướng của Player
Quaternion spawnRot = transform.rotation * Quaternion.Euler(randX, randY, randZ);
// Tạo đạn
GameObject projectile = Instantiate(selectedPrefab, spawnPos, spawnRot);
// Bắt đạn bay về phía trước (hướng nhìn của Player)
if (projectile.TryGetComponent<SukunaProjectile>(out var projScript))
{
projScript.SetDirection(transform.forward);
}
}
private GameObject GetRandomSlashVariant()
{
float chance = Random.Range(0f, 100f);
if (chance <= 20f) return redProjectilePrefab != null ? redProjectilePrefab : blackProjectilePrefab;
return blackProjectilePrefab;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1630760c9d97a5f4eb1bc179549c95cd

View File

@@ -0,0 +1,229 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections;
using System.Collections.Generic;
namespace OnlyScove.Scripts
{
public class SukunaDomainController : MonoBehaviour
{
[Header("References")]
public PlayerStateMachine playerStateMachine;
public GameObject slashPrefab;
public GameObject shrinePrefab;
public VolumeProfile domainVolumeProfile;
public Transform cinematicCameraPoint;
[Header("Domain Settings")]
public float domainRadius = 15f;
public float domainDuration = 10f;
[Tooltip("Số lượng vệt chém tạo ra mỗi giây")]
public float slashRate = 15f;
public float shrineRiseHeight = 7f; // Độ sâu bắt đầu của miếu
[Tooltip("Khoảng cách từ Pivot của Miếu đến mặt sàn nơi Player đứng")]
public float shrineFloorOffset = 0.5f;
public float camMoveSpeed = 4f;
private bool isActive = false;
private List<GameObject> activeSlashes = new List<GameObject>();
private Volume localVolume;
private GameObject spawnedShrine;
private void Start()
{
if (playerStateMachine == null)
playerStateMachine = GetComponent<PlayerStateMachine>();
if (playerStateMachine != null && playerStateMachine.Input != null)
{
playerStateMachine.Input.OnPreviousInteractEvent += HandleDomainExpansion;
Debug.Log("<color=green>[Sukuna] Sẵn sàng. slashRate: " + slashRate + "</color>");
}
}
private void OnDestroy()
{
if (playerStateMachine != null && playerStateMachine.Input != null)
{
playerStateMachine.Input.OnPreviousInteractEvent -= HandleDomainExpansion;
}
}
private void HandleDomainExpansion()
{
if (isActive) return;
Debug.Log("<color=red>[Sukuna] RYŌIKI TENKAI: FUKUMA MIZUZUSHI!</color>");
StartCoroutine(DomainSequence());
}
private IEnumerator DomainSequence()
{
isActive = true;
// Lưu vị trí ban đầu của Player (Vị trí thực tế trên mặt đất)
Vector3 playerStartPos = playerStateMachine.transform.position;
playerStateMachine.SetControl(false);
CameraController camController = playerStateMachine.Cam;
bool originalCamEnabled = true;
Transform mainCam = Camera.main.transform;
if (camController != null)
{
originalCamEnabled = camController.enabled;
camController.enabled = false;
}
// 1. Tạo Volume (Bóng bong lãnh địa)
GameObject volumeObj = new GameObject("SukunaDomainVolume");
volumeObj.transform.position = playerStartPos;
localVolume = volumeObj.AddComponent<Volume>();
localVolume.isGlobal = false;
localVolume.priority = 100;
localVolume.profile = domainVolumeProfile;
SphereCollider volumeCollider = volumeObj.AddComponent<SphereCollider>();
volumeCollider.isTrigger = true;
volumeCollider.radius = 0.1f;
// 2. Mọc miếu và đẩy Player
if (shrinePrefab != null)
{
// Spawn miếu ở vị trí rất sâu dưới chân Player
Vector3 shrineSpawnPos = playerStartPos - Vector3.up * shrineRiseHeight;
spawnedShrine = Instantiate(shrinePrefab, shrineSpawnPos, playerStateMachine.transform.rotation);
float riseDuration = 2.0f;
float elapsed = 0;
while (elapsed < riseDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0, 1, elapsed / riseDuration);
// Di chuyển miếu lên dần dần
Vector3 currentShrinePos = Vector3.Lerp(shrineSpawnPos, playerStartPos, t);
spawnedShrine.transform.position = currentShrinePos;
// Logic đẩy Player:
// floorY là độ cao mặt sàn của miếu tại khung hình hiện tại
float floorY = currentShrinePos.y + shrineFloorOffset;
// Nếu mặt sàn của miếu đã trồi lên cao hơn vị trí chân Player ban đầu
if (floorY > playerStartPos.y)
{
// Player đi theo miếu
playerStateMachine.transform.position = new Vector3(playerStartPos.x, floorY, playerStartPos.z);
}
else
{
// Player đứng yên trên mặt đất ban đầu, chờ miếu trồi lên đỡ
playerStateMachine.transform.position = playerStartPos;
}
// Mở rộng bán kính Volume
volumeCollider.radius = Mathf.Lerp(0.1f, domainRadius, t);
// Lia Camera mượt mà
if (cinematicCameraPoint != null)
{
mainCam.position = Vector3.Lerp(mainCam.position, cinematicCameraPoint.position, Time.deltaTime * camMoveSpeed);
mainCam.rotation = Quaternion.Slerp(mainCam.rotation, Quaternion.LookRotation((playerStateMachine.transform.position + Vector3.up * 2f) - mainCam.position), Time.deltaTime * camMoveSpeed);
}
yield return null;
}
}
// 3. Thực thi chém liên tục dựa trên slashRate
float timer = 0;
float slashCooldown = 1f / slashRate;
float lastSlashTime = 0;
while (timer < domainDuration)
{
timer += Time.deltaTime;
if (timer - lastSlashTime >= slashCooldown)
{
SpawnRandomSlash(playerStateMachine.transform.position);
lastSlashTime = timer;
}
// Camera luôn theo dõi Player trên đỉnh miếu
if (cinematicCameraPoint != null)
{
mainCam.position = Vector3.Lerp(mainCam.position, cinematicCameraPoint.position, Time.deltaTime * camMoveSpeed * 0.5f);
mainCam.LookAt(playerStateMachine.transform.position + Vector3.up * 2f);
}
yield return null;
}
// 4. Miếu sụp xuống (Player đứng lại vị trí Y ban đầu)
if (spawnedShrine != null)
{
float sinkTime = 1f;
float elapsed = 0;
Vector3 currentShrinePos = spawnedShrine.transform.position;
Vector3 targetSinkPos = currentShrinePos - Vector3.up * shrineRiseHeight;
while (elapsed < sinkTime)
{
elapsed += Time.deltaTime;
float t = elapsed / sinkTime;
spawnedShrine.transform.position = Vector3.Lerp(currentShrinePos, targetSinkPos, t);
// Player từ từ hạ xuống sàn ban đầu
float floorY = spawnedShrine.transform.position.y + shrineFloorOffset;
if (floorY > playerStartPos.y)
playerStateMachine.transform.position = new Vector3(playerStartPos.x, floorY, playerStartPos.z);
else
playerStateMachine.transform.position = playerStartPos;
yield return null;
}
Destroy(spawnedShrine);
}
// 5. Thu nhỏ Volume
float shrinkDuration = 0.5f;
float sElapsed = 0;
while (sElapsed < shrinkDuration)
{
sElapsed += Time.deltaTime;
volumeCollider.radius = Mathf.Lerp(domainRadius, 0.1f, sElapsed / shrinkDuration);
yield return null;
}
Destroy(volumeObj);
// Dọn dẹp
foreach (var s in activeSlashes) if (s != null) Destroy(s);
activeSlashes.Clear();
if (camController != null) camController.enabled = originalCamEnabled;
playerStateMachine.SetControl(true);
isActive = false;
}
private void SpawnRandomSlash(Vector3 center)
{
Vector2 randCircle = Random.insideUnitCircle * domainRadius;
Vector3 spawnPos = center + new Vector3(randCircle.x, Random.Range(1f, 6f), randCircle.y);
if (slashPrefab != null)
{
GameObject slash = Instantiate(slashPrefab, spawnPos, Random.rotation);
slash.transform.localScale *= Random.Range(0.6f, 2.5f);
activeSlashes.Add(slash);
StartCoroutine(DestroySlashAfterTime(slash, 0.7f));
}
}
private IEnumerator DestroySlashAfterTime(GameObject slash, float time)
{
yield return new WaitForSeconds(time);
if (activeSlashes != null && activeSlashes.Contains(slash)) activeSlashes.Remove(slash);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 271dd39a46bad974485107bb1a070e0a

View File

@@ -0,0 +1,27 @@
using UnityEngine;
public class SukunaProjectile : MonoBehaviour
{
[Header("Movement")]
public float speed = 50f;
public float lifetime = 3f;
private Vector3 moveDirection;
public void SetDirection(Vector3 direction)
{
// Nhận hướng bay từ Player (luôn là hướng phía trước)
moveDirection = direction.normalized;
}
void Start()
{
Destroy(gameObject, lifetime);
}
void Update()
{
// Di chuyển đạn theo hướng đã gán, bất kể góc xoay hiển thị của nó là gì
transform.position += moveDirection * speed * Time.deltaTime;
}
}

View File

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

View File

@@ -0,0 +1,53 @@
using UnityEngine;
public class SukunaSlashEffect : MonoBehaviour
{
[Header("Settings")]
public float duration = 0.2f;
public float maxScale = 5f;
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Header("Visuals")]
private MeshRenderer meshRenderer;
private MaterialPropertyBlock propBlock;
private float timer = 0f;
private static readonly int DissolveId = Shader.PropertyToID("_Dissolve");
void Awake()
{
meshRenderer = GetComponent<MeshRenderer>();
propBlock = new MaterialPropertyBlock();
}
void Start()
{
// Reset scale ban đầu
transform.localScale = new Vector3(maxScale, 0.1f, 0.1f);
}
void Update()
{
timer += Time.deltaTime;
float normalizedTime = timer / duration;
if (normalizedTime <= 1.0f)
{
// Mở rộng vệt chém theo chiều ngang (Y hoặc Z tùy mesh)
float currentScaleY = scaleCurve.Evaluate(normalizedTime) * maxScale;
transform.localScale = new Vector3(maxScale, currentScaleY, 1f);
// Điều khiển Shader bằng MaterialPropertyBlock
if (meshRenderer != null)
{
meshRenderer.GetPropertyBlock(propBlock);
propBlock.SetFloat(DissolveId, normalizedTime);
meshRenderer.SetPropertyBlock(propBlock);
}
}
else
{
// Tự hủy sau khi xong
Destroy(gameObject);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 66ad11f71e7aac841be73f4b03cf0d83