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

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ba062aa6c92b140379dbc06b43dd3b9b
guid: bfcf56e2246d90745bf402b615e6b7be
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -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<StickyNote>() != null)
{
Debug.LogWarning($"StickyNote component already exists on '{selectedGameObject.name}'.");
return;
}
// Add the StickyNote component
StickyNote stickyNote = selectedGameObject.AddComponent<StickyNote>();
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<StickyNote>() == null;
}
}
}

View File

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

View File

@@ -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<AutoSaveTool>("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<AutoSaveTool>())
{
GetWindow<AutoSaveTool>().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($"<color=#00FF00><b>[AutoSave]</b></color> Project saved automatically at {DateTime.Now.ToString("HH:mm:ss")}");
}
ResetTimer();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a4d73f0e418e4d4f92db04c0d6acc25

View File

@@ -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<CameraBookmarksTool>("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($"<color=#00FF00><b>[CameraBookmarks]</b></color> {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($"<b>[CameraBookmarks]</b> {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($"<color=#00FFFF><b>[CameraBookmarks]</b></color> {message}");
}
catch (Exception ex)
{
Debug.LogError($"<b>[CameraBookmarks]</b> Failed to parse data for Slot {slotIndex}. Error: {ex.Message}");
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5044058a94d2d014bb3bd8a1bd1e5707

View File

@@ -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<DistributeTool>("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($"<color=#FFCC00><b>[DistributeTool]</b></color> Distributed {sorted.Count} objects along the {(axis == 0 ? "X" : axis == 1 ? "Y" : "Z")} axis.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 18664781a6d758c42a67a8d1c895c3dd

View File

@@ -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);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a86ff34ff4b799498a6e0e250dfbd52

View File

@@ -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;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 840e668e5bda80441802a7b8ffef62f9

View File

@@ -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<LevelDecorator>("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($"<color=#FF8800><b>[LevelDecorator]</b></color> 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($"<color=#FF8800><b>[LevelDecorator]</b></color> Scale randomized for {Selection.transforms.Length} objects.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 85b906e8fc762834bab1190294ad2d15

View File

@@ -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 += $"<color=#FF5555>X: {delta.x:F2}</color> ";
if (delta.y > 0.01f) axisText += $"<color=#55FF55>Y: {delta.y:F2}</color> ";
if (delta.z > 0.01f) axisText += $"<color=#5555FF>Z: {delta.z:F2}</color>";
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38e38ba189f79f34eb81984f39aeefaf

View File

@@ -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<PlayFromHereTool>("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("<b>[PlayFromHere]</b> 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($"<color=#00FFFF><b>[PlayFromHere]</b></color> {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<GameObject>(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;
}
}
}

View File

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

View File

@@ -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<AudioClip>(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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 417cdb3b433688a419848d097f778a2b

View File

@@ -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<ProjectDashboardTool>("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("<b>[Dashboard]</b> 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("<b>[Dashboard]</b> Cannot load scene while in Play Mode.");
return;
}
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
EditorSceneManager.OpenScene(path);
Debug.Log($"<b>[Dashboard]</b> 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($"<color=#FF5555><b>[Dashboard]</b></color> Cleared PlayerPrefs and {deletedCount} save file(s).");
// Show notification on the Editor Window
this.ShowNotification(new GUIContent("Data Cleared Successfully!"));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 382b2c71505c85d488edc73f585e957a

View File

@@ -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<string> history = new List<string>();
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<Object>(path);
Selection.activeObject = obj;
EditorGUIUtility.PingObject(obj);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09e4547a2316bd340b1e7443f68858b9

View File

@@ -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<string> foundPaths = new List<string>();
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<ReferenceFinderTool>("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<Object>(path);
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
}
EditorGUILayout.EndHorizontal();
}
}
}

View File

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

View File

@@ -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<SmartBootstrapper>("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<SceneAsset>(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<SceneAsset>(BootScenePath);
if (bootScene == null)
{
Debug.LogWarning($"<b>[SmartBoot]</b> Scene not found at saved path: <color=yellow>{BootScenePath}</color>. 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($"<color=#00FF00><b>[SmartBoot]</b></color> Starting from Boot Scene... <i>(Will return context to {currentSceneName})</i>");
}
}
}
}

View File

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

View File

@@ -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<Color, Texture2D> backgroundCache = new Dictionary<Color, Texture2D>();
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;
}
}
}

View File

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

146
Assets/Editor/TimeLord.cs Normal file
View File

@@ -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 ? "<color=#FF6B6B>PAUSED</color>" : $"<color=#4ECDC4>{Time.timeScale:F2}x</color>";
GUILayout.Label($"⏳ <b>TIME LORD</b> | 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();
}
/// <summary>
/// Draws a preset button that automatically highlights green if it matches the current time scale.
/// </summary>
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;
}
}
}

View File

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

View File

@@ -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

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8105016687592461f977c054a80ce2f2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,9 +1,8 @@
fileFormatVersion: 2
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
guid: 8cdbb6650bb138a45a32b6f7f2517667
folderAsset: yes
timeCreated: 1475835190
licenseType: Store
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,9 +1,8 @@
fileFormatVersion: 2
guid: 3ad9b87dffba344c89909c6d1b1c17e1
guid: 962e9e0d2b8d78d4fbb25fb03224f618
folderAsset: yes
timeCreated: 1475593892
licenseType: Store
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
fileFormatVersion: 2
guid: 8a0c9218a650547d98138cd835033977
guid: b562adae77c550e4db1edcabf68b0530
folderAsset: yes
timeCreated: 1484670163
licenseType: Store
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More