From f42ef22a13d54346979139cbf7cc7e718f3b414f Mon Sep 17 00:00:00 2001 From: Scove <104053980+Scove-Metalreal@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:27:19 +0700 Subject: [PATCH] Commit 1 --- Assets/{TutorialInfo.meta => Editor.meta} | 2 +- Assets/Editor/AddStickyNoteContextMenu.cs | 45 ++ .../Editor/AddStickyNoteContextMenu.cs.meta | 2 + Assets/Editor/AutoSaveTool.cs | 175 +++++ Assets/Editor/AutoSaveTool.cs.meta | 2 + Assets/Editor/CameraBookmarksTool.cs | 226 ++++++ Assets/Editor/CameraBookmarksTool.cs.meta | 2 + Assets/Editor/DistributeTool.cs | 131 ++++ Assets/Editor/DistributeTool.cs.meta | 2 + Assets/Editor/HierarchyEnhancer.cs | 81 +++ Assets/Editor/HierarchyEnhancer.cs.meta | 2 + Assets/Editor/HierarchySeparators.cs | 84 +++ Assets/Editor/HierarchySeparators.cs.meta | 2 + Assets/Editor/LevelDecorator.cs | 189 +++++ Assets/Editor/LevelDecorator.cs.meta | 2 + Assets/Editor/MeasureTool.cs | 149 ++++ Assets/Editor/MeasureTool.cs.meta | 2 + Assets/Editor/PlayFromHereTool.cs | 215 ++++++ Assets/Editor/PlayFromHereTool.cs.meta | 2 + Assets/Editor/ProjectAudioPreview.cs | 163 +++++ Assets/Editor/ProjectAudioPreview.cs.meta | 2 + Assets/Editor/ProjectDashboardTool.cs | 226 ++++++ Assets/Editor/ProjectDashboardTool.cs.meta | 2 + Assets/Editor/ProjectNavigation.cs | 95 +++ Assets/Editor/ProjectNavigation.cs.meta | 2 + Assets/Editor/ReferenceFinderTool.cs | 161 +++++ Assets/Editor/ReferenceFinderTool.cs.meta | 2 + Assets/Editor/SmartBootstrapper.cs | 158 +++++ Assets/Editor/SmartBootstrapper.cs.meta | 2 + Assets/Editor/StickyNoteEditor.cs | 151 ++++ Assets/Editor/StickyNoteEditor.cs.meta | 2 + Assets/Editor/TimeLord.cs | 146 ++++ Assets/Editor/TimeLord.cs.meta | 2 + Assets/Readme.asset | 34 - Assets/Readme.asset.meta | 8 - Assets/{TutorialInfo => }/Scripts.meta | 5 +- .../Camera Controller.meta} | 5 +- .../Camera Controller/CameraController.cs | 287 ++++++++ .../CameraController.cs.meta | 2 + Assets/Scripts/Debug.meta | 3 + Assets/Scripts/Debug/PlayerDebugProvider.cs | 76 ++ .../Scripts/Debug/PlayerDebugProvider.cs.meta | 2 + Assets/Scripts/EnvironmentScanner.cs | 48 ++ Assets/Scripts/EnvironmentScanner.cs.meta | 2 + .../Icons.meta => Scripts/GameSetup.meta} | 5 +- .../Scripts/GameSetup/CharacterAutoSetup.cs | 170 +++++ .../GameSetup/CharacterAutoSetup.cs.meta | 2 + .../GameSetup/CharacterSetupSettings.cs | 26 + .../GameSetup/CharacterSetupSettings.cs.meta | 2 + Assets/Scripts/Interface.meta | 3 + Assets/Scripts/Interface/IInteractable.cs | 8 + .../Scripts/Interface/IInteractable.cs.meta | 2 + Assets/Scripts/Optimization.meta | 8 + .../Optimization/AutoPlayerStateMachine.cs | 49 ++ .../AutoPlayerStateMachine.cs.meta | 2 + .../Optimization/JobsMovementManager.cs | 129 ++++ .../Optimization/JobsMovementManager.cs.meta | 2 + Assets/Scripts/Optimization/MassiveSpawner.cs | 153 ++++ .../Optimization/MassiveSpawner.cs.meta | 2 + Assets/Scripts/Optimization/PerformanceHUD.cs | 34 + .../Optimization/PerformanceHUD.cs.meta | 2 + .../Scripts/Optimization/StressTestSpawner.cs | 85 +++ .../Optimization/StressTestSpawner.cs.meta | 2 + Assets/Scripts/Player Controller.meta | 8 + .../Scripts/Player Controller/InputReader.cs | 89 +++ .../Player Controller/InputReader.cs.meta | 2 + .../Player Controller/ParkourAction.cs | 12 + .../Player Controller/ParkourAction.cs.meta | 2 + .../Player Controller/ParkourController.cs | 59 ++ .../ParkourController.cs.meta | 2 + .../Player Controller/PlayerAirDashState.cs | 58 ++ .../PlayerAirDashState.cs.meta | 2 + .../Player Controller/PlayerBaseState.cs | 25 + .../Player Controller/PlayerBaseState.cs.meta | 2 + .../Player Controller/PlayerController.cs | 134 ++++ .../PlayerController.cs.meta | 2 + .../Player Controller/PlayerCrouchState.cs | 97 +++ .../PlayerCrouchState.cs.meta | 2 + .../Player Controller/PlayerDashState.cs | 86 +++ .../Player Controller/PlayerDashState.cs.meta | 2 + .../Player Controller/PlayerDodgeState.cs | 65 ++ .../PlayerDodgeState.cs.meta | 2 + .../Player Controller/PlayerFallState.cs | 82 +++ .../Player Controller/PlayerFallState.cs.meta | 2 + .../Player Controller/PlayerIdleState.cs | 71 ++ .../Player Controller/PlayerIdleState.cs.meta | 2 + .../Player Controller/PlayerInteractState.cs | 38 + .../PlayerInteractState.cs.meta | 2 + .../Player Controller/PlayerJumpState.cs | 52 ++ .../Player Controller/PlayerJumpState.cs.meta | 2 + .../Player Controller/PlayerMoveState.cs | 97 +++ .../Player Controller/PlayerMoveState.cs.meta | 2 + .../Player Controller/PlayerParkourState.cs | 53 ++ .../PlayerParkourState.cs.meta | 2 + .../Player Controller/PlayerRunState.cs | 94 +++ .../Player Controller/PlayerRunState.cs.meta | 2 + .../Player Controller/PlayerStateMachine.cs | 177 +++++ .../PlayerStateMachine.cs.meta | 2 + .../Player Controller/PlayerThrustState.cs | 43 ++ .../PlayerThrustState.cs.meta | 2 + Assets/Scripts/SpineProxy.cs | 65 ++ Assets/Scripts/SpineProxy.cs.meta | 2 + Assets/Scripts/StickyNote.cs | 8 + Assets/Scripts/StickyNote.cs.meta | 2 + Assets/Scripts/UI.meta | 3 + Assets/Scripts/UI/MyUIDisplay.cs | 42 ++ Assets/Scripts/UI/MyUIDisplay.cs.meta | 3 + Assets/Scripts/VFX.meta | 8 + Assets/Scripts/VFX/SlashMeshGenerator.cs | 71 ++ Assets/Scripts/VFX/SlashMeshGenerator.cs.meta | 2 + Assets/Scripts/VFX/SukunaAbilityController.cs | 69 ++ .../VFX/SukunaAbilityController.cs.meta | 2 + Assets/Scripts/VFX/SukunaDomainController.cs | 229 ++++++ .../VFX/SukunaDomainController.cs.meta | 2 + Assets/Scripts/VFX/SukunaProjectile.cs | 27 + Assets/Scripts/VFX/SukunaProjectile.cs.meta | 2 + Assets/Scripts/VFX/SukunaSlashEffect.cs | 53 ++ Assets/Scripts/VFX/SukunaSlashEffect.cs.meta | 2 + Assets/Settings/PC_RPAsset.asset | 14 +- Assets/TutorialInfo/Icons/URP.png | Bin 24069 -> 0 bytes Assets/TutorialInfo/Icons/URP.png.meta | 134 ---- Assets/TutorialInfo/Layout.wlt | 654 ------------------ Assets/TutorialInfo/Layout.wlt.meta | 8 - .../Scripts/Editor/ReadmeEditor.cs | 242 ------- .../Scripts/Editor/ReadmeEditor.cs.meta | 12 - Assets/TutorialInfo/Scripts/Readme.cs | 16 - Assets/TutorialInfo/Scripts/Readme.cs.meta | 12 - ProjectSettings/ShaderGraphSettings.asset | 1 + ProjectSettings/URPProjectSettings.asset | 3 +- 129 files changed, 5517 insertions(+), 1134 deletions(-) rename Assets/{TutorialInfo.meta => Editor.meta} (77%) create mode 100644 Assets/Editor/AddStickyNoteContextMenu.cs create mode 100644 Assets/Editor/AddStickyNoteContextMenu.cs.meta create mode 100644 Assets/Editor/AutoSaveTool.cs create mode 100644 Assets/Editor/AutoSaveTool.cs.meta create mode 100644 Assets/Editor/CameraBookmarksTool.cs create mode 100644 Assets/Editor/CameraBookmarksTool.cs.meta create mode 100644 Assets/Editor/DistributeTool.cs create mode 100644 Assets/Editor/DistributeTool.cs.meta create mode 100644 Assets/Editor/HierarchyEnhancer.cs create mode 100644 Assets/Editor/HierarchyEnhancer.cs.meta create mode 100644 Assets/Editor/HierarchySeparators.cs create mode 100644 Assets/Editor/HierarchySeparators.cs.meta create mode 100644 Assets/Editor/LevelDecorator.cs create mode 100644 Assets/Editor/LevelDecorator.cs.meta create mode 100644 Assets/Editor/MeasureTool.cs create mode 100644 Assets/Editor/MeasureTool.cs.meta create mode 100644 Assets/Editor/PlayFromHereTool.cs create mode 100644 Assets/Editor/PlayFromHereTool.cs.meta create mode 100644 Assets/Editor/ProjectAudioPreview.cs create mode 100644 Assets/Editor/ProjectAudioPreview.cs.meta create mode 100644 Assets/Editor/ProjectDashboardTool.cs create mode 100644 Assets/Editor/ProjectDashboardTool.cs.meta create mode 100644 Assets/Editor/ProjectNavigation.cs create mode 100644 Assets/Editor/ProjectNavigation.cs.meta create mode 100644 Assets/Editor/ReferenceFinderTool.cs create mode 100644 Assets/Editor/ReferenceFinderTool.cs.meta create mode 100644 Assets/Editor/SmartBootstrapper.cs create mode 100644 Assets/Editor/SmartBootstrapper.cs.meta create mode 100644 Assets/Editor/StickyNoteEditor.cs create mode 100644 Assets/Editor/StickyNoteEditor.cs.meta create mode 100644 Assets/Editor/TimeLord.cs create mode 100644 Assets/Editor/TimeLord.cs.meta delete mode 100644 Assets/Readme.asset delete mode 100644 Assets/Readme.asset.meta rename Assets/{TutorialInfo => }/Scripts.meta (57%) rename Assets/{TutorialInfo/Scripts/Editor.meta => Scripts/Camera Controller.meta} (57%) create mode 100644 Assets/Scripts/Camera Controller/CameraController.cs create mode 100644 Assets/Scripts/Camera Controller/CameraController.cs.meta create mode 100644 Assets/Scripts/Debug.meta create mode 100644 Assets/Scripts/Debug/PlayerDebugProvider.cs create mode 100644 Assets/Scripts/Debug/PlayerDebugProvider.cs.meta create mode 100644 Assets/Scripts/EnvironmentScanner.cs create mode 100644 Assets/Scripts/EnvironmentScanner.cs.meta rename Assets/{TutorialInfo/Icons.meta => Scripts/GameSetup.meta} (57%) create mode 100644 Assets/Scripts/GameSetup/CharacterAutoSetup.cs create mode 100644 Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta create mode 100644 Assets/Scripts/GameSetup/CharacterSetupSettings.cs create mode 100644 Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta create mode 100644 Assets/Scripts/Interface.meta create mode 100644 Assets/Scripts/Interface/IInteractable.cs create mode 100644 Assets/Scripts/Interface/IInteractable.cs.meta create mode 100644 Assets/Scripts/Optimization.meta create mode 100644 Assets/Scripts/Optimization/AutoPlayerStateMachine.cs create mode 100644 Assets/Scripts/Optimization/AutoPlayerStateMachine.cs.meta create mode 100644 Assets/Scripts/Optimization/JobsMovementManager.cs create mode 100644 Assets/Scripts/Optimization/JobsMovementManager.cs.meta create mode 100644 Assets/Scripts/Optimization/MassiveSpawner.cs create mode 100644 Assets/Scripts/Optimization/MassiveSpawner.cs.meta create mode 100644 Assets/Scripts/Optimization/PerformanceHUD.cs create mode 100644 Assets/Scripts/Optimization/PerformanceHUD.cs.meta create mode 100644 Assets/Scripts/Optimization/StressTestSpawner.cs create mode 100644 Assets/Scripts/Optimization/StressTestSpawner.cs.meta create mode 100644 Assets/Scripts/Player Controller.meta create mode 100644 Assets/Scripts/Player Controller/InputReader.cs create mode 100644 Assets/Scripts/Player Controller/InputReader.cs.meta create mode 100644 Assets/Scripts/Player Controller/ParkourAction.cs create mode 100644 Assets/Scripts/Player Controller/ParkourAction.cs.meta create mode 100644 Assets/Scripts/Player Controller/ParkourController.cs create mode 100644 Assets/Scripts/Player Controller/ParkourController.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerAirDashState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerAirDashState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerBaseState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerBaseState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerController.cs create mode 100644 Assets/Scripts/Player Controller/PlayerController.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerCrouchState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerCrouchState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerDashState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerDashState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerDodgeState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerDodgeState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerFallState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerFallState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerIdleState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerIdleState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerInteractState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerInteractState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerJumpState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerJumpState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerMoveState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerMoveState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerParkourState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerParkourState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerRunState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerRunState.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerStateMachine.cs create mode 100644 Assets/Scripts/Player Controller/PlayerStateMachine.cs.meta create mode 100644 Assets/Scripts/Player Controller/PlayerThrustState.cs create mode 100644 Assets/Scripts/Player Controller/PlayerThrustState.cs.meta create mode 100644 Assets/Scripts/SpineProxy.cs create mode 100644 Assets/Scripts/SpineProxy.cs.meta create mode 100644 Assets/Scripts/StickyNote.cs create mode 100644 Assets/Scripts/StickyNote.cs.meta create mode 100644 Assets/Scripts/UI.meta create mode 100644 Assets/Scripts/UI/MyUIDisplay.cs create mode 100644 Assets/Scripts/UI/MyUIDisplay.cs.meta create mode 100644 Assets/Scripts/VFX.meta create mode 100644 Assets/Scripts/VFX/SlashMeshGenerator.cs create mode 100644 Assets/Scripts/VFX/SlashMeshGenerator.cs.meta create mode 100644 Assets/Scripts/VFX/SukunaAbilityController.cs create mode 100644 Assets/Scripts/VFX/SukunaAbilityController.cs.meta create mode 100644 Assets/Scripts/VFX/SukunaDomainController.cs create mode 100644 Assets/Scripts/VFX/SukunaDomainController.cs.meta create mode 100644 Assets/Scripts/VFX/SukunaProjectile.cs create mode 100644 Assets/Scripts/VFX/SukunaProjectile.cs.meta create mode 100644 Assets/Scripts/VFX/SukunaSlashEffect.cs create mode 100644 Assets/Scripts/VFX/SukunaSlashEffect.cs.meta delete mode 100644 Assets/TutorialInfo/Icons/URP.png delete mode 100644 Assets/TutorialInfo/Icons/URP.png.meta delete mode 100644 Assets/TutorialInfo/Layout.wlt delete mode 100644 Assets/TutorialInfo/Layout.wlt.meta delete mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs delete mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta delete mode 100644 Assets/TutorialInfo/Scripts/Readme.cs delete mode 100644 Assets/TutorialInfo/Scripts/Readme.cs.meta diff --git a/Assets/TutorialInfo.meta b/Assets/Editor.meta similarity index 77% rename from Assets/TutorialInfo.meta rename to Assets/Editor.meta index a700bca4..e617f1e5 100644 --- a/Assets/TutorialInfo.meta +++ b/Assets/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ba062aa6c92b140379dbc06b43dd3b9b +guid: bfcf56e2246d90745bf402b615e6b7be folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Editor/AddStickyNoteContextMenu.cs b/Assets/Editor/AddStickyNoteContextMenu.cs new file mode 100644 index 00000000..0c901b36 --- /dev/null +++ b/Assets/Editor/AddStickyNoteContextMenu.cs @@ -0,0 +1,45 @@ +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + public static class AddStickyNoteContextMenu + { + [MenuItem("GameObject/Add Sticky Note", false, 10)] // Menu item at top, with priority 10 + private static void AddStickyNote(MenuCommand menuCommand) + { + // Ensure a GameObject is selected + if (Selection.activeGameObject == null) + { + Debug.LogWarning("No GameObject selected to add Sticky Note."); + return; + } + + GameObject selectedGameObject = Selection.activeGameObject; + + // Check if StickyNote component already exists + if (selectedGameObject.GetComponent() != null) + { + Debug.LogWarning($"StickyNote component already exists on '{selectedGameObject.name}'."); + return; + } + + // Add the StickyNote component + StickyNote stickyNote = selectedGameObject.AddComponent(); + Undo.RegisterCreatedObjectUndo(stickyNote, "Add Sticky Note"); + Debug.Log($"StickyNote added to '{selectedGameObject.name}'."); + } + + // Validate the menu item. + // It will only be enabled if a GameObject is selected and it doesn't already have a StickyNote component. + [MenuItem("GameObject/Add Sticky Note", true)] + private static bool ValidateAddStickyNote() + { + if (Selection.activeGameObject == null) + { + return false; + } + return Selection.activeGameObject.GetComponent() == null; + } + } +} \ No newline at end of file diff --git a/Assets/Editor/AddStickyNoteContextMenu.cs.meta b/Assets/Editor/AddStickyNoteContextMenu.cs.meta new file mode 100644 index 00000000..5da4db05 --- /dev/null +++ b/Assets/Editor/AddStickyNoteContextMenu.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ffe544b9fe35744cba464108a3b4203 \ No newline at end of file diff --git a/Assets/Editor/AutoSaveTool.cs b/Assets/Editor/AutoSaveTool.cs new file mode 100644 index 00000000..ffc8a0ce --- /dev/null +++ b/Assets/Editor/AutoSaveTool.cs @@ -0,0 +1,175 @@ +// =============================================================================== +// AutoSaveTool - Persistent & Robust Auto-Saving for Unity Editor +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// This tool provides a persistent, background auto-saving mechanism for the Unity Editor. +// It runs automatically when Unity starts and saves all open scenes and modified assets +// at a user-defined interval, even when the settings window is closed. +// +// Key Features: +// 1. Runs persistently in the background via [InitializeOnLoad]. +// 2. Saves configuration (interval, status) using EditorPrefs, persisting across Unity sessions. +// 3. Uses System.DateTime for accurate time tracking, unaffected by Play Mode reloads. +// 4. Pauses counting down during Play Mode or compilation to prevent accidental saving. +// +// How to Use: +// 1. Place this script in an 'Editor' folder in your project. +// 2. Open the settings window via: Menu -> Tools -> Auto Save Settings. +// 3. Enable "Bật Auto Save" (Enable Auto Save). +// 4. Set the desired "Thời gian (phút)" (Interval in minutes). +// 5. The tool will now save automatically in the background according to the schedule. +// =============================================================================== + +using System; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace Editor +{ + [InitializeOnLoad] // Critical: Ensures the script runs in the background upon Unity startup + public class AutoSaveTool : EditorWindow + { + // Static Configuration Variables (Persisted via EditorPrefs) + private static bool isAutoSaveEnabled; + private static float saveIntervalMinutes; + private static bool showDebugLog; + + // Time Tracking Variable + private static DateTime nextSaveTime; + + // Static Constructor: Runs when Unity starts or recompiles + static AutoSaveTool() + { + // 1. Load settings from EditorPrefs (persistent storage) + isAutoSaveEnabled = EditorPrefs.GetBool("AutoSave_Enabled", false); + saveIntervalMinutes = EditorPrefs.GetFloat("AutoSave_Interval", 5f); + showDebugLog = EditorPrefs.GetBool("AutoSave_Log", true); + + // 2. Initialize the timer + ResetTimer(); + + // 3. Register the background update loop + EditorApplication.update += OnEditorUpdate; + } + + [MenuItem("Tools/Auto Save Settings")] + public static void ShowWindow() + { + GetWindow("Auto Save"); + } + + private void OnGUI() + { + GUILayout.Label("Auto Save Configuration (Runs in Background)", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Begin tracking changes on the GUI + EditorGUI.BeginChangeCheck(); + + isAutoSaveEnabled = EditorGUILayout.Toggle("Enable Auto Save", isAutoSaveEnabled); + saveIntervalMinutes = EditorGUILayout.FloatField("Interval (Minutes)", saveIntervalMinutes); + showDebugLog = EditorGUILayout.Toggle("Show Debug Log", showDebugLog); + + // If any setting changed, save it immediately to EditorPrefs + if (EditorGUI.EndChangeCheck()) + { + if (saveIntervalMinutes < 0.5f) saveIntervalMinutes = 0.5f; // Minimum 30 seconds + + EditorPrefs.SetBool("AutoSave_Enabled", isAutoSaveEnabled); + EditorPrefs.SetFloat("AutoSave_Interval", saveIntervalMinutes); + EditorPrefs.SetBool("AutoSave_Log", showDebugLog); + + ResetTimer(); // Reset timer based on new settings + } + + EditorGUILayout.Space(); + + if (isAutoSaveEnabled) + { + TimeSpan timeRemaining = nextSaveTime - DateTime.Now; + if (timeRemaining.TotalSeconds < 0) timeRemaining = TimeSpan.Zero; + + // Display warning if Play Mode or Compiling + if (EditorApplication.isPlaying || EditorApplication.isCompiling) + { + EditorGUILayout.HelpBox("Currently in Play Mode or Compiling. Saving is temporarily paused to prevent errors.", MessageType.Warning); + } + else + { + string timeStr = string.Format("{0:00}:{1:00}", timeRemaining.Minutes, timeRemaining.Seconds); + EditorGUILayout.HelpBox($"System is running in background.\nAuto-saving in: {timeStr}", MessageType.Info); + } + + if (GUILayout.Button("Save Now", GUILayout.Height(30))) + { + SaveNow(); + } + } + else + { + EditorGUILayout.HelpBox("Auto Save System is currently DISABLED.", MessageType.Error); + } + } + + // This function runs continuously in the background + private static void OnEditorUpdate() + { + if (!isAutoSaveEnabled) return; + + // If playing or compiling -> PAUSE the countdown (do not reset) + if (EditorApplication.isPlaying || EditorApplication.isCompiling) + { + // Add delta time to nextSaveTime to compensate for time elapsed while paused + nextSaveTime = nextSaveTime.AddSeconds(Time.unscaledDeltaTime); + return; + } + + // Check if it's time to save + if (DateTime.Now >= nextSaveTime) + { + SaveNow(); + } + + // Repaint the GUI if the window is open to keep the countdown smooth + if (HasOpenInstances()) + { + GetWindow().Repaint(); + } + } + + private static void ResetTimer() + { + nextSaveTime = DateTime.Now.AddMinutes(saveIntervalMinutes); + } + + private static void SaveNow() + { + var currentScene = EditorSceneManager.GetActiveScene(); + + bool isSaved = false; + + // 1. Save Current Scene if it is dirty AND has a path (not Untitled) + if (currentScene.isDirty && !string.IsNullOrEmpty(currentScene.path)) + { + EditorSceneManager.SaveOpenScenes(); + isSaved = true; + } + + // 2. Always save Assets (Prefabs, ScriptableObjects, etc.) + AssetDatabase.SaveAssets(); + isSaved = true; + + if (isSaved && showDebugLog) + { + Debug.Log($"[AutoSave] Project saved automatically at {DateTime.Now.ToString("HH:mm:ss")}"); + } + + ResetTimer(); + } + } +} \ No newline at end of file diff --git a/Assets/Editor/AutoSaveTool.cs.meta b/Assets/Editor/AutoSaveTool.cs.meta new file mode 100644 index 00000000..440f5f50 --- /dev/null +++ b/Assets/Editor/AutoSaveTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5a4d73f0e418e4d4f92db04c0d6acc25 \ No newline at end of file diff --git a/Assets/Editor/CameraBookmarksTool.cs b/Assets/Editor/CameraBookmarksTool.cs new file mode 100644 index 00000000..192d9b1f --- /dev/null +++ b/Assets/Editor/CameraBookmarksTool.cs @@ -0,0 +1,226 @@ +// =============================================================================== +// CameraBookmarksTool - Configurable Scene View Camera Save & Load +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 3.0 +// +// Purpose: +// Allows saving and loading specific Scene View camera angles and positions. +// Includes an Editor Window interface to completely customize the keyboard shortcuts. +// +// How to Use: +// 1. Place this script in an 'Editor' folder in your project. +// 2. Open Settings: Menu -> Tools -> Camera Bookmarks Settings. +// 3. Customize your Save/Load modifiers (e.g., Control, Shift, Alt). +// 4. Customize the shortcut keys for Slot 1 to 9. +// 5. Focus the Scene View and use your assigned shortcuts to Save/Load camera angles. +// =============================================================================== + +using UnityEditor; +using UnityEngine; +using System.Globalization; +using System; + +namespace Editor +{ + [InitializeOnLoad] + public class CameraBookmarksTool : EditorWindow + { + // Customizable Shortcut Keys + private static EventModifiers saveModifier; + private static EventModifiers loadModifier; + private static KeyCode[] slotKeys = new KeyCode[9]; + + // Static constructor runs automatically when Unity loads or recompiles + static CameraBookmarksTool() + { + LoadSettings(); + SceneView.duringSceneGui += OnSceneGUI; + }[MenuItem("Tools/Camera Bookmarks Settings")] + public static void ShowWindow() + { + GetWindow("Cam Bookmarks"); + } + + private static void LoadSettings() + { + // Load settings from EditorPrefs, fallback to Control/Shift if not found + saveModifier = (EventModifiers)EditorPrefs.GetInt("CamBM_SaveMod", (int)EventModifiers.Control); + loadModifier = (EventModifiers)EditorPrefs.GetInt("CamBM_LoadMod", (int)EventModifiers.Shift); + + // Load key bindings for 9 slots, fallback to Alpha1-Alpha9 + for (int i = 0; i < 9; i++) + { + int defaultKey = (int)(KeyCode.Alpha1 + i); + slotKeys[i] = (KeyCode)EditorPrefs.GetInt($"CamBM_Key_{i}", defaultKey); + } + } + + private static void SaveSettings() + { + EditorPrefs.SetInt("CamBM_SaveMod", (int)saveModifier); + EditorPrefs.SetInt("CamBM_LoadMod", (int)loadModifier); + + for (int i = 0; i < 9; i++) + { + EditorPrefs.SetInt($"CamBM_Key_{i}", (int)slotKeys[i]); + } + } + + private void OnGUI() + { + GUILayout.Label("Shortcut Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + + // Modifier Keys Setup + saveModifier = (EventModifiers)EditorGUILayout.EnumFlagsField("Save Modifier", saveModifier); + loadModifier = (EventModifiers)EditorGUILayout.EnumFlagsField("Load Modifier", loadModifier); + + if (saveModifier == loadModifier) + { + EditorGUILayout.HelpBox("Warning: Save and Load modifiers are the SAME! This will cause conflicts.", MessageType.Warning); + } + + EditorGUILayout.Space(); + GUILayout.Label("Slot Keys Assignment", EditorStyles.boldLabel); + + // KeyCode Setup for 9 slots + for (int i = 0; i < 9; i++) + { + EditorGUILayout.BeginHorizontal(); + GUILayout.Label($"Slot {i + 1}", GUILayout.Width(100)); + slotKeys[i] = (KeyCode)EditorGUILayout.EnumPopup(slotKeys[i]); + EditorGUILayout.EndHorizontal(); + } + + if (EditorGUI.EndChangeCheck()) + { + SaveSettings(); // Save immediately if anything changes + } + + EditorGUILayout.Space(); + EditorGUILayout.Space(); + + // Reset Button + if (GUILayout.Button("Reset to Default Settings", GUILayout.Height(30))) + { + saveModifier = EventModifiers.Control; + loadModifier = EventModifiers.Shift; + for (int i = 0; i < 9; i++) slotKeys[i] = KeyCode.Alpha1 + i; + + SaveSettings(); + GUI.FocusControl(null); // Remove focus to refresh UI correctly + } + } + + static void OnSceneGUI(SceneView view) + { + Event e = Event.current; + + if (e.type == EventType.KeyDown) + { + // Mask out CapsLock, NumLock, and Function keys. We only care about main modifiers. + EventModifiers currentMods = e.modifiers & (EventModifiers.Shift | EventModifiers.Control | EventModifiers.Alt | EventModifiers.Command); + + // Check if the pressed key matches any of our custom assigned slot keys + int pressedSlotIndex = -1; + for (int i = 0; i < 9; i++) + { + if (e.keyCode == slotKeys[i] && e.keyCode != KeyCode.None) + { + pressedSlotIndex = i + 1; + break; + } + } + + if (pressedSlotIndex != -1) + { + string prefsKey = $"CamBookmark_Slot_{pressedSlotIndex}"; + + // SAVE ACTION + if (currentMods == saveModifier) + { + SaveBookmark(view, prefsKey, pressedSlotIndex); + e.Use(); // Consume the event + } + // LOAD ACTION + else if (currentMods == loadModifier) + { + LoadBookmark(view, prefsKey, pressedSlotIndex); + e.Use(); // Consume the event + } + } + } + } + + private static void SaveBookmark(SceneView view, string key, int slotIndex) + { + Transform camTransform = view.camera.transform; + float size = view.size; + + // Use InvariantCulture to ensure dot (.) is used for decimals, preventing regional bugs + string data = string.Format(CultureInfo.InvariantCulture, + "{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}", + camTransform.position.x, camTransform.position.y, camTransform.position.z, + camTransform.rotation.x, camTransform.rotation.y, camTransform.rotation.z, camTransform.rotation.w, + size); + + EditorPrefs.SetString(key, data); + + // Show feedback in Scene View + string message = $"Saved Camera Bookmark to Slot {slotIndex}"; + view.ShowNotification(new GUIContent(message)); + Debug.Log($"[CameraBookmarks] {message}"); + } + + private static void LoadBookmark(SceneView view, string key, int slotIndex) + { + if (!EditorPrefs.HasKey(key)) + { + string emptyMsg = $"Slot {slotIndex} is empty!"; + view.ShowNotification(new GUIContent(emptyMsg)); + Debug.LogWarning($"[CameraBookmarks] {emptyMsg}"); + return; + } + + string[] parts = EditorPrefs.GetString(key).Split('|'); + + if (parts.Length == 8) + { + try + { + // Parse data back to floats using InvariantCulture + Vector3 pos = new Vector3( + float.Parse(parts[0], CultureInfo.InvariantCulture), + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture) + ); + + Quaternion rot = new Quaternion( + float.Parse(parts[3], CultureInfo.InvariantCulture), + float.Parse(parts[4], CultureInfo.InvariantCulture), + float.Parse(parts[5], CultureInfo.InvariantCulture), + float.Parse(parts[6], CultureInfo.InvariantCulture) + ); + + float size = float.Parse(parts[7], CultureInfo.InvariantCulture); + + // Apply the saved transform to the Scene View camera + view.LookAtDirect(pos, rot, size); + + // Show feedback in Scene View + string message = $"Loaded Camera Bookmark from Slot {slotIndex}"; + view.ShowNotification(new GUIContent(message)); + Debug.Log($"[CameraBookmarks] {message}"); + } + catch (Exception ex) + { + Debug.LogError($"[CameraBookmarks] Failed to parse data for Slot {slotIndex}. Error: {ex.Message}"); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Editor/CameraBookmarksTool.cs.meta b/Assets/Editor/CameraBookmarksTool.cs.meta new file mode 100644 index 00000000..8b8905ed --- /dev/null +++ b/Assets/Editor/CameraBookmarksTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5044058a94d2d014bb3bd8a1bd1e5707 \ No newline at end of file diff --git a/Assets/Editor/DistributeTool.cs b/Assets/Editor/DistributeTool.cs new file mode 100644 index 00000000..abf519cc --- /dev/null +++ b/Assets/Editor/DistributeTool.cs @@ -0,0 +1,131 @@ +// =============================================================================== +// DistributeTool - Professional Object Alignment & Distribution +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// This tool helps organize multiple objects by distributing them evenly along +// the X, Y, or Z axis. It's essential for creating fences, grids, or UI in 3D space. +// +// Key Features: +// 1. Distribute Between Bounds: Keeps the first and last object in place, fills the gap. +// 2. Fixed Spacing: Moves objects based on a specific numerical offset. +// 3. Smart Sorting: Automatically sorts objects by position before distributing. +// 4. Undo Support: Full integration with Unity's Undo system. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open via: Menu -> Tools -> Distribute Tool. +// 3. Select 3 or more objects in the Hierarchy/Scene. +// 4. Click the desired axis button (X, Y, or Z). +// =============================================================================== + +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + public class DistributeTool : EditorWindow + { + private float fixedSpacing = 1.0f; + private bool useFixedSpacing = false; + + [MenuItem("Tools/Distribute Tool")] + public static void ShowWindow() + { + GetWindow("Distribute"); + } + + private void OnGUI() + { + GUILayout.Label("Distribution Settings", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Mode Selection + useFixedSpacing = EditorGUILayout.Toggle("Use Fixed Spacing", useFixedSpacing); + + if (useFixedSpacing) + { + fixedSpacing = EditorGUILayout.FloatField("Distance Offset", fixedSpacing); + } + else + { + EditorGUILayout.HelpBox("Linear Mode: Objects will be distributed evenly between the first and last selected items.", MessageType.Info); + } + + EditorGUILayout.Space(); + GUILayout.Label("Distribute Along Axis:", EditorStyles.label); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("X Axis", GUILayout.Height(30))) Distribute(0); + if (GUILayout.Button("Y Axis", GUILayout.Height(30))) Distribute(1); + if (GUILayout.Button("Z Axis", GUILayout.Height(30))) Distribute(2); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Helpful Reminder + if (Selection.transforms.Length < 3) + { + EditorGUILayout.HelpBox("Please select at least 3 objects to distribute.", MessageType.Warning); + } + else + { + GUILayout.Label($"Objects Selected: {Selection.transforms.Length}", EditorStyles.miniLabel); + } + } + + private void Distribute(int axis) // 0=x, 1=y, 2=z + { + Transform[] selection = Selection.transforms; + + if (selection.Length < 3) + { + Debug.LogWarning("[DistributeTool] You need to select at least 3 objects."); + return; + } + + // Register Undo for all selected objects + Undo.RecordObjects(selection, "Distribute Objects"); + + // Sort selection by position on the chosen axis to maintain visual order + var sorted = selection.OrderBy(t => t.position[axis]).ToList(); + + if (useFixedSpacing) + { + // Fixed Spacing Logic: Move each object relative to the first one + Vector3 startPos = sorted[0].position; + for (int i = 1; i < sorted.Count; i++) + { + Vector3 newPos = sorted[i].position; + newPos[axis] = startPos[axis] + (fixedSpacing * i); + sorted[i].position = newPos; + } + } + else + { + // Linear Distribution Logic: Fill the space between first and last + float start = sorted.First().position[axis]; + float end = sorted.Last().position[axis]; + float totalDistance = end - start; + + // Avoid division by zero if objects are at the same spot + if (Mathf.Abs(totalDistance) < 0.0001f) return; + + float step = totalDistance / (sorted.Count - 1); + + for (int i = 0; i < sorted.Count; i++) + { + Vector3 newPos = sorted[i].position; + newPos[axis] = start + (step * i); + sorted[i].position = newPos; + } + } + + Debug.Log($"[DistributeTool] Distributed {sorted.Count} objects along the {(axis == 0 ? "X" : axis == 1 ? "Y" : "Z")} axis."); + } + } +} \ No newline at end of file diff --git a/Assets/Editor/DistributeTool.cs.meta b/Assets/Editor/DistributeTool.cs.meta new file mode 100644 index 00000000..1e05f5a0 --- /dev/null +++ b/Assets/Editor/DistributeTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 18664781a6d758c42a67a8d1c895c3dd \ No newline at end of file diff --git a/Assets/Editor/HierarchyEnhancer.cs b/Assets/Editor/HierarchyEnhancer.cs new file mode 100644 index 00000000..edde70ed --- /dev/null +++ b/Assets/Editor/HierarchyEnhancer.cs @@ -0,0 +1,81 @@ +// =============================================================================== +// HierarchyEnhancer - Quick Toggle for GameObjects +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// Adds a handy toggle checkbox to the right side of every item in the Hierarchy. +// This allows you to enable or disable GameObjects instantly without selecting them. +// +// Key Features: +// 1. One-click activation/deactivation directly in Hierarchy. +// 2. Full Undo/Redo support integrated with Unity's system. +// 3. Optimized UI placement to avoid overlapping with object names. +// 4. Visual clarity: Helps quickly identify inactive objects in a complex tree. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Look at your Hierarchy window; a small checkbox will appear on the far right. +// 3. Click the checkbox to toggle the Active/Inactive state of any GameObject. +// =============================================================================== + +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + [InitializeOnLoad] + public class HierarchyEnhancer + { + // Define the width of the toggle area + private const float TOGGLE_WIDTH = 16f; + + static HierarchyEnhancer() + { + // Subscribe to the hierarchy item GUI event + EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyItemGUI; + } + + private static void OnHierarchyItemGUI(int instanceID, Rect selectionRect) + { + // Get the GameObject associated with this instance ID + GameObject obj = EditorUtility.EntityIdToObject(instanceID) as GameObject; + if (obj == null) return; + + // Calculate the position for the Toggle (Aligned to the far right) + // selectionRect.xMax gives us the right boundary of the Hierarchy row + Rect toggleRect = new Rect(selectionRect); + toggleRect.x = selectionRect.xMax - TOGGLE_WIDTH; + toggleRect.width = TOGGLE_WIDTH; + + // Check current active state + bool isActive = obj.activeSelf; + + // Handle UI and changes + EditorGUI.BeginChangeCheck(); + + // Set the color based on active state (Optional polish) + Color originalColor = GUI.color; + if (!isActive) GUI.color = new Color(1f, 1f, 1f, 0.5f); // Dim the toggle if inactive + + bool newActive = EditorGUI.Toggle(toggleRect, isActive); + + GUI.color = originalColor; // Restore original color for other elements + + if (EditorGUI.EndChangeCheck()) + { + // Record undo before applying the change + Undo.RecordObject(obj, "Toggle GameObject Active State"); + obj.SetActive(newActive); + + // If it's a Prefab, mark the scene as dirty to ensure it saves + if (!Application.isPlaying) + { + EditorUtility.SetDirty(obj); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Editor/HierarchyEnhancer.cs.meta b/Assets/Editor/HierarchyEnhancer.cs.meta new file mode 100644 index 00000000..b4679694 --- /dev/null +++ b/Assets/Editor/HierarchyEnhancer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5a86ff34ff4b799498a6e0e250dfbd52 \ No newline at end of file diff --git a/Assets/Editor/HierarchySeparators.cs b/Assets/Editor/HierarchySeparators.cs new file mode 100644 index 00000000..8b231596 --- /dev/null +++ b/Assets/Editor/HierarchySeparators.cs @@ -0,0 +1,84 @@ +// =============================================================================== +// HierarchySeparators - Visual Organization for Unity Hierarchy +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// Converts GameObjects starting with "//" into visual separators or headers. +// This helps organize large scenes by creating clear, readable sections. +// +// Key Features: +// 1. Automatic formatting: "// player" becomes a bold, centered "PLAYER" header. +// 2. Custom background: Draws a distinctive bar to separate different logic groups. +// 3. Clean UI: Strips out the "//" prefix for a professional look in the editor. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Create an Empty GameObject in your Hierarchy. +// 3. Rename it starting with "//" (e.g., "// --- ENVIRONMENT ---"). +// =============================================================================== + +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + [InitializeOnLoad] + public class HierarchySeparators + { + // Custom styling colors + private static readonly Color HeaderBackgroundColor = new Color(0.22f, 0.22f, 0.22f, 1f); + private static readonly Color TextColor = new Color(0.9f, 0.9f, 0.9f, 1f); + private static readonly Color BorderColor = new Color(0.15f, 0.15f, 0.15f, 1f); + + static HierarchySeparators() + { + // Subscribe to the hierarchy item GUI event + EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyItemGUI; + } + + private static void OnHierarchyItemGUI(int instanceID, Rect selectionRect) + { + // Get the object from the instance ID + GameObject obj = EditorUtility.EntityIdToObject(instanceID) as GameObject; + + // Trigger only if the name starts with "//" + if (obj != null && obj.name.StartsWith("//")) + { + // 1. Draw Background + EditorGUI.DrawRect(selectionRect, HeaderBackgroundColor); + + // 2. Draw Subtle Bottom Border for better depth + Rect borderRect = new Rect(selectionRect.x, selectionRect.yMax - 1f, selectionRect.width, 1f); + EditorGUI.DrawRect(borderRect, BorderColor); + + // 3. Configure Text Style + GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) + { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = TextColor }, + fontSize = 11, + fontStyle = FontStyle.Bold + }; + + // 4. Clean and Format the string + // Removes "//", trims spaces, and converts to Uppercase + string headerName = obj.name.Replace("//", "").Trim().ToUpper(); + + // 5. Draw the Header Label + EditorGUI.LabelField(selectionRect, headerName, headerStyle); + + // Optional: To prevent selecting the separator as a normal object + // (keeps focus on actual game objects), uncomment the lines below: + + if (Event.current.type == EventType.MouseDown && selectionRect.Contains(Event.current.mousePosition)) + { + Selection.activeGameObject = null; + } + + } + } + } +} \ No newline at end of file diff --git a/Assets/Editor/HierarchySeparators.cs.meta b/Assets/Editor/HierarchySeparators.cs.meta new file mode 100644 index 00000000..7010f20f --- /dev/null +++ b/Assets/Editor/HierarchySeparators.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 840e668e5bda80441802a7b8ffef62f9 \ No newline at end of file diff --git a/Assets/Editor/LevelDecorator.cs b/Assets/Editor/LevelDecorator.cs new file mode 100644 index 00000000..db96f286 --- /dev/null +++ b/Assets/Editor/LevelDecorator.cs @@ -0,0 +1,189 @@ +// =============================================================================== +// LevelDecorator - Professional Environment Randomizer (Chaos Maker) +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// Quickly adds natural variety to your levels by randomizing the rotation +// and scale of selected objects. Perfect for placing foliage, rocks, or debris. +// +// Key Features: +// 1. Persistent Settings: Remembers your min/max values even after closing Unity. +// 2. Uniform Scale: Toggle between independent axes or proportional scaling. +// 3. Smart Rotation: Control specific Y-axis variance and subtle "tilt" separately. +// 4. Undo Integrated: One click to randomize, one click to undo (Ctrl+Z). +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open via: Menu -> Tools -> Level Decorator (Chaos Maker). +// 3. Select the objects you want to randomize in the Scene. +// 4. Adjust the sliders and click "Apply" or "Randomize Everything". +// =============================================================================== + +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + public class LevelDecorator : EditorWindow + { + // Settings Variables + private Vector3 minScale = new Vector3(0.9f, 0.9f, 0.9f); + private Vector3 maxScale = new Vector3(1.1f, 1.1f, 1.1f); + private float maxRotationY = 180f; + private float maxTilt = 5f; + private bool uniformScale = true; + + [MenuItem("Tools/Level Decorator (Chaos Maker)")] + public static void ShowWindow() + { + GetWindow("Chaos Maker"); + } + + private void OnEnable() + { + LoadSettings(); + } + + private void LoadSettings() + { + maxRotationY = EditorPrefs.GetFloat("LD_MaxRotY", 180f); + maxTilt = EditorPrefs.GetFloat("LD_MaxTilt", 5f); + uniformScale = EditorPrefs.GetBool("LD_Uniform", true); + + minScale.x = EditorPrefs.GetFloat("LD_MinScaleX", 0.9f); + minScale.y = EditorPrefs.GetFloat("LD_MinScaleY", 0.9f); + minScale.z = EditorPrefs.GetFloat("LD_MinScaleZ", 0.9f); + + maxScale.x = EditorPrefs.GetFloat("LD_MaxScaleX", 1.1f); + maxScale.y = EditorPrefs.GetFloat("LD_MaxScaleY", 1.1f); + maxScale.z = EditorPrefs.GetFloat("LD_MaxScaleZ", 1.1f); + } + + private void SaveSettings() + { + EditorPrefs.SetFloat("LD_MaxRotY", maxRotationY); + EditorPrefs.SetFloat("LD_MaxTilt", maxTilt); + EditorPrefs.SetBool("LD_Uniform", uniformScale); + EditorPrefs.SetFloat("LD_MinScaleX", minScale.x); + EditorPrefs.SetFloat("LD_MinScaleY", minScale.y); + EditorPrefs.SetFloat("LD_MinScaleZ", minScale.z); + EditorPrefs.SetFloat("LD_MaxScaleX", maxScale.x); + EditorPrefs.SetFloat("LD_MaxScaleY", maxScale.y); + EditorPrefs.SetFloat("LD_MaxScaleZ", maxScale.z); + } + + private void OnGUI() + { + GUILayout.Label("CHAOS MAKER - RANDOMIZER", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + + // --- ROTATION SECTION --- + EditorGUILayout.BeginVertical("box"); + GUILayout.Label("1. Rotation", EditorStyles.boldLabel); + maxRotationY = EditorGUILayout.Slider("Random Y Axis (0-360)", maxRotationY, 0, 360); + maxTilt = EditorGUILayout.Slider("Random Tilt (X & Z)", maxTilt, 0, 45); + + if (GUILayout.Button("Randomize Rotation")) + { + ApplyRotation(); + } + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + + // --- SCALE SECTION --- + EditorGUILayout.BeginVertical("box"); + GUILayout.Label("2. Scale", EditorStyles.boldLabel); + uniformScale = EditorGUILayout.Toggle("Uniform Scale", uniformScale); + + if (uniformScale) + { + float minU = minScale.x; + float maxU = maxScale.x; + minU = EditorGUILayout.FloatField("Min Scale", minU); + maxU = EditorGUILayout.FloatField("Max Scale", maxU); + minScale = new Vector3(minU, minU, minU); + maxScale = new Vector3(maxU, maxU, maxU); + } + else + { + minScale = EditorGUILayout.Vector3Field("Min Scale", minScale); + maxScale = EditorGUILayout.Vector3Field("Max Scale", maxScale); + } + + if (GUILayout.Button("Randomize Scale")) + { + ApplyScale(); + } + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // --- MASTER ACTION --- + GUI.backgroundColor = new Color(0.7f, 1f, 0.7f); // Light green button + if (GUILayout.Button("RANDOMIZE EVERYTHING", GUILayout.Height(40))) + { + ApplyRotation(); + ApplyScale(); + } + GUI.backgroundColor = Color.white; + + if (EditorGUI.EndChangeCheck()) + { + SaveSettings(); + } + + EditorGUILayout.Space(); + int selectCount = Selection.transforms.Length; + EditorGUILayout.HelpBox($"Objects Selected: {selectCount}\nSettings are automatically saved.", MessageType.None); + } + + private void ApplyRotation() + { + if (Selection.transforms.Length == 0) return; + + Undo.RecordObjects(Selection.transforms, "Chaos Rotation"); + + foreach (Transform t in Selection.transforms) + { + Vector3 currentRot = t.localEulerAngles; + + float randY = Random.Range(-maxRotationY, maxRotationY); + float randX = Random.Range(-maxTilt, maxTilt); + float randZ = Random.Range(-maxTilt, maxTilt); + + t.localEulerAngles = new Vector3(currentRot.x + randX, currentRot.y + randY, currentRot.z + randZ); + } + Debug.Log($"[LevelDecorator] Rotation randomized for {Selection.transforms.Length} objects."); + } + + private void ApplyScale() + { + if (Selection.transforms.Length == 0) return; + + Undo.RecordObjects(Selection.transforms, "Chaos Scale"); + + foreach (Transform t in Selection.transforms) + { + if (uniformScale) + { + float uniformRnd = Random.Range(minScale.x, maxScale.x); + t.localScale = new Vector3(uniformRnd, uniformRnd, uniformRnd); + } + else + { + float rX = Random.Range(minScale.x, maxScale.x); + float rY = Random.Range(minScale.y, maxScale.y); + float rZ = Random.Range(minScale.z, maxScale.z); + t.localScale = new Vector3(rX, rY, rZ); + } + } + Debug.Log($"[LevelDecorator] Scale randomized for {Selection.transforms.Length} objects."); + } + } +} \ No newline at end of file diff --git a/Assets/Editor/LevelDecorator.cs.meta b/Assets/Editor/LevelDecorator.cs.meta new file mode 100644 index 00000000..a521ac7b --- /dev/null +++ b/Assets/Editor/LevelDecorator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 85b906e8fc762834bab1190294ad2d15 \ No newline at end of file diff --git a/Assets/Editor/MeasureTool.cs b/Assets/Editor/MeasureTool.cs new file mode 100644 index 00000000..12c4e9cf --- /dev/null +++ b/Assets/Editor/MeasureTool.cs @@ -0,0 +1,149 @@ +// =============================================================================== +// MeasureTool - Smart Scene Measurement & Analysis +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 3.0 +// +// Purpose: +// A professional measurement tool that visualizes distances between objects +// in the Scene View. Supports single distance, chain distance, and axis breakdown. +// +// Key Features: +// 1. Two-Point & Chain Mode: Select 2 or more objects to measure. +// 2. Axis Breakdown: Shows Delta X, Y, Z with Unity-standard colors. +// 3. Scene Overlay: On-screen toggle and settings directly in Scene View. +// 4. Readable UI: High-contrast labels with background for any lighting condition. +// +// How to Use: +// 1. Place in an 'Editor' folder. +// 2. Select 2 or more GameObjects in the Hierarchy. +// 3. Use the "MEASURE" overlay in the Scene View to toggle axis details. +// =============================================================================== + +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; + +namespace Editor +{ + [InitializeOnLoad] + public class MeasureTool + { + // Settings (Persisted via EditorPrefs) + private static bool IsEnabled = true; + private static bool ShowAxisBreakdown = true; + private static bool ShowTotalDistance = true; + + static MeasureTool() + { + IsEnabled = EditorPrefs.GetBool("MeasureTool_Enabled", true); + ShowAxisBreakdown = EditorPrefs.GetBool("MeasureTool_Axis", true); + + SceneView.duringSceneGui += OnSceneGUI; + } + + private static void OnSceneGUI(SceneView view) + { + DrawOverlay(view); + + if (!IsEnabled || Selection.transforms.Length < 2) return; + + Transform[] selected = Selection.transforms; + + // Draw measurement for each pair in the selection + for (int i = 0; i < selected.Length - 1; i++) + { + DrawDistance(selected[i].position, selected[i + 1].position); + } + } + + private static void DrawDistance(Vector3 p1, Vector3 p2) + { + float distance = Vector3.Distance(p1, p2); + Vector3 midPoint = (p1 + p2) * 0.5f; + + // 1. Draw the Main Dotted Line + Handles.color = Color.cyan; + Handles.DrawDottedLine(p1, p2, 4f); + + // Draw small spheres at start/end points for clarity + Handles.SphereHandleCap(0, p1, Quaternion.identity, 0.1f, EventType.Repaint); + Handles.SphereHandleCap(0, p2, Quaternion.identity, 0.1f, EventType.Repaint); + + // 2. Draw Main Label (Total Distance) + if (ShowTotalDistance) + { + GUIStyle labelStyle = GetLabelStyle(Color.white, new Color(0, 0, 0, 0.6f)); + string labelText = $"Dist: {distance:F2}m"; + Handles.Label(midPoint + (Vector3.up * 0.1f), labelText, labelStyle); + } + + // 3. Draw Axis Breakdown (X, Y, Z Delta) + if (ShowAxisBreakdown) + { + Vector3 delta = new Vector3(Mathf.Abs(p1.x - p2.x), Mathf.Abs(p1.y - p2.y), Mathf.Abs(p1.z - p2.z)); + + // We only show axis delta if it's significant (> 0.01) + string axisText = ""; + if (delta.x > 0.01f) axisText += $"X: {delta.x:F2} "; + if (delta.y > 0.01f) axisText += $"Y: {delta.y:F2} "; + if (delta.z > 0.01f) axisText += $"Z: {delta.z:F2}"; + + if (!string.IsNullOrEmpty(axisText)) + { + GUIStyle axisStyle = GetLabelStyle(Color.white, new Color(0.1f, 0.1f, 0.1f, 0.8f)); + axisStyle.richText = true; + axisStyle.fontSize = 11; + Handles.Label(midPoint - (Vector3.up * 0.3f), axisText, axisStyle); + } + } + } + + private static void DrawOverlay(SceneView view) + { + Handles.BeginGUI(); + + // Positioning the overlay in the bottom right + float width = 140; + float height = 90; + Rect rect = new Rect(view.position.width - width - 10, view.position.height - height - 30, width, height); + + GUILayout.BeginArea(rect, "MEASURE TOOL", GUI.skin.window); + + EditorGUI.BeginChangeCheck(); + + IsEnabled = GUILayout.Toggle(IsEnabled, " Enable Tool"); + ShowTotalDistance = GUILayout.Toggle(ShowTotalDistance, " Show Total"); + ShowAxisBreakdown = GUILayout.Toggle(ShowAxisBreakdown, " Axis Breakdown"); + + if (EditorGUI.EndChangeCheck()) + { + EditorPrefs.SetBool("MeasureTool_Enabled", IsEnabled); + EditorPrefs.SetBool("MeasureTool_Axis", ShowAxisBreakdown); + SceneView.RepaintAll(); + } + + GUILayout.EndArea(); + Handles.EndGUI(); + } + + private static GUIStyle GetLabelStyle(Color textColor, Color bgColor) + { + GUIStyle style = new GUIStyle(); + style.normal.textColor = textColor; + style.fontSize = 13; + style.fontStyle = FontStyle.Bold; + style.alignment = TextAnchor.MiddleCenter; + style.padding = new RectOffset(4, 4, 2, 2); + + // Create a background texture dynamically + Texture2D tex = new Texture2D(1, 1); + tex.SetPixel(0, 0, bgColor); + tex.Apply(); + style.normal.background = tex; + + return style; + } + } +} \ No newline at end of file diff --git a/Assets/Editor/MeasureTool.cs.meta b/Assets/Editor/MeasureTool.cs.meta new file mode 100644 index 00000000..07c032a8 --- /dev/null +++ b/Assets/Editor/MeasureTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 38e38ba189f79f34eb81984f39aeefaf \ No newline at end of file diff --git a/Assets/Editor/PlayFromHereTool.cs b/Assets/Editor/PlayFromHereTool.cs new file mode 100644 index 00000000..fdce9f2c --- /dev/null +++ b/Assets/Editor/PlayFromHereTool.cs @@ -0,0 +1,215 @@ +// =============================================================================== +// PlayFromHereTool - Professional Instant Testing Workflow +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 3.0 +// +// Purpose: +// Speeds up level testing by instantly teleporting the Player to your current +// Scene View camera position. It allows you to teleport and optionally start +// Play Mode immediately with fully customizable hotkeys. +// +// Key Features: +// 1. Custom Hotkeys: User-definable modifiers and keys stored in EditorPrefs. +// 2. Smart Detection: Automatically finds the player using Selection -> Tag -> Name. +// 3. Two Modes: "Teleport Only" for setup, and "Play From Here" for testing. +// 4. UI Feedback: Displays notifications directly in the Scene View. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open Settings: Menu -> Tools -> Play From Here Settings. +// 3. Assign your preferred keys (e.g., Ctrl+Alt+P for Teleport). +// 4. In the Scene View, press your shortcut to move the player. +// =============================================================================== + +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + [InitializeOnLoad] + public class PlayFromHereTool : EditorWindow + { + // Customizable Shortcuts for Mode 1 (Teleport Only) + private static EventModifiers teleportModifier; + private static KeyCode teleportKey; + + // Customizable Shortcuts for Mode 2 (Teleport & Play) + private static EventModifiers playModifier; + private static KeyCode playKey; + + // Static constructor runs automatically when Unity loads or recompiles + static PlayFromHereTool() + { + LoadSettings(); + // Subscribe to SceneView GUI to listen for keyboard inputs + SceneView.duringSceneGui += OnSceneGUI; + } + + [MenuItem("Tools/Play From Here Settings")] + public static void ShowWindow() + { + GetWindow("Play From Here"); + } + + private static void LoadSettings() + { + // Load settings from EditorPrefs, fallback to default values + teleportModifier = (EventModifiers)EditorPrefs.GetInt("PFH_TeleportMod", (int)(EventModifiers.Control | EventModifiers.Alt)); + teleportKey = (KeyCode)EditorPrefs.GetInt("PFH_TeleportKey", (int)KeyCode.P); + + playModifier = (EventModifiers)EditorPrefs.GetInt("PFH_PlayMod", (int)(EventModifiers.Control | EventModifiers.Alt | EventModifiers.Shift)); + playKey = (KeyCode)EditorPrefs.GetInt("PFH_PlayKey", (int)KeyCode.P); + } + + private static void SaveSettings() + { + EditorPrefs.SetInt("PFH_TeleportMod", (int)teleportModifier); + EditorPrefs.SetInt("PFH_TeleportKey", (int)teleportKey); + EditorPrefs.SetInt("PFH_PlayMod", (int)playModifier); + EditorPrefs.SetInt("PFH_PlayKey", (int)playKey); + } + + private void OnGUI() + { + GUILayout.Label("Shortcut Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + + // Section for Mode 1 + EditorGUILayout.BeginVertical("box"); + GUILayout.Label("Mode 1: Teleport Only", EditorStyles.boldLabel); + teleportModifier = (EventModifiers)EditorGUILayout.EnumFlagsField("Modifier Keys", teleportModifier); + teleportKey = (KeyCode)EditorGUILayout.EnumPopup("Main Key", teleportKey); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + + // Section for Mode 2 + EditorGUILayout.BeginVertical("box"); + GUILayout.Label("Mode 2: Play From Here (Teleport + Play)", EditorStyles.boldLabel); + playModifier = (EventModifiers)EditorGUILayout.EnumFlagsField("Modifier Keys", playModifier); + playKey = (KeyCode)EditorGUILayout.EnumPopup("Main Key", playKey); + EditorGUILayout.EndVertical(); + + if (EditorGUI.EndChangeCheck()) + { + SaveSettings(); + } + + // Conflict warning + if (teleportModifier == playModifier && teleportKey == playKey) + { + EditorGUILayout.HelpBox("Conflict: Mode 1 and Mode 2 have the same hotkey!", MessageType.Error); + } + + EditorGUILayout.Space(); + EditorGUILayout.Space(); + + if (GUILayout.Button("Reset to Defaults", GUILayout.Height(30))) + { + teleportModifier = EventModifiers.Control | EventModifiers.Alt; + teleportKey = KeyCode.P; + playModifier = EventModifiers.Control | EventModifiers.Alt | EventModifiers.Shift; + playKey = KeyCode.P; + SaveSettings(); + GUI.FocusControl(null); + } + } + + private static void OnSceneGUI(SceneView view) + { + Event e = Event.current; + + // Only listen for key down events + if (e.type == EventType.KeyDown && e.keyCode != KeyCode.None) + { + // Clean modifiers (ignore CapsLock, etc.) + EventModifiers currentMods = e.modifiers & (EventModifiers.Shift | EventModifiers.Control | EventModifiers.Alt | EventModifiers.Command); + + // Try Mode 2 first (stricter modifiers) + if (e.keyCode == playKey && currentMods == playModifier) + { + ExecuteTeleport(true); + e.Use(); + } + // Try Mode 1 + else if (e.keyCode == teleportKey && currentMods == teleportModifier) + { + ExecuteTeleport(false); + e.Use(); + } + } + } + + private static void ExecuteTeleport(bool startPlayMode) + { + if (EditorApplication.isPlaying) return; + + GameObject player = FindPlayerObject(); + + if (player == null) + { + Debug.LogWarning("[PlayFromHere] Player not found! Tag your object 'Player', name it 'Player', or select it manually."); + return; + } + + if (SceneView.lastActiveSceneView == null || SceneView.lastActiveSceneView.camera == null) return; + + Camera sceneCam = SceneView.lastActiveSceneView.camera; + + // Record Undo + Undo.RecordObject(player.transform, "Teleport Player"); + + // Teleport + player.transform.position = sceneCam.transform.position; + + // Rotate Y axis (Yaw) to match camera + Vector3 camRot = sceneCam.transform.rotation.eulerAngles; + player.transform.rotation = Quaternion.Euler(0, camRot.y, 0); + + // Notify + string msg = startPlayMode ? "Teleported & Starting Play..." : "Player Teleported Here"; + SceneView.lastActiveSceneView.ShowNotification(new GUIContent($"{msg}\nTarget: {player.name}")); + Debug.Log($"[PlayFromHere] {msg}"); + + if (startPlayMode) + { + EditorApplication.isPlaying = true; + } + } + + private static GameObject FindPlayerObject() + { + // 1. Check current selection (The user knows best) + if (Selection.activeGameObject != null) return Selection.activeGameObject; + + // 2. Check Tag "Player" + try + { + GameObject tagPlayer = GameObject.FindGameObjectWithTag("Player"); + if (tagPlayer != null) return tagPlayer; + } + catch { } + + // 3. Check exact name "Player" + GameObject namePlayer = GameObject.Find("Player"); + if (namePlayer != null) return namePlayer; + + // 4. Search for objects containing common names + GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); + foreach (var obj in allObjects) + { + string n = obj.name.ToLower(); + if (n.Contains("player") || n.Contains("character") || n.Contains("controller")) + { + return obj; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/Assets/Editor/PlayFromHereTool.cs.meta b/Assets/Editor/PlayFromHereTool.cs.meta new file mode 100644 index 00000000..41c54621 --- /dev/null +++ b/Assets/Editor/PlayFromHereTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8d518311a665394bbaf16052dc48f1c \ No newline at end of file diff --git a/Assets/Editor/ProjectAudioPreview.cs b/Assets/Editor/ProjectAudioPreview.cs new file mode 100644 index 00000000..1a8123d8 --- /dev/null +++ b/Assets/Editor/ProjectAudioPreview.cs @@ -0,0 +1,163 @@ +// =============================================================================== +// ProjectAudioPreview - Instant Audio Preview in Project Window +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 2.0 +// +// Purpose: +// Allows users to quickly preview AudioClips directly from the Project window +// without selecting them or looking at the Inspector. +// +// Key Features: +// 1. Hover-to-Show: Play button only appears when hovering over an audio file. +// 2. Play/Stop Toggle: Single button to start and stop the preview. +// 3. High Performance: Cached reflection methods to prevent UI lag. +// 4. Integrated UI: Uses Unity's built-in editor icons for a native feel. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open the Project window in List View (Two-Column layout). +// 3. Hover over any AudioClip; a Play/Stop icon will appear on the right side. +// =============================================================================== + +using UnityEditor; +using UnityEngine; +using System; +using System.Reflection; + +namespace EditorTools +{ + [InitializeOnLoad] + public class ProjectAudioPreview + { + // Reflection Cache + private static MethodInfo playPreviewMethod; + private static MethodInfo stopAllPreviewMethod; + private static MethodInfo isPreviewPlayingMethod; + + private static AudioClip currentlyPlayingClip; + + static ProjectAudioPreview() + { + // Initialize Reflection once to save performance + InitReflection(); + + // Subscribe to project window item GUI + EditorApplication.projectWindowItemOnGUI += OnProjectWindowGUI; + } + + private static void InitReflection() + { + Assembly unityEditorAssembly = typeof(AudioImporter).Assembly; + Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil"); + + if (audioUtilClass != null) + { + playPreviewMethod = audioUtilClass.GetMethod("PlayPreviewClip", BindingFlags.Static | BindingFlags.Public); + stopAllPreviewMethod = audioUtilClass.GetMethod("StopAllPreviewClips", BindingFlags.Static | BindingFlags.Public); + isPreviewPlayingMethod = audioUtilClass.GetMethod("IsPreviewClipPlaying", BindingFlags.Static | BindingFlags.Public); + } + } + + static void OnProjectWindowGUI(string guid, Rect selectionRect) + { + // Optimization: Only process if the row is wide enough (List View) + if (selectionRect.width < 50) return; + + // Only show button if mouse is hovering over the current item + Event e = Event.current; + if (!selectionRect.Contains(e.mousePosition)) return; + + // Check if asset is an AudioClip + string path = AssetDatabase.GUIDToAssetPath(guid); + AudioType audioType = GetAudioType(path); + + if (audioType != AudioType.UNKNOWN) + { + DrawPreviewButton(selectionRect, path); + } + } + + private static void DrawPreviewButton(Rect rect, string path) + { + // Calculate button position (Right-aligned) + Rect btnRect = new Rect(rect.xMax - 25, rect.y, 20, rect.height); + + AudioClip clip = AssetDatabase.LoadAssetAtPath(path); + if (clip == null) return; + + bool isPlaying = IsClipPlaying(clip); + + // Choose icon based on state + // "d_PlayButton" and "d_PreMatQuad" are internal Unity icons + GUIContent icon = isPlaying + ? EditorGUIUtility.IconContent("d_PreMatQuad") + : EditorGUIUtility.IconContent("d_PlayButton"); + + // Styling the button + GUIStyle btnStyle = new GUIStyle(GUI.skin.button); + btnStyle.padding = new RectOffset(0, 0, 0, 0); + + // FIX: backgroundColor is a property of GUI, not GUIStyle + Color prevColor = GUI.backgroundColor; + GUI.backgroundColor = isPlaying ? Color.cyan : Color.white; + + if (GUI.Button(btnRect, icon, btnStyle)) + { + if (isPlaying) + { + StopAllClips(); + } + else + { + StopAllClips(); // Stop previous before playing new + PlayClip(clip); + } + } + + // Restore original background color + GUI.backgroundColor = prevColor; + } + + private static void PlayClip(AudioClip clip) + { + if (playPreviewMethod != null) + { + currentlyPlayingClip = clip; + playPreviewMethod.Invoke(null, new object[] { clip, 0, false }); + } + } + + private static void StopAllClips() + { + if (stopAllPreviewMethod != null) + { + stopAllPreviewMethod.Invoke(null, new object[] { }); + currentlyPlayingClip = null; + } + } + + private static bool IsClipPlaying(AudioClip clip) + { + if (isPreviewPlayingMethod != null && currentlyPlayingClip == clip) + { + return (bool)isPreviewPlayingMethod.Invoke(null, new object[] { }); + } + return false; + } + + private static AudioType GetAudioType(string path) + { + string ext = System.IO.Path.GetExtension(path).ToLower(); + switch (ext) + { + case ".mp3": return AudioType.MPEG; + case ".wav": return AudioType.WAV; + case ".ogg": return AudioType.OGGVORBIS; + case ".aiff": return AudioType.AIFF; + default: return AudioType.UNKNOWN; + } + } + } +} \ No newline at end of file diff --git a/Assets/Editor/ProjectAudioPreview.cs.meta b/Assets/Editor/ProjectAudioPreview.cs.meta new file mode 100644 index 00000000..2d1a0e0f --- /dev/null +++ b/Assets/Editor/ProjectAudioPreview.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 417cdb3b433688a419848d097f778a2b \ No newline at end of file diff --git a/Assets/Editor/ProjectDashboardTool.cs b/Assets/Editor/ProjectDashboardTool.cs new file mode 100644 index 00000000..a8958b8c --- /dev/null +++ b/Assets/Editor/ProjectDashboardTool.cs @@ -0,0 +1,226 @@ +// =============================================================================== +// ProjectDashboardTool - Central Control Panel for Unity Projects +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 2.0 +// +// Purpose: +// A centralized dashboard to quickly navigate between scenes, start the game +// from the initialization (Boot) scene, and manage save data / PlayerPrefs. +// +// Key Features: +// 1. Dynamic Scene List: Automatically fetches scenes from Build Settings. +// 2. 1-Click Play: Instantly loads the Boot scene and enters Play Mode. +// 3. Data Management: Clear PlayerPrefs, delete save files, or open the Save folder. +// 4. Color-Coded UI: Prevents accidental data deletion with clear visual warnings. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open via: Menu -> Tools -> Project Dashboard. +// 3. Add your scenes to File -> Build Settings to see them in the list. +// =============================================================================== + +using UnityEditor; +using UnityEngine; +using UnityEditor.SceneManagement; +using System.IO; + +namespace Editor +{ + public class ProjectDashboardTool : EditorWindow + { + private Vector2 sceneScrollPos; + + [MenuItem("Tools/Project Dashboard")] + public static void ShowWindow() + { + // Create a window with a minimum size + ProjectDashboardTool window = GetWindow("Dashboard"); + window.minSize = new Vector2(300, 450); + } + + private void OnGUI() + { + EditorGUILayout.Space(); + + DrawPlaySection(); + EditorGUILayout.Space(15); + + DrawSceneNavigation(); + EditorGUILayout.Space(15); + + DrawDataManagement(); + } + + private void DrawPlaySection() + { + GUILayout.Label("QUICK PLAY", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical("box"); + + // Green Play Button + Color oldColor = GUI.backgroundColor; + GUI.backgroundColor = new Color(0.4f, 1f, 0.4f); // Light Green + + if (GUILayout.Button("▶ PLAY GAME (From Boot Scene)", GUILayout.Height(40))) + { + PlayFromBootScene(); + } + + GUI.backgroundColor = oldColor; + + EditorGUILayout.HelpBox("Automatically saves your current scene, loads the first scene in Build Settings, and presses Play.", MessageType.Info); + EditorGUILayout.EndVertical(); + } + + private void DrawSceneNavigation() + { + GUILayout.Label("SCENE NAVIGATION (Build Settings)", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical("box"); + + // Fetch scenes dynamically from Build Settings + EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes; + + if (scenes.Length == 0) + { + EditorGUILayout.HelpBox("No scenes found in Build Settings! Please go to File -> Build Settings and add your scenes.", MessageType.Warning); + } + else + { + sceneScrollPos = EditorGUILayout.BeginScrollView(sceneScrollPos, GUILayout.MaxHeight(200)); + + for (int i = 0; i < scenes.Length; i++) + { + if (scenes[i].enabled) + { + string scenePath = scenes[i].path; + string sceneName = Path.GetFileNameWithoutExtension(scenePath); + + EditorGUILayout.BeginHorizontal(); + GUILayout.Label($"[{i}]", GUILayout.Width(25)); + + if (GUILayout.Button($"Load {sceneName}", GUILayout.Height(25))) + { + OpenScene(scenePath); + } + EditorGUILayout.EndHorizontal(); + } + } + + EditorGUILayout.EndScrollView(); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawDataManagement() + { + GUILayout.Label("DATA MANAGEMENT", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical("box"); + + if (GUILayout.Button("Open Save Folder (Explorer/Finder)", GUILayout.Height(30))) + { + EditorUtility.RevealInFinder(Application.persistentDataPath); + } + + EditorGUILayout.Space(5); + + // Red Delete Button + Color oldColor = GUI.backgroundColor; + GUI.backgroundColor = new Color(1f, 0.4f, 0.4f); // Light Red + + if (GUILayout.Button("⚠ Clear PlayerPrefs & Save Data", GUILayout.Height(35))) + { + if (EditorUtility.DisplayDialog( + "Clear All Data?", + "Are you sure you want to delete all PlayerPrefs and JSON save files?\nThis action cannot be undone.", + "Yes, Delete Everything", + "Cancel")) + { + ClearAllData(); + } + } + + GUI.backgroundColor = oldColor; + EditorGUILayout.EndVertical(); + } + + private void PlayFromBootScene() + { + EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes; + if (scenes.Length == 0) + { + Debug.LogError("[Dashboard] Cannot Play: No scenes in Build Settings."); + return; + } + + // Stop playing if currently playing + if (EditorApplication.isPlaying) + { + EditorApplication.isPlaying = false; + return; + } + + // Save current scene and load Scene 0 + if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) + { + EditorSceneManager.OpenScene(scenes[0].path); + EditorApplication.isPlaying = true; + } + } + + private void OpenScene(string path) + { + if (EditorApplication.isPlaying) + { + Debug.LogWarning("[Dashboard] Cannot load scene while in Play Mode."); + return; + } + + if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) + { + EditorSceneManager.OpenScene(path); + Debug.Log($"[Dashboard] Loaded scene: {Path.GetFileNameWithoutExtension(path)}"); + } + } + + private void ClearAllData() + { + // 1. Clear Unity PlayerPrefs + PlayerPrefs.DeleteAll(); + PlayerPrefs.Save(); + + // 2. Clear common save files in persistentDataPath + string persistentPath = Application.persistentDataPath; + string[] filesToDelete = new string[] + { + "save_data.json", + "player_data.dat", + "settings.json" + }; + + int deletedCount = 0; + + foreach (string file in filesToDelete) + { + string filePath = Path.Combine(persistentPath, file); + if (File.Exists(filePath)) + { + File.Delete(filePath); + deletedCount++; + } + } + + // Alternative: Delete ALL .json files in the folder (Uncomment if needed) + /* + string[] allJsonFiles = Directory.GetFiles(persistentPath, "*.json"); + foreach (string file in allJsonFiles) { File.Delete(file); } + */ + + Debug.Log($"[Dashboard] Cleared PlayerPrefs and {deletedCount} save file(s)."); + + // Show notification on the Editor Window + this.ShowNotification(new GUIContent("Data Cleared Successfully!")); + } + } +} \ No newline at end of file diff --git a/Assets/Editor/ProjectDashboardTool.cs.meta b/Assets/Editor/ProjectDashboardTool.cs.meta new file mode 100644 index 00000000..82dd20c0 --- /dev/null +++ b/Assets/Editor/ProjectDashboardTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 382b2c71505c85d488edc73f585e957a \ No newline at end of file diff --git a/Assets/Editor/ProjectNavigation.cs b/Assets/Editor/ProjectNavigation.cs new file mode 100644 index 00000000..92843324 --- /dev/null +++ b/Assets/Editor/ProjectNavigation.cs @@ -0,0 +1,95 @@ +// =============================================================================== +// ProjectNavigation - Navigation History for Unity Project Window +// +// Creator: Scove +// Last Updated: 2026-03-05 +// Version: 1.1 +// +// Purpose: +// This tool provides a navigation history for the Unity Project window, allowing +// users to quickly jump back and forward between previously visited folders. +// +// Key Features: +// 1. Tracks folder selection history automatically. +// 2. Supports Back and Forward navigation via menu items and shortcuts. +// 3. Pings the selected folder for quick visual identification. +// +// How to Use: +// 1. Place this script in an 'Editor' folder in your project. +// 2. Use Alt + Left Arrow to navigate Back. +// 3. Use Alt + Right Arrow to navigate Forward. +// =============================================================================== + +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + public class ProjectNavigation : EditorWindow + { + private static List history = new List(); + private static int currentIndex = -1; + private static string lastPath = ""; + + // Store the current position when selecting a folder + [InitializeOnLoadMethod] + static void Init() + { + Selection.selectionChanged += OnSelectionChanged; + } + + static void OnSelectionChanged() + { + if (Selection.activeObject != null) + { + string path = AssetDatabase.GetAssetPath(Selection.activeObject); + + // Only track if it's a valid folder and different from the last recorded path + if (AssetDatabase.IsValidFolder(path) && path != lastPath) + { + // If we are in the middle of history and perform a new "direct jump", + // clear the forward history (browser-style navigation) + if (currentIndex < history.Count - 1) + { + history.RemoveRange(currentIndex + 1, history.Count - (currentIndex + 1)); + } + + history.Add(path); + currentIndex++; + lastPath = path; + } + } + } + + // Shortcut Alt + Left Arrow (Back) + [MenuItem("Tools/Navigation/Back &LEFT")] + static void Back() + { + if (currentIndex > 0) + { + currentIndex--; + NavigateTo(history[currentIndex]); + } + } + + // Shortcut Alt + Right Arrow (Forward) + [MenuItem("Tools/Navigation/Forward &RIGHT")] + static void Forward() + { + if (currentIndex < history.Count - 1) + { + currentIndex++; + NavigateTo(history[currentIndex]); + } + } + + static void NavigateTo(string path) + { + lastPath = path; + Object obj = AssetDatabase.LoadAssetAtPath(path); + Selection.activeObject = obj; + EditorGUIUtility.PingObject(obj); + } + } +} diff --git a/Assets/Editor/ProjectNavigation.cs.meta b/Assets/Editor/ProjectNavigation.cs.meta new file mode 100644 index 00000000..3eca9bcd --- /dev/null +++ b/Assets/Editor/ProjectNavigation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 09e4547a2316bd340b1e7443f68858b9 \ No newline at end of file diff --git a/Assets/Editor/ReferenceFinderTool.cs b/Assets/Editor/ReferenceFinderTool.cs new file mode 100644 index 00000000..530a656b --- /dev/null +++ b/Assets/Editor/ReferenceFinderTool.cs @@ -0,0 +1,161 @@ +// =============================================================================== +// ReferenceFinderTool - Deep Dependency Analysis for Unity Assets +// +// Creator: Scove +// Last Updated: 2024-05-08 +// Version: 2.0 +// +// Purpose: +// Finds every asset in the project that references the selected item by +// scanning GUIDs inside Unity's YAML-based files (Scenes, Prefabs, Materials, etc.). +// +// Key Features: +// 1. Interactive Result List: Click any result to highlight the asset in Project window. +// 2. Wide Scope: Scans all text-based assets (Mat, PhysMat, Controller, Asset, etc.). +// 3. Optimized Search: Faster file reading and progress bar with Cancel support. +// 4. Clean UI: Integrated as a professional Editor Window. +// +// How to Use: +// 1. Right-click any asset in the Project window. +// 2. Select "Find References (Deep Scan)". +// 3. View the results in the pop-up window. +// =============================================================================== + +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; +using System.IO; + +namespace Editor +{ + public class ReferenceFinderTool : EditorWindow + { + private static List foundPaths = new List(); + private static string targetAssetName = ""; + private static string targetAssetGUID = ""; + private Vector2 scrollPosition; + + [MenuItem("Assets/Find References (Deep Scan)", false, 25)] + public static void FindReferences() + { + Object selected = Selection.activeObject; + if (selected == null) return; + + string path = AssetDatabase.GetAssetPath(selected); + targetAssetGUID = AssetDatabase.AssetPathToGUID(path); + targetAssetName = selected.name; + + if (string.IsNullOrEmpty(targetAssetGUID)) return; + + PerformDeepScan(); + + // Open the results window + ReferenceFinderTool window = GetWindow("Reference Finder"); + window.minSize = new Vector2(400, 300); + window.Show(); + } + + private static void PerformDeepScan() + { + foundPaths.Clear(); + + // We look for all assets that are usually saved in Text/YAML format + // Added: Materials, Animators, ScriptableObjects, etc. + string[] allGuids = AssetDatabase.FindAssets("t:Prefab t:Scene t:Material t:PhysicMaterial t:RuntimeAnimatorController t:ScriptableObject t:AnimatorOverrideController"); + + int total = allGuids.Length; + + for (int i = 0; i < total; i++) + { + string assetPath = AssetDatabase.GUIDToAssetPath(allGuids[i]); + + // Update Progress Bar + bool isCanceled = EditorUtility.DisplayCancelableProgressBar( + "Searching References", + $"Scanning: {Path.GetFileName(assetPath)}", + (float)i / total); + + if (isCanceled) break; + + // Check if the asset file contains the Target GUID + if (FileContainsGUID(assetPath, targetAssetGUID)) + { + foundPaths.Add(assetPath); + } + } + + EditorUtility.ClearProgressBar(); + } + + private static bool FileContainsGUID(string path, string guid) + { + if (!File.Exists(path)) return false; + + // Using StreamReader is faster than File.ReadAllText for very large files + using (StreamReader reader = new StreamReader(path)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (line.Contains(guid)) return true; + } + } + return false; + } + + private void OnGUI() + { + GUILayout.Label($"References for: {targetAssetName}", EditorStyles.boldLabel); + EditorGUILayout.LabelField($"GUID: {targetAssetGUID}", EditorStyles.miniLabel); + EditorGUILayout.Space(); + + if (foundPaths.Count == 0) + { + EditorGUILayout.HelpBox("No references found. Note: Ensure 'Asset Serialization' is set to 'Force Text' in Project Settings.", MessageType.Info); + } + else + { + GUILayout.Label($"Found {foundPaths.Count} items:", EditorStyles.label); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + for (int i = 0; i < foundPaths.Count; i++) + { + DrawResultItem(foundPaths[i]); + } + + EditorGUILayout.EndScrollView(); + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Rescan", GUILayout.Height(30))) + { + PerformDeepScan(); + } + } + + private void DrawResultItem(string path) + { + EditorGUILayout.BeginHorizontal("box"); + + // Get the asset icon + Texture icon = AssetDatabase.GetCachedIcon(path); + GUILayout.Label(icon, GUILayout.Width(16), GUILayout.Height(16)); + + // Display path + GUILayout.Label(path, EditorStyles.wordWrappedLabel); + + GUILayout.FlexibleSpace(); + + // Ping Button + if (GUILayout.Button("Ping", GUILayout.Width(50))) + { + Object obj = AssetDatabase.LoadAssetAtPath(path); + EditorGUIUtility.PingObject(obj); + Selection.activeObject = obj; + } + + EditorGUILayout.EndHorizontal(); + } + } +} \ No newline at end of file diff --git a/Assets/Editor/ReferenceFinderTool.cs.meta b/Assets/Editor/ReferenceFinderTool.cs.meta new file mode 100644 index 00000000..d7cb3af4 --- /dev/null +++ b/Assets/Editor/ReferenceFinderTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e4aa815f17d76a34e8063c78c9821e5e \ No newline at end of file diff --git a/Assets/Editor/SmartBootstrapper.cs b/Assets/Editor/SmartBootstrapper.cs new file mode 100644 index 00000000..2a9b98c2 --- /dev/null +++ b/Assets/Editor/SmartBootstrapper.cs @@ -0,0 +1,158 @@ +// =============================================================================== +// SmartBootstrapper - Auto-Boot Scene Loader for Unity +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 3.0 (Drag & Drop UI Added) +// +// Purpose: +// Forces the Unity Editor to always start from an initialization (Boot) scene +// when hitting Play, regardless of which scene is currently open. +// +// Key Features: +// 1. Drag & Drop UI: Easily assign your Boot Scene via a settings window. +// 2. Persistent Settings: Saves your configuration automatically via EditorPrefs. +// 3. Quick Toggle: Enable or disable the feature directly from the window. +// 4. Smart Play Mode: Gracefully returns to your working scene after testing. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Open via: Menu -> Tools -> Smart Boot Settings. +// 3. Drag and drop your Boot Scene into the slot and enable the tool. +// =============================================================================== + +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace Editor +{[InitializeOnLoad] + public class SmartBootstrapper : EditorWindow + { + // EditorPrefs Keys + private const string PREFS_TOGGLE_KEY = "SmartBoot_Enabled"; + private const string PREFS_PATH_KEY = "SmartBoot_ScenePath"; + + private static bool IsEnabled + { + get => EditorPrefs.GetBool(PREFS_TOGGLE_KEY, false); + set => EditorPrefs.SetBool(PREFS_TOGGLE_KEY, value); + } + + private static string BootScenePath + { + get => EditorPrefs.GetString(PREFS_PATH_KEY, ""); + set => EditorPrefs.SetString(PREFS_PATH_KEY, value); + }[MenuItem("Tools/Smart Boot Settings")] + public static void ShowWindow() + { + SmartBootstrapper window = GetWindow("Smart Boot"); + window.minSize = new Vector2(350, 150); + window.maxSize = new Vector2(500, 160); + window.Show(); + } + + private void OnGUI() + { + GUILayout.Space(10); + EditorGUILayout.LabelField("Boot Scene Configuration", EditorStyles.boldLabel); + GUILayout.Space(5); + + SceneAsset currentScene = null; + if (!string.IsNullOrEmpty(BootScenePath)) + { + currentScene = AssetDatabase.LoadAssetAtPath(BootScenePath); + } + + EditorGUI.BeginChangeCheck(); + SceneAsset draggedScene = (SceneAsset)EditorGUILayout.ObjectField( + "Boot Scene", + currentScene, + typeof(SceneAsset), + false + ); + + // IF THE USER DRAGS AND DROPS A NEW SCENE + if (EditorGUI.EndChangeCheck()) + { + if (draggedScene == null) + { + BootScenePath = ""; + EditorSceneManager.playModeStartScene = null; // Clear immediately + } + else + { + BootScenePath = AssetDatabase.GetAssetPath(draggedScene); + EditorSceneManager.playModeStartScene = draggedScene; // Update immediately + } + } + + GUILayout.Space(10); + + EditorGUI.BeginChangeCheck(); + bool newToggleState = EditorGUILayout.Toggle("Enable Auto-Boot", IsEnabled); + + // IF THE USER TOGGLES THE SWITCH + if (EditorGUI.EndChangeCheck()) + { + IsEnabled = newToggleState; + if (!IsEnabled) + EditorSceneManager.playModeStartScene = null; // Clear cache immediately when disabled + } + + GUILayout.Space(15); + if (string.IsNullOrEmpty(BootScenePath)) + { + EditorGUILayout.HelpBox("Please drag and drop a Boot Scene into the slot above.", MessageType.Warning); + } + else if (IsEnabled) + { + EditorGUILayout.HelpBox($"Ready! Hitting PLAY will always start from:\n{Path.GetFileName(BootScenePath)}", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("Auto-Boot is currently disabled. The active scene will play normally.", MessageType.None); + } + } + + static SmartBootstrapper() + { + // Avoid registering the event multiple times on recompile + EditorApplication.playModeStateChanged -= OnPlayModeChanged; + EditorApplication.playModeStateChanged += OnPlayModeChanged; + } + + private static void OnPlayModeChanged(PlayModeStateChange state) + { + if (state != PlayModeStateChange.ExitingEditMode) return; + + // 1. Feature disabled or no scene assigned + if (!IsEnabled || string.IsNullOrEmpty(BootScenePath)) + { + EditorSceneManager.playModeStartScene = null; + return; + } + + // 2. Find Scene Asset + SceneAsset bootScene = AssetDatabase.LoadAssetAtPath(BootScenePath); + if (bootScene == null) + { + Debug.LogWarning($"[SmartBoot] Scene not found at saved path: {BootScenePath}. Please re-assign it."); + EditorSceneManager.playModeStartScene = null; + return; + } + + // 3. ALWAYS override with the Boot Scene (fixes old Scene sticking) + EditorSceneManager.playModeStartScene = bootScene; + + // 4. Only show Log if the open Scene is not the Boot Scene + string activeScenePath = EditorSceneManager.GetActiveScene().path; + if (activeScenePath != BootScenePath) + { + string currentSceneName = Path.GetFileNameWithoutExtension(activeScenePath); + Debug.Log($"[SmartBoot] Starting from Boot Scene... (Will return context to {currentSceneName})"); + } + } + } +} \ No newline at end of file diff --git a/Assets/Editor/SmartBootstrapper.cs.meta b/Assets/Editor/SmartBootstrapper.cs.meta new file mode 100644 index 00000000..9f073c0f --- /dev/null +++ b/Assets/Editor/SmartBootstrapper.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: db3e1e6db7211d04cb2f994b539535be \ No newline at end of file diff --git a/Assets/Editor/StickyNoteEditor.cs b/Assets/Editor/StickyNoteEditor.cs new file mode 100644 index 00000000..e9c560f1 --- /dev/null +++ b/Assets/Editor/StickyNoteEditor.cs @@ -0,0 +1,151 @@ +// =============================================================================== +// StickyNoteEditor - Custom Inspector & Scene Gizmo for U_StickyNote +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 2.0 (Ergonomic UI & Scene Rendering) +// +// Purpose: +// Enhances the Unity Inspector and Scene View for the U_StickyNote component. +// Makes level designing and leaving notes in the scene intuitive, readable, +// and visually appealing. +// +// Key Features: +// 1. Beautiful Scene Gizmos: Renders a colored background pad with +// auto-contrasting text (black/white) for readability in any environment. +// 2. Pin-Line Indicator: Draws a visual pin connecting the floating note +// to its actual GameObject position. +// 3. Ergonomic Inspector: Large text area for multi-line notes instead of +// a cramped default single-line text field. +// 4. Quick Color Presets: 1-click pastel color buttons (Yellow, Blue, Green, +// Pink, White) for rapid and aesthetic note tagging. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Attach your 'StickyNote' script to any GameObject in the scene. +// 3. Select the GameObject to see the upgraded Inspector and Scene View note! +// =============================================================================== + +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + + +namespace Editor +{ + [CustomEditor(typeof(global::StickyNote))] + public class StickyNoteEditor : UnityEditor.Editor + { + private SerializedProperty noteText; + private SerializedProperty noteColor; + private SerializedProperty showAlways; + + // Cache background texture for performance optimization + private static Dictionary backgroundCache = new Dictionary(); + + private void OnEnable() + { + // Link properties from the original script + noteText = serializedObject.FindProperty("noteText"); + noteColor = serializedObject.FindProperty("noteColor"); + showAlways = serializedObject.FindProperty("showAlways"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // 1. Stylish Header + EditorGUILayout.Space(5); + GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }; + EditorGUILayout.LabelField("📝 STICKY NOTE PANEL", headerStyle); + EditorGUILayout.Space(5); + + // 2. Large & Convenient Text Input Area + EditorGUILayout.LabelField("Note Content:", EditorStyles.boldLabel); + GUIStyle textAreaStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true, fontSize = 12, padding = new RectOffset(8, 8, 8, 8) }; + noteText.stringValue = EditorGUILayout.TextArea(noteText.stringValue, textAreaStyle, GUILayout.Height(80)); + EditorGUILayout.Space(5); + + // 3. Quick Color Selection Buttons (Preset Colors - highly convenient) + EditorGUILayout.LabelField("Quick Colors:", EditorStyles.boldLabel); + GUILayout.BeginHorizontal(); + DrawColorPresetButton("Yellow", new Color(1f, 0.92f, 0.53f)); // Pastel Yellow + DrawColorPresetButton("Blue", new Color(0.68f, 0.85f, 0.9f)); // Pastel Blue + DrawColorPresetButton("Green", new Color(0.67f, 0.88f, 0.69f)); // Pastel Green + DrawColorPresetButton("Pink", new Color(1f, 0.71f, 0.76f)); // Pastel Pink + DrawColorPresetButton("White", new Color(0.95f, 0.95f, 0.95f)); // Off-white + GUILayout.EndHorizontal(); + + // Custom color picker + EditorGUILayout.PropertyField(noteColor, new GUIContent("Custom Color")); + EditorGUILayout.Space(5); + + // 4. Toggle Settings + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.PropertyField(showAlways, new GUIContent("📌 Show Always in Scene")); + EditorGUILayout.EndVertical(); + serializedObject.ApplyModifiedProperties(); + } + + // Function to create a colored button + private void DrawColorPresetButton(string label, Color color) + { + Color oldColor = GUI.backgroundColor; + GUI.backgroundColor = color; + if (GUILayout.Button(label, GUILayout.Height(25))) + { + noteColor.colorValue = color; + } + GUI.backgroundColor = oldColor; + } + + // ========================================================================= + // SCENE VIEW RENDERING - VISUAL NOTE INTERFACE + // ========================================================================= + [DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.Active)] + static void DrawGizmo(StickyNote note, GizmoType gizmoType) + { + if (!note.showAlways && (gizmoType & GizmoType.Selected) == 0) return; + if (string.IsNullOrEmpty(note.noteText)) return; + + Vector3 basePos = note.transform.position; + Vector3 labelPos = basePos + Vector3.up * 1.2f; // Height of the note board + + // 1. Draw the "Pin" and connecting line + Gizmos.color = note.noteColor; + Gizmos.DrawLine(basePos, labelPos); + Gizmos.DrawSphere(basePos, 0.1f); // Pin base + Gizmos.DrawSphere(labelPos, 0.05f); // Pin top + + // 2. Initialize Style for the Note Board + GUIStyle noteStyle = new GUIStyle(GUI.skin.label); + noteStyle.normal.background = GetBackgroundTexture(note.noteColor); + + // Calculate text color for readability (If dark background -> white text, light background -> black text) + float luminance = (0.299f * note.noteColor.r) + (0.587f * note.noteColor.g) + (0.114f * note.noteColor.b); + noteStyle.normal.textColor = luminance > 0.6f ? Color.black : Color.white; + noteStyle.fontSize = 13; + noteStyle.fontStyle = FontStyle.Bold; + noteStyle.alignment = TextAnchor.MiddleCenter; + noteStyle.padding = new RectOffset(10, 10, 8, 8); // Text distance to border (Padding) + + // 3. Draw Label with sharp background and padding + Handles.Label(labelPos + Vector3.up * 0.2f, note.noteText, noteStyle); + } + // Function to automatically create colored background Texture (Cache to prevent lag) + private static Texture2D GetBackgroundTexture(Color color) + { + // Make the color slightly transparent for a more professional look + Color bgColor = new Color(color.r, color.g, color.b, 0.9f); + if (!backgroundCache.TryGetValue(bgColor, out Texture2D tex) || tex == null) + { + tex = new Texture2D(1, 1); + tex.SetPixel(0, 0, bgColor); + tex.Apply(); + backgroundCache[bgColor] = tex; + } + return tex; + } + } +} \ No newline at end of file diff --git a/Assets/Editor/StickyNoteEditor.cs.meta b/Assets/Editor/StickyNoteEditor.cs.meta new file mode 100644 index 00000000..8c31d107 --- /dev/null +++ b/Assets/Editor/StickyNoteEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be3283bc34f51f742aefd7d713f5c8fa \ No newline at end of file diff --git a/Assets/Editor/TimeLord.cs b/Assets/Editor/TimeLord.cs new file mode 100644 index 00000000..1d1901a2 --- /dev/null +++ b/Assets/Editor/TimeLord.cs @@ -0,0 +1,146 @@ +// =============================================================================== +// TimeLord - In-Scene Time Manipulation Tool +// +// Creator: Scove +// Last Updated: 2026-03-03 +// Version: 2.0 (Sleek UI & Ergonomic Controls) +// +// Purpose: +// Provides a floating, interactive control panel inside the Scene View during +// Play Mode. Allows developers to easily manipulate game time (slow motion, +// fast forward, or pause) without constantly looking away from the action. +// +// Key Features: +// 1. Floating Dashboard: Clean, unobtrusive UI placed directly in the Scene View. +// 2. Dynamic Slider: Drag to fine-tune the time scale smoothly from 0x to 10x. +// 3. Smart Highlighting: Active speeds light up (Green), making it easy to read. +// 4. Auto-Resume: Clicking a speed preset while paused automatically resumes play. +// 5. Visual Pause State: Prominent pause button changes color when active. +// +// How to Use: +// 1. Place this script in an 'Editor' folder. +// 2. Hit PLAY in Unity. +// 3. Move your mouse to the Scene View and use the top-center Time Lord panel! +// =============================================================================== + +using UnityEditor; +using UnityEngine; + +namespace Editor +{ + [InitializeOnLoad] + public class TimeLord + { + // Panel configuration + private const float PANEL_WIDTH = 340f; + private const float PANEL_HEIGHT = 105f; + + static TimeLord() + { + // Unsubscribe first to prevent double-hooking upon script recompile + SceneView.duringSceneGui -= OnSceneGUI; + SceneView.duringSceneGui += OnSceneGUI; + } + + private static void OnSceneGUI(SceneView sceneView) + { + // Only display the panel when the game is actually running + if (!Application.isPlaying) return; + + Handles.BeginGUI(); + + // Calculate center-top position dynamically based on current Scene View size + float posX = (sceneView.position.width - PANEL_WIDTH) / 2f; + float posY = 15f; + Rect panelRect = new Rect(posX, posY, PANEL_WIDTH, PANEL_HEIGHT); + + // Draw the main background box + GUI.Box(panelRect, GUIContent.none, EditorStyles.helpBox); + + GUILayout.BeginArea(new Rect(posX + 10, posY + 10, PANEL_WIDTH - 20, PANEL_HEIGHT - 20)); + + // --- 1. HEADER (Title & Current Speed) --- + GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) + { + richText = true, + alignment = TextAnchor.MiddleCenter, + fontSize = 13 + }; + + // Format the time scale to show 2 decimal places + string currentSpeedText = EditorApplication.isPaused ? "PAUSED" : $"{Time.timeScale:F2}x"; + GUILayout.Label($"⏳ TIME LORD | Current: {currentSpeedText}", headerStyle); + GUILayout.Space(5); + + // --- 2. TIME SLIDER (Fine-tune control) --- + EditorGUI.BeginChangeCheck(); + float newTimeScale = GUILayout.HorizontalSlider(Time.timeScale, 0f, 10f); + if (EditorGUI.EndChangeCheck()) + { + Time.timeScale = newTimeScale; + // Auto-resume if adjusting slider while paused + if (EditorApplication.isPaused && newTimeScale > 0f) + { + EditorApplication.isPaused = false; + } + } + GUILayout.Space(5); + + // --- 3. PRESET SPEED BUTTONS --- + GUILayout.BeginHorizontal(); + DrawSpeedButton("0.1x", 0.1f); + DrawSpeedButton("0.5x", 0.5f); + DrawSpeedButton("1x", 1f); + DrawSpeedButton("2x", 2f); + DrawSpeedButton("5x", 5f); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + // --- 4. PAUSE / RESUME BUTTON --- + Color oldBgColor = GUI.backgroundColor; + GUI.backgroundColor = EditorApplication.isPaused ? new Color(1f, 0.4f, 0.4f) : new Color(0.9f, 0.9f, 0.9f); + + string pauseLabel = EditorApplication.isPaused ? "▶ RESUME GAME" : "⏸ PAUSE GAME"; + GUIStyle pauseStyle = new GUIStyle(GUI.skin.button) { fontStyle = FontStyle.Bold }; + + if (GUILayout.Button(pauseLabel, pauseStyle, GUILayout.Height(25))) + { + EditorApplication.isPaused = !EditorApplication.isPaused; + } + GUI.backgroundColor = oldBgColor; + + GUILayout.EndArea(); + Handles.EndGUI(); + } + + /// + /// Draws a preset button that automatically highlights green if it matches the current time scale. + /// + private static void DrawSpeedButton(string label, float targetSpeed) + { + Color oldBgColor = GUI.backgroundColor; + + // Highlight green if this is the active speed AND the game is not paused + bool isActive = Mathf.Approximately(Time.timeScale, targetSpeed) && !EditorApplication.isPaused; + if (isActive) + { + GUI.backgroundColor = new Color(0.4f, 1f, 0.4f); // Light Green + } + + if (GUILayout.Button(label, GUILayout.Height(22))) + { + Time.timeScale = targetSpeed; + + // Auto-resume if the player clicked a speed while paused + if (EditorApplication.isPaused) + { + EditorApplication.isPaused = false; + } + } + + // Restore previous color + GUI.backgroundColor = oldBgColor; + } + } +} \ No newline at end of file diff --git a/Assets/Editor/TimeLord.cs.meta b/Assets/Editor/TimeLord.cs.meta new file mode 100644 index 00000000..7ceb477f --- /dev/null +++ b/Assets/Editor/TimeLord.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 66de86109a2db614797e172526afa8da \ No newline at end of file diff --git a/Assets/Readme.asset b/Assets/Readme.asset deleted file mode 100644 index 77c2f83c..00000000 --- a/Assets/Readme.asset +++ /dev/null @@ -1,34 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fcf7219bab7fe46a1ad266029b2fee19, type: 3} - m_Name: Readme - m_EditorClassIdentifier: - icon: {fileID: 2800000, guid: 727a75301c3d24613a3ebcec4a24c2c8, type: 3} - title: URP Empty Template - sections: - - heading: Welcome to the Universal Render Pipeline - text: This template includes the settings and assets you need to start creating with the Universal Render Pipeline. - linkText: - url: - - heading: URP Documentation - text: - linkText: Read more about URP - url: https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest - - heading: Forums - text: - linkText: Get answers and support - url: https://forum.unity.com/forums/universal-render-pipeline.383/ - - heading: Report bugs - text: - linkText: Submit a report - url: https://unity3d.com/unity/qa/bug-reporting - loadedLayout: 1 diff --git a/Assets/Readme.asset.meta b/Assets/Readme.asset.meta deleted file mode 100644 index ab3ad453..00000000 --- a/Assets/Readme.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 8105016687592461f977c054a80ce2f2 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts.meta b/Assets/Scripts.meta similarity index 57% rename from Assets/TutorialInfo/Scripts.meta rename to Assets/Scripts.meta index 02da605b..002d3c25 100644 --- a/Assets/TutorialInfo/Scripts.meta +++ b/Assets/Scripts.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: 5a9bcd70e6a4b4b05badaa72e827d8e0 +guid: 8cdbb6650bb138a45a32b6f7f2517667 folderAsset: yes -timeCreated: 1475835190 -licenseType: Store DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Editor.meta b/Assets/Scripts/Camera Controller.meta similarity index 57% rename from Assets/TutorialInfo/Scripts/Editor.meta rename to Assets/Scripts/Camera Controller.meta index f59f0996..86f0f3d9 100644 --- a/Assets/TutorialInfo/Scripts/Editor.meta +++ b/Assets/Scripts/Camera Controller.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: 3ad9b87dffba344c89909c6d1b1c17e1 +guid: 962e9e0d2b8d78d4fbb25fb03224f618 folderAsset: yes -timeCreated: 1475593892 -licenseType: Store DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Scripts/Camera Controller/CameraController.cs b/Assets/Scripts/Camera Controller/CameraController.cs new file mode 100644 index 00000000..451f068c --- /dev/null +++ b/Assets/Scripts/Camera Controller/CameraController.cs @@ -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(); + 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(); + 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); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Camera Controller/CameraController.cs.meta b/Assets/Scripts/Camera Controller/CameraController.cs.meta new file mode 100644 index 00000000..18f3a536 --- /dev/null +++ b/Assets/Scripts/Camera Controller/CameraController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3a2f40f0d755824f91dfa62616cd6fc \ No newline at end of file diff --git a/Assets/Scripts/Debug.meta b/Assets/Scripts/Debug.meta new file mode 100644 index 00000000..6fd5b9dc --- /dev/null +++ b/Assets/Scripts/Debug.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9477ecbb64ef4d9c8863fb16d2c4bc96 +timeCreated: 1773383891 \ No newline at end of file diff --git a/Assets/Scripts/Debug/PlayerDebugProvider.cs b/Assets/Scripts/Debug/PlayerDebugProvider.cs new file mode 100644 index 00000000..0a97a137 --- /dev/null +++ b/Assets/Scripts/Debug/PlayerDebugProvider.cs @@ -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(); + 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); + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Debug/PlayerDebugProvider.cs.meta b/Assets/Scripts/Debug/PlayerDebugProvider.cs.meta new file mode 100644 index 00000000..12bbe1b8 --- /dev/null +++ b/Assets/Scripts/Debug/PlayerDebugProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bf6aff0b7e11d41439ac80f4963a0795 \ No newline at end of file diff --git a/Assets/Scripts/EnvironmentScanner.cs b/Assets/Scripts/EnvironmentScanner.cs new file mode 100644 index 00000000..2fe5d56c --- /dev/null +++ b/Assets/Scripts/EnvironmentScanner.cs @@ -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; +} \ No newline at end of file diff --git a/Assets/Scripts/EnvironmentScanner.cs.meta b/Assets/Scripts/EnvironmentScanner.cs.meta new file mode 100644 index 00000000..c36eb832 --- /dev/null +++ b/Assets/Scripts/EnvironmentScanner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 169e37d35fede30409266070c88b118f \ No newline at end of file diff --git a/Assets/TutorialInfo/Icons.meta b/Assets/Scripts/GameSetup.meta similarity index 57% rename from Assets/TutorialInfo/Icons.meta rename to Assets/Scripts/GameSetup.meta index 1d19fb99..6fa146e3 100644 --- a/Assets/TutorialInfo/Icons.meta +++ b/Assets/Scripts/GameSetup.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: 8a0c9218a650547d98138cd835033977 +guid: b562adae77c550e4db1edcabf68b0530 folderAsset: yes -timeCreated: 1484670163 -licenseType: Store DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Scripts/GameSetup/CharacterAutoSetup.cs b/Assets/Scripts/GameSetup/CharacterAutoSetup.cs new file mode 100644 index 00000000..aa8da5ab --- /dev/null +++ b/Assets/Scripts/GameSetup/CharacterAutoSetup.cs @@ -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(); + PlayerStateMachine stateMachine = GetComponent(); + Animator animator = GetComponentInChildren(); + StringBuilder sb = new StringBuilder(); + + sb.AppendLine($"[AUTO-SETUP REPORT] Character: {gameObject.name}"); + sb.AppendLine("------------------------------------------------------------"); + + // 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("1. HEIGHT: {0:F3}m ({1}) ➔ {2:F3}m", 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("2. RADIUS: {0:F3}m ({1}) [/ 2] ➔ {2:F3}m", shoulderWidth, radiusMethod, finalRadius)); + + // 3. COLLISION PRECISION + float skinWidth = finalRadius * 0.10f; + sb.AppendLine(string.Format("3. SKIN WIDTH: {0:F3}m (Radius) [x 0.10] ➔ {1:F3}m", finalRadius, skinWidth)); + + // 4. CAPSULE CENTER + // Center Y = (Height / 2) + SkinWidth + float centerY = (finalHeight / 2f) + skinWidth; + sb.AppendLine(string.Format("4. CENTER Y: ({0:F3}m / 2) + {1:F3}m (Skin) ➔ {2:F3}m", finalHeight, skinWidth, centerY)); + sb.AppendLine(string.Format("5. CENTER Z: [Fixed Offset] ➔ {0:F3}m", zCenterOffset)); + + // 5. MOVEMENT CONSTRAINTS + float stepOffset = finalHeight * 0.15f; + sb.AppendLine(string.Format("6. STEP OFFSET: {0:F3}m (Height) [x 0.15] ➔ {1:F3}m", finalHeight, stepOffset)); + + // 6. GROUND CHECK (STATE MACHINE) + sb.AppendLine("------------------------------------------------------------"); + 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("7. GROUND CHECK SETUP:"); + sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ {1:F3}m", finalRadius, groundCheckRadius)); + sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ {1:F3}m", finalHeight, groundCheckOffsetY)); + sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ {0:F3}m", zCenterOffset)); + } + + sb.AppendLine("------------------------------------------------------------"); + + // 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(); + + 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; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta b/Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta new file mode 100644 index 00000000..7dcb2af1 --- /dev/null +++ b/Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e16a6690e589f0449ad89a6bf508ab62 \ No newline at end of file diff --git a/Assets/Scripts/GameSetup/CharacterSetupSettings.cs b/Assets/Scripts/GameSetup/CharacterSetupSettings.cs new file mode 100644 index 00000000..e917be03 --- /dev/null +++ b/Assets/Scripts/GameSetup/CharacterSetupSettings.cs @@ -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; + } +} \ No newline at end of file diff --git a/Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta b/Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta new file mode 100644 index 00000000..498b476d --- /dev/null +++ b/Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0d44cb4bd45c0e24bb3d8196a137db00 \ No newline at end of file diff --git a/Assets/Scripts/Interface.meta b/Assets/Scripts/Interface.meta new file mode 100644 index 00000000..83f7d629 --- /dev/null +++ b/Assets/Scripts/Interface.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f1398e4608a9406cb68d769d6bc9a3ec +timeCreated: 1773397974 \ No newline at end of file diff --git a/Assets/Scripts/Interface/IInteractable.cs b/Assets/Scripts/Interface/IInteractable.cs new file mode 100644 index 00000000..4107663c --- /dev/null +++ b/Assets/Scripts/Interface/IInteractable.cs @@ -0,0 +1,8 @@ +namespace OnlyScove.Scripts +{ + public interface IInteractable + { + string InteractionPrompt { get; } + void OnInteract(PlayerStateMachine player); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Interface/IInteractable.cs.meta b/Assets/Scripts/Interface/IInteractable.cs.meta new file mode 100644 index 00000000..bd9f3fde --- /dev/null +++ b/Assets/Scripts/Interface/IInteractable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f001a3bb0fe5e954ea724ded3158209d \ No newline at end of file diff --git a/Assets/Scripts/Optimization.meta b/Assets/Scripts/Optimization.meta new file mode 100644 index 00000000..b420068a --- /dev/null +++ b/Assets/Scripts/Optimization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e6d134b82fd19fc4aa38896d5e45efed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs b/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs new file mode 100644 index 00000000..6d782cc5 --- /dev/null +++ b/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs @@ -0,0 +1,49 @@ +using UnityEngine; + +namespace OnlyScove.Scripts +{ + /// + /// A version of PlayerStateMachine that simulates constant forward or backward input. + /// + 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(); + 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(); + } + } +} diff --git a/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs.meta b/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs.meta new file mode 100644 index 00000000..b00381b9 --- /dev/null +++ b/Assets/Scripts/Optimization/AutoPlayerStateMachine.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dbe8532b2e328a249bf61ae957c92486 \ No newline at end of file diff --git a/Assets/Scripts/Optimization/JobsMovementManager.cs b/Assets/Scripts/Optimization/JobsMovementManager.cs new file mode 100644 index 00000000..e7f76e92 --- /dev/null +++ b/Assets/Scripts/Optimization/JobsMovementManager.cs @@ -0,0 +1,129 @@ +using UnityEngine; +using Unity.Collections; +using Unity.Jobs; +using UnityEngine.Jobs; +using Unity.Burst; + +namespace Elbyss.Optimization +{ + /// + /// Manages 10,000+ objects using C# Job System and Burst Compiler. + /// This avoids the overhead of 10,000 individual Update() calls. + /// + 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 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(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(out var anim)) anim.enabled = false; + if (go.TryGetComponent(out var cc)) cc.enabled = false; + + // Disable all other custom scripts + MonoBehaviour[] scripts = go.GetComponents(); + 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 Directions; + + public void Execute(int index, TransformAccess transform) + { + // Directly modify the transform position in parallel + transform.position += Directions[index] * Speed * DeltaTime; + } + } + } +} diff --git a/Assets/Scripts/Optimization/JobsMovementManager.cs.meta b/Assets/Scripts/Optimization/JobsMovementManager.cs.meta new file mode 100644 index 00000000..2c3a9a82 --- /dev/null +++ b/Assets/Scripts/Optimization/JobsMovementManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 41899df442467714dbee462ae451773a \ No newline at end of file diff --git a/Assets/Scripts/Optimization/MassiveSpawner.cs b/Assets/Scripts/Optimization/MassiveSpawner.cs new file mode 100644 index 00000000..f757a4e1 --- /dev/null +++ b/Assets/Scripts/Optimization/MassiveSpawner.cs @@ -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 _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(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 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); +// } +// } +// } +// } diff --git a/Assets/Scripts/Optimization/MassiveSpawner.cs.meta b/Assets/Scripts/Optimization/MassiveSpawner.cs.meta new file mode 100644 index 00000000..334b7683 --- /dev/null +++ b/Assets/Scripts/Optimization/MassiveSpawner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 50831f537cbccac4c9bf4067b6b158c7 \ No newline at end of file diff --git a/Assets/Scripts/Optimization/PerformanceHUD.cs b/Assets/Scripts/Optimization/PerformanceHUD.cs new file mode 100644 index 00000000..a25d63f5 --- /dev/null +++ b/Assets/Scripts/Optimization/PerformanceHUD.cs @@ -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); + } + } +} diff --git a/Assets/Scripts/Optimization/PerformanceHUD.cs.meta b/Assets/Scripts/Optimization/PerformanceHUD.cs.meta new file mode 100644 index 00000000..031cdef0 --- /dev/null +++ b/Assets/Scripts/Optimization/PerformanceHUD.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9c9a374a3ab089d41a6784b1ffad3b6f \ No newline at end of file diff --git a/Assets/Scripts/Optimization/StressTestSpawner.cs b/Assets/Scripts/Optimization/StressTestSpawner.cs new file mode 100644 index 00000000..72178969 --- /dev/null +++ b/Assets/Scripts/Optimization/StressTestSpawner.cs @@ -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 spawnedObjects = new List(); + 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(); + if (realInput != null) realInput.enabled = false; + + var originalSM = newObj.GetComponent(); + if (originalSM != null) + { + DestroyImmediate(originalSM); + var autoSM = newObj.AddComponent(); + autoSM.alwaysMoveForward = true; + autoSM.alwaysRun = true; + } + } + + if (stripHeavyComponents) + { + if (newObj.TryGetComponent(out var anim)) anim.enabled = false; + if (newObj.TryGetComponent(out var cc)) cc.enabled = false; + if (newObj.TryGetComponent(out var sm)) sm.enabled = false; + } + + spawnedObjects.Add(newObj); + currentSpawnCount++; + } + } +} diff --git a/Assets/Scripts/Optimization/StressTestSpawner.cs.meta b/Assets/Scripts/Optimization/StressTestSpawner.cs.meta new file mode 100644 index 00000000..a49cd7b1 --- /dev/null +++ b/Assets/Scripts/Optimization/StressTestSpawner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4a7c5ef310b7f354685dc6706be2d530 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller.meta b/Assets/Scripts/Player Controller.meta new file mode 100644 index 00000000..f878e1bf --- /dev/null +++ b/Assets/Scripts/Player Controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e0fd09e39d1b90458b7097e555a9f3f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Player Controller/InputReader.cs b/Assets/Scripts/Player Controller/InputReader.cs new file mode 100644 index 00000000..f62a3b3d --- /dev/null +++ b/Assets/Scripts/Player Controller/InputReader.cs @@ -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(); + } + + public void OnLook(InputAction.CallbackContext context) + { + LookInput = context.ReadValue(); + } + + public void OnScroll(InputAction.CallbackContext context) + { + ScrollInput = context.ReadValue(); + } + + 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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/InputReader.cs.meta b/Assets/Scripts/Player Controller/InputReader.cs.meta new file mode 100644 index 00000000..84788156 --- /dev/null +++ b/Assets/Scripts/Player Controller/InputReader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5962d8f2c8e40e240a4a4907c7b539fa \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/ParkourAction.cs b/Assets/Scripts/Player Controller/ParkourAction.cs new file mode 100644 index 00000000..02ae6bdb --- /dev/null +++ b/Assets/Scripts/Player Controller/ParkourAction.cs @@ -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; + } +} diff --git a/Assets/Scripts/Player Controller/ParkourAction.cs.meta b/Assets/Scripts/Player Controller/ParkourAction.cs.meta new file mode 100644 index 00000000..23e0398c --- /dev/null +++ b/Assets/Scripts/Player Controller/ParkourAction.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 985468dedaf32f44191b2cc29c813c8c \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/ParkourController.cs b/Assets/Scripts/Player Controller/ParkourController.cs new file mode 100644 index 00000000..3f515a00 --- /dev/null +++ b/Assets/Scripts/Player Controller/ParkourController.cs @@ -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(); + environmentScanner = GetComponent(); + animator = GetComponent(); + playerController = GetComponent(); + } + + 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; + } + } +} diff --git a/Assets/Scripts/Player Controller/ParkourController.cs.meta b/Assets/Scripts/Player Controller/ParkourController.cs.meta new file mode 100644 index 00000000..72f45c02 --- /dev/null +++ b/Assets/Scripts/Player Controller/ParkourController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0407ea3fbe445ac43b4f1ca3077ce283 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerAirDashState.cs b/Assets/Scripts/Player Controller/PlayerAirDashState.cs new file mode 100644 index 00000000..261fb982 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerAirDashState.cs @@ -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() {} + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerAirDashState.cs.meta b/Assets/Scripts/Player Controller/PlayerAirDashState.cs.meta new file mode 100644 index 00000000..4dde67d3 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerAirDashState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 43c3e5e400fea604fb432f84d5dd9ce1 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerBaseState.cs b/Assets/Scripts/Player Controller/PlayerBaseState.cs new file mode 100644 index 00000000..b7fc648f --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerBaseState.cs @@ -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(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerBaseState.cs.meta b/Assets/Scripts/Player Controller/PlayerBaseState.cs.meta new file mode 100644 index 00000000..d67fe415 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerBaseState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af622221f1750284885366b02b8bfbba \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerController.cs b/Assets/Scripts/Player Controller/PlayerController.cs new file mode 100644 index 00000000..903f8add --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerController.cs @@ -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(); + animator = GetComponent(); + characterController = GetComponent(); + } + + 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); + } + } +} diff --git a/Assets/Scripts/Player Controller/PlayerController.cs.meta b/Assets/Scripts/Player Controller/PlayerController.cs.meta new file mode 100644 index 00000000..791e6827 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cd897e8bcaabdc8408bb6aaf7c037537 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerCrouchState.cs b/Assets/Scripts/Player Controller/PlayerCrouchState.cs new file mode 100644 index 00000000..bb17cdc0 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerCrouchState.cs @@ -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)); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerCrouchState.cs.meta b/Assets/Scripts/Player Controller/PlayerCrouchState.cs.meta new file mode 100644 index 00000000..269e98e5 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerCrouchState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 82b04241d720e444a937973caed8eb42 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerDashState.cs b/Assets/Scripts/Player Controller/PlayerDashState.cs new file mode 100644 index 00000000..03891e85 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerDashState.cs @@ -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)); + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerDashState.cs.meta b/Assets/Scripts/Player Controller/PlayerDashState.cs.meta new file mode 100644 index 00000000..9e689d92 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerDashState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 03721c0a319be8c4093b616e2f46afe8 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerDodgeState.cs b/Assets/Scripts/Player Controller/PlayerDodgeState.cs new file mode 100644 index 00000000..1a1adc18 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerDodgeState.cs @@ -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() {} + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerDodgeState.cs.meta b/Assets/Scripts/Player Controller/PlayerDodgeState.cs.meta new file mode 100644 index 00000000..bb7be8b9 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerDodgeState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 71bce3b0373df774c8b1598487bcd4ee \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerFallState.cs b/Assets/Scripts/Player Controller/PlayerFallState.cs new file mode 100644 index 00000000..396de4bd --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerFallState.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerFallState.cs.meta b/Assets/Scripts/Player Controller/PlayerFallState.cs.meta new file mode 100644 index 00000000..fb45b846 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerFallState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2b239715e32b6a4791f677e0159b862 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerIdleState.cs b/Assets/Scripts/Player Controller/PlayerIdleState.cs new file mode 100644 index 00000000..509df688 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerIdleState.cs @@ -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)); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerIdleState.cs.meta b/Assets/Scripts/Player Controller/PlayerIdleState.cs.meta new file mode 100644 index 00000000..313a184c --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerIdleState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fd83e34f11c7aaa4ab144ee59db04e8d \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerInteractState.cs b/Assets/Scripts/Player Controller/PlayerInteractState.cs new file mode 100644 index 00000000..9617ebb7 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerInteractState.cs @@ -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() { } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerInteractState.cs.meta b/Assets/Scripts/Player Controller/PlayerInteractState.cs.meta new file mode 100644 index 00000000..cfabf0df --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerInteractState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0b772d1c0c26c634fad5e71b43db9385 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerJumpState.cs b/Assets/Scripts/Player Controller/PlayerJumpState.cs new file mode 100644 index 00000000..9f579b69 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerJumpState.cs @@ -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() {} + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerJumpState.cs.meta b/Assets/Scripts/Player Controller/PlayerJumpState.cs.meta new file mode 100644 index 00000000..1e1df8e3 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerJumpState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ab5bb519df10fe45a5af5f45111ed4d \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerMoveState.cs b/Assets/Scripts/Player Controller/PlayerMoveState.cs new file mode 100644 index 00000000..01d8d195 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerMoveState.cs @@ -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)); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerMoveState.cs.meta b/Assets/Scripts/Player Controller/PlayerMoveState.cs.meta new file mode 100644 index 00000000..b0e20926 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerMoveState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a11b0f0549f3dbd45bbdfd8114586a43 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerParkourState.cs b/Assets/Scripts/Player Controller/PlayerParkourState.cs new file mode 100644 index 00000000..6e7b8a42 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerParkourState.cs @@ -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() {} + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerParkourState.cs.meta b/Assets/Scripts/Player Controller/PlayerParkourState.cs.meta new file mode 100644 index 00000000..c23eec07 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerParkourState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d9b73ee252f157e49a08f8f7566dc6ac \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerRunState.cs b/Assets/Scripts/Player Controller/PlayerRunState.cs new file mode 100644 index 00000000..51616543 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerRunState.cs @@ -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)); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerRunState.cs.meta b/Assets/Scripts/Player Controller/PlayerRunState.cs.meta new file mode 100644 index 00000000..c6fa4e62 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerRunState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ef096feea261a8d449384a68f71470b3 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerStateMachine.cs b/Assets/Scripts/Player Controller/PlayerStateMachine.cs new file mode 100644 index 00000000..19a2ee9b --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerStateMachine.cs @@ -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 interactablesNearby = new List(); + 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(); + Input = GetComponent(); + Anim = GetComponentInChildren(); + Scanner = GetComponent(); + Cam = Camera.main?.GetComponent(); + } + + 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); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerStateMachine.cs.meta b/Assets/Scripts/Player Controller/PlayerStateMachine.cs.meta new file mode 100644 index 00000000..c0aab493 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerStateMachine.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 848ad6fdeb60b254497391392419b063 \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerThrustState.cs b/Assets/Scripts/Player Controller/PlayerThrustState.cs new file mode 100644 index 00000000..0896f740 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerThrustState.cs @@ -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() {} + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player Controller/PlayerThrustState.cs.meta b/Assets/Scripts/Player Controller/PlayerThrustState.cs.meta new file mode 100644 index 00000000..a77c6f95 --- /dev/null +++ b/Assets/Scripts/Player Controller/PlayerThrustState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4e76cfd208cc6f44b8b954147ee1defb \ No newline at end of file diff --git a/Assets/Scripts/SpineProxy.cs b/Assets/Scripts/SpineProxy.cs new file mode 100644 index 00000000..41c853c8 --- /dev/null +++ b/Assets/Scripts/SpineProxy.cs @@ -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; + } + } +} diff --git a/Assets/Scripts/SpineProxy.cs.meta b/Assets/Scripts/SpineProxy.cs.meta new file mode 100644 index 00000000..b8262516 --- /dev/null +++ b/Assets/Scripts/SpineProxy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 682245d9ac89ba4409aa3a92f17f5c6c \ No newline at end of file diff --git a/Assets/Scripts/StickyNote.cs b/Assets/Scripts/StickyNote.cs new file mode 100644 index 00000000..91162616 --- /dev/null +++ b/Assets/Scripts/StickyNote.cs @@ -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 +} \ No newline at end of file diff --git a/Assets/Scripts/StickyNote.cs.meta b/Assets/Scripts/StickyNote.cs.meta new file mode 100644 index 00000000..60347617 --- /dev/null +++ b/Assets/Scripts/StickyNote.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 20733ff6cdef89e408acddf8ce51503d \ No newline at end of file diff --git a/Assets/Scripts/UI.meta b/Assets/Scripts/UI.meta new file mode 100644 index 00000000..5df265da --- /dev/null +++ b/Assets/Scripts/UI.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 91b95b0bf23143b68483f912b558e6f0 +timeCreated: 1773383929 \ No newline at end of file diff --git a/Assets/Scripts/UI/MyUIDisplay.cs b/Assets/Scripts/UI/MyUIDisplay.cs new file mode 100644 index 00000000..e1da65dc --- /dev/null +++ b/Assets/Scripts/UI/MyUIDisplay.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UI/MyUIDisplay.cs.meta b/Assets/Scripts/UI/MyUIDisplay.cs.meta new file mode 100644 index 00000000..cdb74b2a --- /dev/null +++ b/Assets/Scripts/UI/MyUIDisplay.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cd13c5c96000414397dd7d41a73edd62 +timeCreated: 1773383951 \ No newline at end of file diff --git a/Assets/Scripts/VFX.meta b/Assets/Scripts/VFX.meta new file mode 100644 index 00000000..cf5580d9 --- /dev/null +++ b/Assets/Scripts/VFX.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 793d95c58fcd4034f8b3152f2317a9e5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/VFX/SlashMeshGenerator.cs b/Assets/Scripts/VFX/SlashMeshGenerator.cs new file mode 100644 index 00000000..4ba81871 --- /dev/null +++ b/Assets/Scripts/VFX/SlashMeshGenerator.cs @@ -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().mesh = mesh; + } + + void Awake() + { + GenerateMesh(); + } +} diff --git a/Assets/Scripts/VFX/SlashMeshGenerator.cs.meta b/Assets/Scripts/VFX/SlashMeshGenerator.cs.meta new file mode 100644 index 00000000..c7c5165c --- /dev/null +++ b/Assets/Scripts/VFX/SlashMeshGenerator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 78fcc45270373164cadbaa6681bab73b \ No newline at end of file diff --git a/Assets/Scripts/VFX/SukunaAbilityController.cs b/Assets/Scripts/VFX/SukunaAbilityController.cs new file mode 100644 index 00000000..526227ea --- /dev/null +++ b/Assets/Scripts/VFX/SukunaAbilityController.cs @@ -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(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; + } +} diff --git a/Assets/Scripts/VFX/SukunaAbilityController.cs.meta b/Assets/Scripts/VFX/SukunaAbilityController.cs.meta new file mode 100644 index 00000000..5d60d5e0 --- /dev/null +++ b/Assets/Scripts/VFX/SukunaAbilityController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1630760c9d97a5f4eb1bc179549c95cd \ No newline at end of file diff --git a/Assets/Scripts/VFX/SukunaDomainController.cs b/Assets/Scripts/VFX/SukunaDomainController.cs new file mode 100644 index 00000000..082a8529 --- /dev/null +++ b/Assets/Scripts/VFX/SukunaDomainController.cs @@ -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 activeSlashes = new List(); + private Volume localVolume; + private GameObject spawnedShrine; + + private void Start() + { + if (playerStateMachine == null) + playerStateMachine = GetComponent(); + + if (playerStateMachine != null && playerStateMachine.Input != null) + { + playerStateMachine.Input.OnPreviousInteractEvent += HandleDomainExpansion; + Debug.Log("[Sukuna] Sẵn sàng. slashRate: " + slashRate + ""); + } + } + + private void OnDestroy() + { + if (playerStateMachine != null && playerStateMachine.Input != null) + { + playerStateMachine.Input.OnPreviousInteractEvent -= HandleDomainExpansion; + } + } + + private void HandleDomainExpansion() + { + if (isActive) return; + Debug.Log("[Sukuna] RYŌIKI TENKAI: FUKUMA MIZUZUSHI!"); + 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(); + localVolume.isGlobal = false; + localVolume.priority = 100; + localVolume.profile = domainVolumeProfile; + SphereCollider volumeCollider = volumeObj.AddComponent(); + 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); + } + } +} diff --git a/Assets/Scripts/VFX/SukunaDomainController.cs.meta b/Assets/Scripts/VFX/SukunaDomainController.cs.meta new file mode 100644 index 00000000..1aa125f5 --- /dev/null +++ b/Assets/Scripts/VFX/SukunaDomainController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 271dd39a46bad974485107bb1a070e0a \ No newline at end of file diff --git a/Assets/Scripts/VFX/SukunaProjectile.cs b/Assets/Scripts/VFX/SukunaProjectile.cs new file mode 100644 index 00000000..c557f6cf --- /dev/null +++ b/Assets/Scripts/VFX/SukunaProjectile.cs @@ -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; + } +} diff --git a/Assets/Scripts/VFX/SukunaProjectile.cs.meta b/Assets/Scripts/VFX/SukunaProjectile.cs.meta new file mode 100644 index 00000000..f7d48e16 --- /dev/null +++ b/Assets/Scripts/VFX/SukunaProjectile.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b9b0aad9f1697954a9f8710b4e8f3f2e \ No newline at end of file diff --git a/Assets/Scripts/VFX/SukunaSlashEffect.cs b/Assets/Scripts/VFX/SukunaSlashEffect.cs new file mode 100644 index 00000000..a11fd61e --- /dev/null +++ b/Assets/Scripts/VFX/SukunaSlashEffect.cs @@ -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(); + 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); + } + } +} diff --git a/Assets/Scripts/VFX/SukunaSlashEffect.cs.meta b/Assets/Scripts/VFX/SukunaSlashEffect.cs.meta new file mode 100644 index 00000000..cda3e9e8 --- /dev/null +++ b/Assets/Scripts/VFX/SukunaSlashEffect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 66ad11f71e7aac841be73f4b03cf0d83 \ No newline at end of file diff --git a/Assets/Settings/PC_RPAsset.asset b/Assets/Settings/PC_RPAsset.asset index 8b30a060..9b2b0467 100644 --- a/Assets/Settings/PC_RPAsset.asset +++ b/Assets/Settings/PC_RPAsset.asset @@ -12,8 +12,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3} m_Name: PC_RPAsset m_EditorClassIdentifier: - k_AssetVersion: 12 - k_AssetPreviousVersion: 12 + k_AssetVersion: 13 + k_AssetPreviousVersion: 13 m_RendererType: 1 m_RendererData: {fileID: 0} m_RendererDataList: @@ -53,6 +53,7 @@ MonoBehaviour: m_AdditionalLightsShadowResolutionTierHigh: 1024 m_ReflectionProbeBlending: 1 m_ReflectionProbeBoxProjection: 1 + m_ReflectionProbeAtlas: 1 m_ShadowDistance: 50 m_ShadowCascadeCount: 4 m_Cascade2Split: 0.25 @@ -78,11 +79,11 @@ MonoBehaviour: m_UseAdaptivePerformance: 1 m_ColorGradingMode: 0 m_ColorGradingLutSize: 32 + m_AllowPostProcessAlphaOutput: 0 m_UseFastSRGBLinearConversion: 0 m_SupportDataDrivenLensFlare: 1 m_SupportScreenSpaceLensFlare: 1 m_GPUResidentDrawerMode: 0 - m_UseLegacyLightmaps: 0 m_SmallMeshScreenPercentage: 0 m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0 m_ShadowType: 1 @@ -109,6 +110,7 @@ MonoBehaviour: m_PrefilterDebugKeywords: 1 m_PrefilterWriteRenderingLayers: 0 m_PrefilterHDROutput: 1 + m_PrefilterAlphaOutput: 0 m_PrefilterSSAODepthNormals: 0 m_PrefilterSSAOSourceDepthLow: 1 m_PrefilterSSAOSourceDepthMedium: 1 @@ -126,8 +128,14 @@ MonoBehaviour: m_PrefilterSoftShadowsQualityHigh: 0 m_PrefilterSoftShadows: 0 m_PrefilterScreenCoord: 1 + m_PrefilterScreenSpaceIrradiance: 0 m_PrefilterNativeRenderPass: 1 m_PrefilterUseLegacyLightmaps: 0 + m_PrefilterBicubicLightmapSampling: 0 + m_PrefilterReflectionProbeRotation: 0 + m_PrefilterReflectionProbeBlending: 0 + m_PrefilterReflectionProbeBoxProjection: 0 + m_PrefilterReflectionProbeAtlas: 0 m_ShaderVariantLogLevel: 0 m_ShadowCascades: 0 m_Textures: diff --git a/Assets/TutorialInfo/Icons/URP.png b/Assets/TutorialInfo/Icons/URP.png deleted file mode 100644 index 6194a807e27158f864a7c7677f4cbf62d8b94503..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24069 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 HejoV