Commit 1
This commit is contained in:
8
Assets/Scripts/Camera Controller.meta
Normal file
8
Assets/Scripts/Camera Controller.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 962e9e0d2b8d78d4fbb25fb03224f618
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
287
Assets/Scripts/Camera Controller/CameraController.cs
Normal file
287
Assets/Scripts/Camera Controller/CameraController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3a2f40f0d755824f91dfa62616cd6fc
|
||||
3
Assets/Scripts/Debug.meta
Normal file
3
Assets/Scripts/Debug.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9477ecbb64ef4d9c8863fb16d2c4bc96
|
||||
timeCreated: 1773383891
|
||||
76
Assets/Scripts/Debug/PlayerDebugProvider.cs
Normal file
76
Assets/Scripts/Debug/PlayerDebugProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Debug/PlayerDebugProvider.cs.meta
Normal file
2
Assets/Scripts/Debug/PlayerDebugProvider.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf6aff0b7e11d41439ac80f4963a0795
|
||||
48
Assets/Scripts/EnvironmentScanner.cs
Normal file
48
Assets/Scripts/EnvironmentScanner.cs
Normal 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;
|
||||
}
|
||||
2
Assets/Scripts/EnvironmentScanner.cs.meta
Normal file
2
Assets/Scripts/EnvironmentScanner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 169e37d35fede30409266070c88b118f
|
||||
8
Assets/Scripts/GameSetup.meta
Normal file
8
Assets/Scripts/GameSetup.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b562adae77c550e4db1edcabf68b0530
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
170
Assets/Scripts/GameSetup/CharacterAutoSetup.cs
Normal file
170
Assets/Scripts/GameSetup/CharacterAutoSetup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta
Normal file
2
Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e16a6690e589f0449ad89a6bf508ab62
|
||||
26
Assets/Scripts/GameSetup/CharacterSetupSettings.cs
Normal file
26
Assets/Scripts/GameSetup/CharacterSetupSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta
Normal file
2
Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d44cb4bd45c0e24bb3d8196a137db00
|
||||
3
Assets/Scripts/Interface.meta
Normal file
3
Assets/Scripts/Interface.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1398e4608a9406cb68d769d6bc9a3ec
|
||||
timeCreated: 1773397974
|
||||
8
Assets/Scripts/Interface/IInteractable.cs
Normal file
8
Assets/Scripts/Interface/IInteractable.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public interface IInteractable
|
||||
{
|
||||
string InteractionPrompt { get; }
|
||||
void OnInteract(PlayerStateMachine player);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Interface/IInteractable.cs.meta
Normal file
2
Assets/Scripts/Interface/IInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f001a3bb0fe5e954ea724ded3158209d
|
||||
8
Assets/Scripts/Optimization.meta
Normal file
8
Assets/Scripts/Optimization.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6d134b82fd19fc4aa38896d5e45efed
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Assets/Scripts/Optimization/AutoPlayerStateMachine.cs
Normal file
49
Assets/Scripts/Optimization/AutoPlayerStateMachine.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbe8532b2e328a249bf61ae957c92486
|
||||
129
Assets/Scripts/Optimization/JobsMovementManager.cs
Normal file
129
Assets/Scripts/Optimization/JobsMovementManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/JobsMovementManager.cs.meta
Normal file
2
Assets/Scripts/Optimization/JobsMovementManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41899df442467714dbee462ae451773a
|
||||
153
Assets/Scripts/Optimization/MassiveSpawner.cs
Normal file
153
Assets/Scripts/Optimization/MassiveSpawner.cs
Normal 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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
2
Assets/Scripts/Optimization/MassiveSpawner.cs.meta
Normal file
2
Assets/Scripts/Optimization/MassiveSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50831f537cbccac4c9bf4067b6b158c7
|
||||
34
Assets/Scripts/Optimization/PerformanceHUD.cs
Normal file
34
Assets/Scripts/Optimization/PerformanceHUD.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/PerformanceHUD.cs.meta
Normal file
2
Assets/Scripts/Optimization/PerformanceHUD.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c9a374a3ab089d41a6784b1ffad3b6f
|
||||
85
Assets/Scripts/Optimization/StressTestSpawner.cs
Normal file
85
Assets/Scripts/Optimization/StressTestSpawner.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/StressTestSpawner.cs.meta
Normal file
2
Assets/Scripts/Optimization/StressTestSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a7c5ef310b7f354685dc6706be2d530
|
||||
8
Assets/Scripts/Player Controller.meta
Normal file
8
Assets/Scripts/Player Controller.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e0fd09e39d1b90458b7097e555a9f3f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
89
Assets/Scripts/Player Controller/InputReader.cs
Normal file
89
Assets/Scripts/Player Controller/InputReader.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/InputReader.cs.meta
Normal file
2
Assets/Scripts/Player Controller/InputReader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5962d8f2c8e40e240a4a4907c7b539fa
|
||||
12
Assets/Scripts/Player Controller/ParkourAction.cs
Normal file
12
Assets/Scripts/Player Controller/ParkourAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/ParkourAction.cs.meta
Normal file
2
Assets/Scripts/Player Controller/ParkourAction.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 985468dedaf32f44191b2cc29c813c8c
|
||||
59
Assets/Scripts/Player Controller/ParkourController.cs
Normal file
59
Assets/Scripts/Player Controller/ParkourController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0407ea3fbe445ac43b4f1ca3077ce283
|
||||
58
Assets/Scripts/Player Controller/PlayerAirDashState.cs
Normal file
58
Assets/Scripts/Player Controller/PlayerAirDashState.cs
Normal 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() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43c3e5e400fea604fb432f84d5dd9ce1
|
||||
25
Assets/Scripts/Player Controller/PlayerBaseState.cs
Normal file
25
Assets/Scripts/Player Controller/PlayerBaseState.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerBaseState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerBaseState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af622221f1750284885366b02b8bfbba
|
||||
134
Assets/Scripts/Player Controller/PlayerController.cs
Normal file
134
Assets/Scripts/Player Controller/PlayerController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd897e8bcaabdc8408bb6aaf7c037537
|
||||
97
Assets/Scripts/Player Controller/PlayerCrouchState.cs
Normal file
97
Assets/Scripts/Player Controller/PlayerCrouchState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82b04241d720e444a937973caed8eb42
|
||||
86
Assets/Scripts/Player Controller/PlayerDashState.cs
Normal file
86
Assets/Scripts/Player Controller/PlayerDashState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerDashState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerDashState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03721c0a319be8c4093b616e2f46afe8
|
||||
65
Assets/Scripts/Player Controller/PlayerDodgeState.cs
Normal file
65
Assets/Scripts/Player Controller/PlayerDodgeState.cs
Normal 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() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71bce3b0373df774c8b1598487bcd4ee
|
||||
82
Assets/Scripts/Player Controller/PlayerFallState.cs
Normal file
82
Assets/Scripts/Player Controller/PlayerFallState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerFallState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerFallState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2b239715e32b6a4791f677e0159b862
|
||||
71
Assets/Scripts/Player Controller/PlayerIdleState.cs
Normal file
71
Assets/Scripts/Player Controller/PlayerIdleState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerIdleState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerIdleState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd83e34f11c7aaa4ab144ee59db04e8d
|
||||
38
Assets/Scripts/Player Controller/PlayerInteractState.cs
Normal file
38
Assets/Scripts/Player Controller/PlayerInteractState.cs
Normal 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() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b772d1c0c26c634fad5e71b43db9385
|
||||
52
Assets/Scripts/Player Controller/PlayerJumpState.cs
Normal file
52
Assets/Scripts/Player Controller/PlayerJumpState.cs
Normal 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() {}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerJumpState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerJumpState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ab5bb519df10fe45a5af5f45111ed4d
|
||||
97
Assets/Scripts/Player Controller/PlayerMoveState.cs
Normal file
97
Assets/Scripts/Player Controller/PlayerMoveState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerMoveState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerMoveState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a11b0f0549f3dbd45bbdfd8114586a43
|
||||
53
Assets/Scripts/Player Controller/PlayerParkourState.cs
Normal file
53
Assets/Scripts/Player Controller/PlayerParkourState.cs
Normal 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() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9b73ee252f157e49a08f8f7566dc6ac
|
||||
94
Assets/Scripts/Player Controller/PlayerRunState.cs
Normal file
94
Assets/Scripts/Player Controller/PlayerRunState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerRunState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerRunState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef096feea261a8d449384a68f71470b3
|
||||
177
Assets/Scripts/Player Controller/PlayerStateMachine.cs
Normal file
177
Assets/Scripts/Player Controller/PlayerStateMachine.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 848ad6fdeb60b254497391392419b063
|
||||
43
Assets/Scripts/Player Controller/PlayerThrustState.cs
Normal file
43
Assets/Scripts/Player Controller/PlayerThrustState.cs
Normal 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() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e76cfd208cc6f44b8b954147ee1defb
|
||||
65
Assets/Scripts/SpineProxy.cs
Normal file
65
Assets/Scripts/SpineProxy.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SpineProxy.cs.meta
Normal file
2
Assets/Scripts/SpineProxy.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 682245d9ac89ba4409aa3a92f17f5c6c
|
||||
8
Assets/Scripts/StickyNote.cs
Normal file
8
Assets/Scripts/StickyNote.cs
Normal 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
|
||||
}
|
||||
2
Assets/Scripts/StickyNote.cs.meta
Normal file
2
Assets/Scripts/StickyNote.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20733ff6cdef89e408acddf8ce51503d
|
||||
3
Assets/Scripts/UI.meta
Normal file
3
Assets/Scripts/UI.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b95b0bf23143b68483f912b558e6f0
|
||||
timeCreated: 1773383929
|
||||
42
Assets/Scripts/UI/MyUIDisplay.cs
Normal file
42
Assets/Scripts/UI/MyUIDisplay.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/MyUIDisplay.cs.meta
Normal file
3
Assets/Scripts/UI/MyUIDisplay.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd13c5c96000414397dd7d41a73edd62
|
||||
timeCreated: 1773383951
|
||||
8
Assets/Scripts/VFX.meta
Normal file
8
Assets/Scripts/VFX.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 793d95c58fcd4034f8b3152f2317a9e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
71
Assets/Scripts/VFX/SlashMeshGenerator.cs
Normal file
71
Assets/Scripts/VFX/SlashMeshGenerator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SlashMeshGenerator.cs.meta
Normal file
2
Assets/Scripts/VFX/SlashMeshGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78fcc45270373164cadbaa6681bab73b
|
||||
69
Assets/Scripts/VFX/SukunaAbilityController.cs
Normal file
69
Assets/Scripts/VFX/SukunaAbilityController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaAbilityController.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaAbilityController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1630760c9d97a5f4eb1bc179549c95cd
|
||||
229
Assets/Scripts/VFX/SukunaDomainController.cs
Normal file
229
Assets/Scripts/VFX/SukunaDomainController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaDomainController.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaDomainController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 271dd39a46bad974485107bb1a070e0a
|
||||
27
Assets/Scripts/VFX/SukunaProjectile.cs
Normal file
27
Assets/Scripts/VFX/SukunaProjectile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaProjectile.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaProjectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9b0aad9f1697954a9f8710b4e8f3f2e
|
||||
53
Assets/Scripts/VFX/SukunaSlashEffect.cs
Normal file
53
Assets/Scripts/VFX/SukunaSlashEffect.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaSlashEffect.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaSlashEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66ad11f71e7aac841be73f4b03cf0d83
|
||||
Reference in New Issue
Block a user