Commit 1
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba062aa6c92b140379dbc06b43dd3b9b
|
||||
guid: bfcf56e2246d90745bf402b615e6b7be
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
45
Assets/Editor/AddStickyNoteContextMenu.cs
Normal file
45
Assets/Editor/AddStickyNoteContextMenu.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/AddStickyNoteContextMenu.cs.meta
Normal file
2
Assets/Editor/AddStickyNoteContextMenu.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ffe544b9fe35744cba464108a3b4203
|
||||
175
Assets/Editor/AutoSaveTool.cs
Normal file
175
Assets/Editor/AutoSaveTool.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/AutoSaveTool.cs.meta
Normal file
2
Assets/Editor/AutoSaveTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a4d73f0e418e4d4f92db04c0d6acc25
|
||||
226
Assets/Editor/CameraBookmarksTool.cs
Normal file
226
Assets/Editor/CameraBookmarksTool.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/CameraBookmarksTool.cs.meta
Normal file
2
Assets/Editor/CameraBookmarksTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5044058a94d2d014bb3bd8a1bd1e5707
|
||||
131
Assets/Editor/DistributeTool.cs
Normal file
131
Assets/Editor/DistributeTool.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/DistributeTool.cs.meta
Normal file
2
Assets/Editor/DistributeTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18664781a6d758c42a67a8d1c895c3dd
|
||||
81
Assets/Editor/HierarchyEnhancer.cs
Normal file
81
Assets/Editor/HierarchyEnhancer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/HierarchyEnhancer.cs.meta
Normal file
2
Assets/Editor/HierarchyEnhancer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a86ff34ff4b799498a6e0e250dfbd52
|
||||
84
Assets/Editor/HierarchySeparators.cs
Normal file
84
Assets/Editor/HierarchySeparators.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/HierarchySeparators.cs.meta
Normal file
2
Assets/Editor/HierarchySeparators.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 840e668e5bda80441802a7b8ffef62f9
|
||||
189
Assets/Editor/LevelDecorator.cs
Normal file
189
Assets/Editor/LevelDecorator.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/LevelDecorator.cs.meta
Normal file
2
Assets/Editor/LevelDecorator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85b906e8fc762834bab1190294ad2d15
|
||||
149
Assets/Editor/MeasureTool.cs
Normal file
149
Assets/Editor/MeasureTool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/MeasureTool.cs.meta
Normal file
2
Assets/Editor/MeasureTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38e38ba189f79f34eb81984f39aeefaf
|
||||
215
Assets/Editor/PlayFromHereTool.cs
Normal file
215
Assets/Editor/PlayFromHereTool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/PlayFromHereTool.cs.meta
Normal file
2
Assets/Editor/PlayFromHereTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8d518311a665394bbaf16052dc48f1c
|
||||
163
Assets/Editor/ProjectAudioPreview.cs
Normal file
163
Assets/Editor/ProjectAudioPreview.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/ProjectAudioPreview.cs.meta
Normal file
2
Assets/Editor/ProjectAudioPreview.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 417cdb3b433688a419848d097f778a2b
|
||||
226
Assets/Editor/ProjectDashboardTool.cs
Normal file
226
Assets/Editor/ProjectDashboardTool.cs
Normal 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!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/ProjectDashboardTool.cs.meta
Normal file
2
Assets/Editor/ProjectDashboardTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 382b2c71505c85d488edc73f585e957a
|
||||
95
Assets/Editor/ProjectNavigation.cs
Normal file
95
Assets/Editor/ProjectNavigation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/ProjectNavigation.cs.meta
Normal file
2
Assets/Editor/ProjectNavigation.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09e4547a2316bd340b1e7443f68858b9
|
||||
161
Assets/Editor/ReferenceFinderTool.cs
Normal file
161
Assets/Editor/ReferenceFinderTool.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/ReferenceFinderTool.cs.meta
Normal file
2
Assets/Editor/ReferenceFinderTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4aa815f17d76a34e8063c78c9821e5e
|
||||
158
Assets/Editor/SmartBootstrapper.cs
Normal file
158
Assets/Editor/SmartBootstrapper.cs
Normal 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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/SmartBootstrapper.cs.meta
Normal file
2
Assets/Editor/SmartBootstrapper.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db3e1e6db7211d04cb2f994b539535be
|
||||
151
Assets/Editor/StickyNoteEditor.cs
Normal file
151
Assets/Editor/StickyNoteEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/StickyNoteEditor.cs.meta
Normal file
2
Assets/Editor/StickyNoteEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be3283bc34f51f742aefd7d713f5c8fa
|
||||
146
Assets/Editor/TimeLord.cs
Normal file
146
Assets/Editor/TimeLord.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/TimeLord.cs.meta
Normal file
2
Assets/Editor/TimeLord.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66de86109a2db614797e172526afa8da
|
||||
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8105016687592461f977c054a80ce2f2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
|
||||
guid: 8cdbb6650bb138a45a32b6f7f2517667
|
||||
folderAsset: yes
|
||||
timeCreated: 1475835190
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ad9b87dffba344c89909c6d1b1c17e1
|
||||
guid: 962e9e0d2b8d78d4fbb25fb03224f618
|
||||
folderAsset: yes
|
||||
timeCreated: 1475593892
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
287
Assets/Scripts/Camera Controller/CameraController.cs
Normal file
287
Assets/Scripts/Camera Controller/CameraController.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class CameraController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] InputReader inputReader; // Kéo thả Object chứa InputReader vào đây
|
||||
[SerializeField] Transform followTarget;
|
||||
[SerializeField] float distance = 5;
|
||||
[SerializeField] float minDistance = 2f;
|
||||
[SerializeField] float maxDistance = 15f;
|
||||
[SerializeField] float zoomSensitivity = 1f;
|
||||
[SerializeField] float sensitivity = 0.1f; // Độ nhạy (chỉnh trong Inspector)
|
||||
[SerializeField] LayerMask collisionLayers;
|
||||
[SerializeField] float cameraRadius = 0.2f;
|
||||
[SerializeField] float positionSmoothTime = 0.12f; // Độ trễ đuổi theo nhân vật
|
||||
[SerializeField] float rotationSmoothTime = 5f; // Tốc độ làm mượt vòng xoay chuột
|
||||
|
||||
[Header("Auto Rotation")]
|
||||
[SerializeField] bool useAutoRotation = true;
|
||||
[SerializeField] float autoRotateDelay = 2.5f; // Sau bao lâu không chạm chuột thì xoay
|
||||
[SerializeField] float autoRotateSpeed = 2f; // Tốc độ xoay về sau lưng
|
||||
|
||||
[Header("Occlusion Transparency")]
|
||||
[SerializeField] bool useTransparency = true;
|
||||
[SerializeField] LayerMask transparencyLayers;
|
||||
[SerializeField] float fadeAlpha = 0.3f; // Độ trong suốt (0 là biến mất, 1 là hiện rõ)
|
||||
|
||||
[Header("Dynamic FOV")]
|
||||
[SerializeField] bool useDynamicFOV = true;
|
||||
[SerializeField] float baseFOV = 60f;
|
||||
[SerializeField] float sprintFOV = 70f;
|
||||
[SerializeField] float fovSmoothTime = 5f;
|
||||
|
||||
[Header("Character Fading")]
|
||||
[SerializeField] bool useCharacterFading = true;
|
||||
[SerializeField] float minVisibleDistance = 1.2f; // Khoảng cách bắt đầu mờ
|
||||
[SerializeField] float fullyHiddenDistance = 0.6f; // Khoảng cách biến mất hẳn
|
||||
[SerializeField] Renderer[] characterRenderers; // Kéo các Mesh của nhân vật vào đây
|
||||
|
||||
[Header("Side Bias")]
|
||||
[SerializeField] bool useSideBias = true;
|
||||
[SerializeField] float horizontalBiasAmount = 0.5f; // Độ lệch sang trái/phải
|
||||
[SerializeField] float biasSmoothTime = 3f; // Tốc độ chuyển đổi độ lệch
|
||||
|
||||
[Header("Camera Shake")]
|
||||
[SerializeField] bool useShake = true;
|
||||
private float shakeIntensity = 0f;
|
||||
private float shakeDuration = 0f;
|
||||
private float shakeTimer = 0f;
|
||||
private Vector3 shakeOffset;
|
||||
|
||||
[SerializeField] float minVerticalAngle = -45f;
|
||||
[SerializeField] float maxVerticalAngle = 45f;
|
||||
[SerializeField] Vector2 framingOffset;
|
||||
[SerializeField] private bool invertX;
|
||||
[SerializeField] private bool invertY;
|
||||
|
||||
private float rotationX;
|
||||
private float rotationY;
|
||||
|
||||
private float invertXVal;
|
||||
private float invertYVal;
|
||||
|
||||
private float lastInputTime;
|
||||
private Vector3 currentVelocity;
|
||||
private Quaternion currentRotation;
|
||||
|
||||
private Camera cam;
|
||||
private Renderer lastFadedRenderer;
|
||||
private Color originalColor;
|
||||
|
||||
private float currentSideBias;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
cam = GetComponent<Camera>();
|
||||
Cursor.visible = false;
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
|
||||
// Khởi tạo vòng xoay hiện tại
|
||||
rotationX = transform.eulerAngles.x;
|
||||
rotationY = transform.eulerAngles.y;
|
||||
currentRotation = transform.rotation;
|
||||
lastInputTime = Time.time;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (inputReader != null)
|
||||
{
|
||||
// Kiểm tra xem có input xoay chuột không
|
||||
if (inputReader.LookInput.magnitude > 0.01f)
|
||||
{
|
||||
lastInputTime = Time.time;
|
||||
}
|
||||
|
||||
invertXVal = (invertX) ? -1 : 1;
|
||||
invertYVal = (invertY) ? -1 : 1;
|
||||
|
||||
rotationX -= inputReader.LookInput.y * invertYVal * sensitivity * Time.deltaTime;
|
||||
rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);
|
||||
|
||||
rotationY += inputReader.LookInput.x * invertXVal * sensitivity * Time.deltaTime;
|
||||
|
||||
// Logic Side Bias (Lệch khung hình khi di chuyển)
|
||||
if (useSideBias)
|
||||
{
|
||||
float targetBias = -inputReader.MoveInput.x * horizontalBiasAmount;
|
||||
currentSideBias = Mathf.Lerp(currentSideBias, targetBias, biasSmoothTime * Time.deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSideBias = 0;
|
||||
}
|
||||
|
||||
// Logic Tự động xoay sau lưng (Auto-Correction)
|
||||
if (useAutoRotation && Time.time - lastInputTime > autoRotateDelay)
|
||||
{
|
||||
// Chỉ xoay khi nhân vật đang di chuyển
|
||||
if (inputReader.MoveInput.magnitude > 0.1f)
|
||||
{
|
||||
// Lấy hướng nhân vật đang nhìn (Yaw)
|
||||
float targetYaw = followTarget.eulerAngles.y;
|
||||
// Dùng LerpAngle để xoay mượt mà về hướng đó
|
||||
rotationY = Mathf.LerpAngle(rotationY, targetYaw, autoRotateSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
float scrollDelta = inputReader.ScrollInput.y;
|
||||
if (Mathf.Abs(scrollDelta) > 0.1f)
|
||||
{
|
||||
distance -= scrollDelta * zoomSensitivity * Time.deltaTime;
|
||||
distance = Mathf.Clamp(distance, minDistance, maxDistance);
|
||||
}
|
||||
}
|
||||
|
||||
// Xoay chuột: Làm mượt bằng Slerp
|
||||
Quaternion targetRotation = Quaternion.Euler(rotationX, rotationY, 0f);
|
||||
currentRotation = Quaternion.Slerp(currentRotation, targetRotation, rotationSmoothTime * Time.deltaTime);
|
||||
|
||||
// Vị trí mục tiêu: Áp dụng offset + Side Bias
|
||||
Vector3 focusPosition = followTarget.position + currentRotation * new Vector3(framingOffset.x + currentSideBias, framingOffset.y, 0);
|
||||
|
||||
// Collision Logic (Dùng currentRotation đã được làm mượt)
|
||||
float targetDistance = distance;
|
||||
RaycastHit hit;
|
||||
Vector3 rayStart = focusPosition;
|
||||
Vector3 rayDirection = currentRotation * Vector3.back;
|
||||
|
||||
if (Physics.SphereCast(rayStart, cameraRadius, rayDirection, out hit, distance, collisionLayers))
|
||||
{
|
||||
targetDistance = Mathf.Max(minDistance, hit.distance - 0.1f);
|
||||
}
|
||||
|
||||
// Logic Làm mờ nhân vật (Character Fading)
|
||||
if (useCharacterFading && characterRenderers != null && characterRenderers.Length > 0)
|
||||
{
|
||||
HandleCharacterFading(targetDistance);
|
||||
}
|
||||
|
||||
// Vị trí cuối cùng: Làm mượt bằng SmoothDamp để tạo độ trễ đuổi theo
|
||||
Vector3 targetPosition = focusPosition - currentRotation * new Vector3(0, 0, targetDistance);
|
||||
|
||||
// Xử lý Camera Shake
|
||||
if (useShake && shakeTimer > 0)
|
||||
{
|
||||
HandleShake();
|
||||
}
|
||||
else
|
||||
{
|
||||
shakeOffset = Vector3.zero;
|
||||
}
|
||||
|
||||
transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref currentVelocity, positionSmoothTime) + shakeOffset;
|
||||
transform.rotation = currentRotation;
|
||||
|
||||
// Logic Làm trong suốt vật cản (Occlusion Transparency)
|
||||
if (useTransparency)
|
||||
{
|
||||
HandleTransparency(focusPosition);
|
||||
}
|
||||
|
||||
// Logic FOV linh hoạt
|
||||
if (useDynamicFOV && cam != null)
|
||||
{
|
||||
HandleDynamicFOV();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDynamicFOV()
|
||||
{
|
||||
float targetFOV = baseFOV;
|
||||
|
||||
// Nếu đang di chuyển và nhấn giữ nút Sprint
|
||||
if (inputReader.MoveInput.magnitude > 0.1f && inputReader.IsSprintHeld)
|
||||
{
|
||||
targetFOV = sprintFOV;
|
||||
}
|
||||
|
||||
// Làm mượt quá trình thay đổi FOV
|
||||
cam.fieldOfView = Mathf.Lerp(cam.fieldOfView, targetFOV, fovSmoothTime * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void HandleShake()
|
||||
{
|
||||
shakeTimer -= Time.deltaTime;
|
||||
|
||||
// Cường độ rung giảm dần theo thời gian
|
||||
float currentIntensity = (shakeTimer / shakeDuration) * shakeIntensity;
|
||||
|
||||
// Dùng Perlin Noise để tạo rung động mượt mà
|
||||
float shakeX = (Mathf.PerlinNoise(Time.time * 25f, 0f) - 0.5f) * 2f;
|
||||
float shakeY = (Mathf.PerlinNoise(0f, Time.time * 25f) - 0.5f) * 2f;
|
||||
float shakeZ = (Mathf.PerlinNoise(Time.time * 25f, Time.time * 25f) - 0.5f) * 2f;
|
||||
|
||||
shakeOffset = new Vector3(shakeX, shakeY, shakeZ) * currentIntensity;
|
||||
}
|
||||
|
||||
public void Shake(float intensity, float duration)
|
||||
{
|
||||
shakeIntensity = intensity;
|
||||
shakeDuration = duration;
|
||||
shakeTimer = duration;
|
||||
}
|
||||
|
||||
private void HandleCharacterFading(float currentDistance)
|
||||
{
|
||||
// Tính độ mờ dựa trên khoảng cách
|
||||
float alpha = Mathf.InverseLerp(fullyHiddenDistance, minVisibleDistance, currentDistance);
|
||||
|
||||
foreach (var renderer in characterRenderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
Color color = renderer.material.color;
|
||||
color.a = alpha;
|
||||
renderer.material.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTransparency(Vector3 focusPosition)
|
||||
{
|
||||
Vector3 direction = focusPosition - transform.position;
|
||||
float distanceToPlayer = direction.magnitude;
|
||||
RaycastHit hit;
|
||||
|
||||
// Bắn một tia từ Camera đến Nhân vật
|
||||
if (Physics.Raycast(transform.position, direction.normalized, out hit, distanceToPlayer, transparencyLayers))
|
||||
{
|
||||
Renderer renderer = hit.collider.GetComponent<Renderer>();
|
||||
if (renderer != null && renderer != lastFadedRenderer)
|
||||
{
|
||||
// Nếu chạm vật mới, khôi phục vật cũ
|
||||
ResetLastRenderer();
|
||||
|
||||
// Lưu thông tin vật mới và làm mờ
|
||||
lastFadedRenderer = renderer;
|
||||
originalColor = renderer.material.color;
|
||||
Color fadedColor = originalColor;
|
||||
fadedColor.a = fadeAlpha;
|
||||
|
||||
// Lưu ý: Material cần hỗ trợ Transparency (Surface Type: Transparent trong URP)
|
||||
renderer.material.color = fadedColor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nếu không chạm gì, khôi phục vật cũ
|
||||
ResetLastRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetLastRenderer()
|
||||
{
|
||||
if (lastFadedRenderer != null)
|
||||
{
|
||||
lastFadedRenderer.material.color = originalColor;
|
||||
lastFadedRenderer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public Quaternion PlanarRotation => Quaternion.Euler(0f, rotationY, 0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3a2f40f0d755824f91dfa62616cd6fc
|
||||
3
Assets/Scripts/Debug.meta
Normal file
3
Assets/Scripts/Debug.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9477ecbb64ef4d9c8863fb16d2c4bc96
|
||||
timeCreated: 1773383891
|
||||
76
Assets/Scripts/Debug/PlayerDebugProvider.cs
Normal file
76
Assets/Scripts/Debug/PlayerDebugProvider.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerDebugProvider : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private PlayerStateMachine stateMachine;
|
||||
[Tooltip("Kéo cái Canvas (World Space) bạn đã thiết kế vào đây")]
|
||||
[SerializeField] private GameObject debugCanvas;
|
||||
|
||||
[Header("Follow Settings")]
|
||||
[SerializeField] private Vector3 followOffset = new Vector3(1.5f, 2f, 0f);
|
||||
[SerializeField] private float smoothTime = 0.15f;
|
||||
[SerializeField] private bool lookAtCamera = true;
|
||||
|
||||
// Các thuộc tính Public để bạn truy cập từ Script UI của bạn
|
||||
public string CurrentState => stateMachine != null ? stateMachine.CurrentStateName : "N/A";
|
||||
public string GroundedStatus => (stateMachine != null && stateMachine.IsGrounded) ? "YES" : "NO";
|
||||
public float HorizontalSpeed => stateMachine != null ? new Vector3(stateMachine.Controller.velocity.x, 0, stateMachine.Controller.velocity.z).magnitude : 0f;
|
||||
public float VerticalSpeed => stateMachine != null ? stateMachine.VelocityY : 0f;
|
||||
public Vector2 MoveInput => stateMachine != null ? stateMachine.Input.MoveInput : Vector2.zero;
|
||||
public bool IsSprinting => stateMachine != null ? stateMachine.Input.IsSprintHeld : false;
|
||||
public string TargetInteractable => stateMachine != null ? (stateMachine.GetInteractable()?.InteractionPrompt ?? "None") : "N/A";
|
||||
|
||||
private Vector3 currentVelocity;
|
||||
private Transform cameraTransform;
|
||||
private bool isVisible = true;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (stateMachine == null) stateMachine = GetComponent<PlayerStateMachine>();
|
||||
cameraTransform = Camera.main?.transform;
|
||||
|
||||
if (debugCanvas != null) debugCanvas.SetActive(isVisible);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Toggle Visibility (Ctrl + Shift + B) sử dụng New Input System
|
||||
if (Keyboard.current != null)
|
||||
{
|
||||
bool ctrl = Keyboard.current.leftCtrlKey.isPressed || Keyboard.current.rightCtrlKey.isPressed;
|
||||
bool shift = Keyboard.current.leftShiftKey.isPressed || Keyboard.current.rightShiftKey.isPressed;
|
||||
bool bDown = Keyboard.current.bKey.wasPressedThisFrame;
|
||||
|
||||
if (ctrl && shift && bDown)
|
||||
{
|
||||
isVisible = !isVisible;
|
||||
if (debugCanvas != null) debugCanvas.SetActive(isVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!isVisible || debugCanvas == null) return;
|
||||
|
||||
// 1. Damping Follow: UI đuổi theo Player
|
||||
Vector3 targetPos = transform.position + followOffset;
|
||||
debugCanvas.transform.position = Vector3.SmoothDamp(
|
||||
debugCanvas.transform.position,
|
||||
targetPos,
|
||||
ref currentVelocity,
|
||||
smoothTime
|
||||
);
|
||||
|
||||
// 2. Billboard: Luôn nhìn về Camera
|
||||
if (lookAtCamera && cameraTransform != null)
|
||||
{
|
||||
debugCanvas.transform.LookAt(debugCanvas.transform.position + cameraTransform.forward);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Debug/PlayerDebugProvider.cs.meta
Normal file
2
Assets/Scripts/Debug/PlayerDebugProvider.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf6aff0b7e11d41439ac80f4963a0795
|
||||
48
Assets/Scripts/EnvironmentScanner.cs
Normal file
48
Assets/Scripts/EnvironmentScanner.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class EnvironmentScanner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Vector3 forwardRayOffset = new Vector3(0, 2.5f, 0);
|
||||
[SerializeField] float forwardRayLength = 10f;
|
||||
[SerializeField] LayerMask obstacleLayer;
|
||||
[SerializeField] float heightRayLength;
|
||||
|
||||
public ObstacleHitInfo ObstacleCheck()
|
||||
{
|
||||
var hitData = new ObstacleHitInfo();
|
||||
|
||||
var forwardOrigin = transform.position + forwardRayOffset;
|
||||
|
||||
hitData.forwardHitFound = Physics.Raycast(transform.position + forwardRayOffset,
|
||||
transform.forward,
|
||||
out hitData.forwardHit,
|
||||
forwardRayLength, obstacleLayer
|
||||
);
|
||||
|
||||
Debug.DrawRay(forwardOrigin, transform.forward * forwardRayLength, (hitData.forwardHitFound) ? Color.red : Color.green);
|
||||
|
||||
if (hitData.forwardHitFound)
|
||||
{
|
||||
var heightOrigin = hitData.forwardHit.point + Vector3.up * heightRayLength;
|
||||
hitData.heightHitFound = Physics.Raycast(heightOrigin, Vector3.down,
|
||||
out hitData.heightHit,
|
||||
heightRayLength, obstacleLayer);
|
||||
|
||||
Debug.DrawRay(heightOrigin, Vector3.down * heightRayLength, (hitData.heightHitFound) ? Color.red : Color.green);
|
||||
|
||||
}
|
||||
return hitData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct ObstacleHitInfo
|
||||
{
|
||||
public RaycastHit forwardHit;
|
||||
public RaycastHit heightHit;
|
||||
public bool forwardHitFound;
|
||||
public bool heightHitFound;
|
||||
}
|
||||
2
Assets/Scripts/EnvironmentScanner.cs.meta
Normal file
2
Assets/Scripts/EnvironmentScanner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 169e37d35fede30409266070c88b118f
|
||||
@@ -1,9 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a0c9218a650547d98138cd835033977
|
||||
guid: b562adae77c550e4db1edcabf68b0530
|
||||
folderAsset: yes
|
||||
timeCreated: 1484670163
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
170
Assets/Scripts/GameSetup/CharacterAutoSetup.cs
Normal file
170
Assets/Scripts/GameSetup/CharacterAutoSetup.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using UnityEngine;
|
||||
using System.Text;
|
||||
|
||||
namespace OnlyScove.Scripts.GameSetup
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class CharacterAutoSetup : MonoBehaviour
|
||||
{
|
||||
[Header("Manual Overrides (If Detection Fails)")]
|
||||
[SerializeField] private float defaultHeight = 1.8f;
|
||||
[SerializeField] private float defaultShoulderWidth = 0.4f;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private Transform modelRoot;
|
||||
[SerializeField] private bool autoDetectOnStart = true;
|
||||
[SerializeField] private float zCenterOffset = 0.05f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoDetectOnStart)
|
||||
{
|
||||
ApplyAutoSetup();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Apply Auto Setup")]
|
||||
public void ApplyAutoSetup()
|
||||
{
|
||||
CharacterController controller = GetComponent<CharacterController>();
|
||||
PlayerStateMachine stateMachine = GetComponent<PlayerStateMachine>();
|
||||
Animator animator = GetComponentInChildren<Animator>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"<color=#4FC3F7><b>[AUTO-SETUP REPORT]</b></color> Character: <b>{gameObject.name}</b>");
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
|
||||
// 1. HEIGHT DETECTION
|
||||
float finalHeight = defaultHeight;
|
||||
string heightMethod = "Default Fallback";
|
||||
|
||||
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Head) != null)
|
||||
{
|
||||
heightMethod = "Humanoid Bones (Feet to Head)";
|
||||
Transform head = animator.GetBoneTransform(HumanBodyBones.Head);
|
||||
// We measure from the local Y=0 (feet) to the head bone and add 10% for the skull/hair
|
||||
float headHeight = transform.InverseTransformPoint(head.position).y;
|
||||
finalHeight = headHeight * 1.12f; // 12% extra for the top of the skull
|
||||
}
|
||||
else
|
||||
{
|
||||
heightMethod = "Local Mesh Bounds";
|
||||
Bounds localBounds = GetRelativeBounds();
|
||||
if (localBounds.size.y > 0) finalHeight = localBounds.size.y;
|
||||
}
|
||||
|
||||
sb.AppendLine(string.Format("<b>1. HEIGHT:</b> {0:F3}m ({1}) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, heightMethod, finalHeight));
|
||||
|
||||
// 2. RADIUS DETECTION
|
||||
float shoulderWidth = defaultShoulderWidth;
|
||||
string radiusMethod = "Default Fallback";
|
||||
|
||||
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Hips) != null)
|
||||
{
|
||||
radiusMethod = "Humanoid Bones (Shoulders)";
|
||||
Transform leftArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||
Transform rightArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
||||
|
||||
if (leftArm != null && rightArm != null)
|
||||
{
|
||||
float distance = Vector3.Distance(leftArm.position, rightArm.position);
|
||||
shoulderWidth = distance * 1.2f; // Add 20% for arm/shoulder thickness
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
radiusMethod = "Bounds Width Fallback";
|
||||
Bounds b = GetRelativeBounds();
|
||||
shoulderWidth = b.size.x * 0.25f;
|
||||
}
|
||||
|
||||
float finalRadius = shoulderWidth / 2f;
|
||||
sb.AppendLine(string.Format("<b>2. RADIUS:</b> {0:F3}m ({1}) [/ 2] ➔ <color=#81C784>{2:F3}m</color>", shoulderWidth, radiusMethod, finalRadius));
|
||||
|
||||
// 3. COLLISION PRECISION
|
||||
float skinWidth = finalRadius * 0.10f;
|
||||
sb.AppendLine(string.Format("<b>3. SKIN WIDTH:</b> {0:F3}m (Radius) [x 0.10] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, skinWidth));
|
||||
|
||||
// 4. CAPSULE CENTER
|
||||
// Center Y = (Height / 2) + SkinWidth
|
||||
float centerY = (finalHeight / 2f) + skinWidth;
|
||||
sb.AppendLine(string.Format("<b>4. CENTER Y:</b> ({0:F3}m / 2) + {1:F3}m (Skin) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, skinWidth, centerY));
|
||||
sb.AppendLine(string.Format("<b>5. CENTER Z:</b> [Fixed Offset] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
||||
|
||||
// 5. MOVEMENT CONSTRAINTS
|
||||
float stepOffset = finalHeight * 0.15f;
|
||||
sb.AppendLine(string.Format("<b>6. STEP OFFSET:</b> {0:F3}m (Height) [x 0.15] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, stepOffset));
|
||||
|
||||
// 6. GROUND CHECK (STATE MACHINE)
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
if (stateMachine != null)
|
||||
{
|
||||
float groundCheckRadius = finalRadius * 0.6f;
|
||||
float groundCheckOffsetY = finalHeight / 24f;
|
||||
Vector3 groundCheckOffset = new Vector3(0, groundCheckOffsetY, zCenterOffset);
|
||||
|
||||
stateMachine.SetGroundCheck(groundCheckRadius, groundCheckOffset);
|
||||
|
||||
sb.AppendLine("<b>7. GROUND CHECK SETUP:</b>");
|
||||
sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, groundCheckRadius));
|
||||
sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, groundCheckOffsetY));
|
||||
sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
||||
}
|
||||
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
|
||||
// Apply to Controller
|
||||
controller.height = finalHeight;
|
||||
controller.radius = finalRadius;
|
||||
controller.skinWidth = skinWidth;
|
||||
controller.center = new Vector3(0, centerY, zCenterOffset);
|
||||
controller.slopeLimit = 45f;
|
||||
controller.stepOffset = stepOffset;
|
||||
controller.minMoveDistance = 0.001f;
|
||||
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private Bounds GetRelativeBounds()
|
||||
{
|
||||
Transform targetRoot = modelRoot != null ? modelRoot : transform;
|
||||
Renderer[] renderers = targetRoot.GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (renderers.Length == 0) return new Bounds(Vector3.zero, Vector3.zero);
|
||||
|
||||
// Using local bounds of SkinnedMeshRenderers for better accuracy on animated characters
|
||||
Bounds combinedLocalBounds = new Bounds();
|
||||
bool first = true;
|
||||
|
||||
foreach (Renderer renderer in renderers)
|
||||
{
|
||||
if (renderer is ParticleSystemRenderer) continue;
|
||||
|
||||
Bounds localB;
|
||||
if (renderer is SkinnedMeshRenderer smr)
|
||||
{
|
||||
localB = smr.localBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For static meshes, convert world bounds back to local root space
|
||||
Vector3 min = transform.InverseTransformPoint(renderer.bounds.min);
|
||||
Vector3 max = transform.InverseTransformPoint(renderer.bounds.max);
|
||||
localB = new Bounds((min + max) / 2f, max - min);
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
combinedLocalBounds = localB;
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedLocalBounds.Encapsulate(localB);
|
||||
}
|
||||
}
|
||||
|
||||
return combinedLocalBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta
Normal file
2
Assets/Scripts/GameSetup/CharacterAutoSetup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e16a6690e589f0449ad89a6bf508ab62
|
||||
26
Assets/Scripts/GameSetup/CharacterSetupSettings.cs
Normal file
26
Assets/Scripts/GameSetup/CharacterSetupSettings.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts.GameSetup
|
||||
{
|
||||
[CreateAssetMenu(fileName = "CharacterSetupSettings", menuName = "Setup/Character Setup Settings")]
|
||||
public class CharacterSetupSettings : ScriptableObject
|
||||
{
|
||||
[Header("Movement Constraints")]
|
||||
public float slopeLimit = 45f;
|
||||
[Range(0.01f, 0.5f)] public float stepHeightRatio = 0.15f; // Step offset as % of height
|
||||
|
||||
[Header("Precision & Collision")]
|
||||
public float skinWidthRatio = 0.1f; // Skin width as % of radius
|
||||
public float minMoveDistance = 0.001f;
|
||||
|
||||
[Header("Dimension Multipliers")]
|
||||
[Tooltip("Multiplies the detected shoulder width to define Radius.")]
|
||||
public float radiusMultiplier = 0.8f;
|
||||
[Tooltip("Multiplies the detected bounding box height.")]
|
||||
public float heightMultiplier = 1.0f;
|
||||
|
||||
[Header("Center Offset")]
|
||||
[Tooltip("Y-axis offset for the center of the capsule (0.5 means exact middle).")]
|
||||
public float centerYRatio = 0.5f;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta
Normal file
2
Assets/Scripts/GameSetup/CharacterSetupSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d44cb4bd45c0e24bb3d8196a137db00
|
||||
3
Assets/Scripts/Interface.meta
Normal file
3
Assets/Scripts/Interface.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1398e4608a9406cb68d769d6bc9a3ec
|
||||
timeCreated: 1773397974
|
||||
8
Assets/Scripts/Interface/IInteractable.cs
Normal file
8
Assets/Scripts/Interface/IInteractable.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public interface IInteractable
|
||||
{
|
||||
string InteractionPrompt { get; }
|
||||
void OnInteract(PlayerStateMachine player);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Interface/IInteractable.cs.meta
Normal file
2
Assets/Scripts/Interface/IInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f001a3bb0fe5e954ea724ded3158209d
|
||||
8
Assets/Scripts/Optimization.meta
Normal file
8
Assets/Scripts/Optimization.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6d134b82fd19fc4aa38896d5e45efed
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Assets/Scripts/Optimization/AutoPlayerStateMachine.cs
Normal file
49
Assets/Scripts/Optimization/AutoPlayerStateMachine.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
/// <summary>
|
||||
/// A version of PlayerStateMachine that simulates constant forward or backward input.
|
||||
/// </summary>
|
||||
public class AutoPlayerStateMachine : PlayerStateMachine
|
||||
{
|
||||
[Header("Auto Pilot Settings")]
|
||||
public bool alwaysMoveForward = true;
|
||||
public bool alwaysRun = true;
|
||||
public bool isRed = false; // New property to differentiate movement direction
|
||||
|
||||
private class FakeInputReader : InputReader
|
||||
{
|
||||
public Vector2 ForcedMove { get; set; }
|
||||
public bool ForcedSprint { get; set; }
|
||||
|
||||
public override Vector2 MoveInput => ForcedMove;
|
||||
public override bool IsSprintHeld => ForcedSprint;
|
||||
}
|
||||
|
||||
private FakeInputReader fakeInput;
|
||||
|
||||
public override InputReader Input => fakeInput;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
fakeInput = gameObject.AddComponent<FakeInputReader>();
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (fakeInput != null)
|
||||
{
|
||||
// Logic updated: isRed moves backward (Vector2.down), others move forward (Vector2.up)
|
||||
fakeInput.ForcedMove = (isRed)
|
||||
? (alwaysMoveForward ? Vector2.down : Vector2.zero)
|
||||
: (alwaysMoveForward ? Vector2.up : Vector2.zero);
|
||||
|
||||
fakeInput.ForcedSprint = alwaysRun;
|
||||
}
|
||||
|
||||
base.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbe8532b2e328a249bf61ae957c92486
|
||||
129
Assets/Scripts/Optimization/JobsMovementManager.cs
Normal file
129
Assets/Scripts/Optimization/JobsMovementManager.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using UnityEngine;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using UnityEngine.Jobs;
|
||||
using Unity.Burst;
|
||||
|
||||
namespace Elbyss.Optimization
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages 10,000+ objects using C# Job System and Burst Compiler.
|
||||
/// This avoids the overhead of 10,000 individual Update() calls.
|
||||
/// </summary>
|
||||
public class JobsMovementManager : MonoBehaviour
|
||||
{
|
||||
[Header("Spawn Settings")]
|
||||
public GameObject prefab;
|
||||
public int objectCount = 10000;
|
||||
public float spacing = 1.5f;
|
||||
|
||||
[Header("Movement Settings")]
|
||||
public float speed = 5f;
|
||||
|
||||
private TransformAccessArray transformAccessArray;
|
||||
private NativeArray<Vector3> directions;
|
||||
private bool isInitialized = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Optional: Start automatically or via Context Menu
|
||||
// Setup(objectCount);
|
||||
}
|
||||
|
||||
[ContextMenu("Setup 10k Objects")]
|
||||
public void InitialSetup()
|
||||
{
|
||||
Setup(objectCount);
|
||||
}
|
||||
|
||||
public void Setup(int count)
|
||||
{
|
||||
if (isInitialized) Cleanup();
|
||||
|
||||
objectCount = count;
|
||||
Transform[] transforms = new Transform[objectCount];
|
||||
directions = new NativeArray<Vector3>(objectCount, Allocator.Persistent);
|
||||
|
||||
int rowSize = Mathf.CeilToInt(Mathf.Sqrt(objectCount));
|
||||
|
||||
for (int i = 0; i < objectCount; i++)
|
||||
{
|
||||
float x = (i % rowSize) * spacing;
|
||||
float z = (i / rowSize) * spacing;
|
||||
Vector3 pos = transform.position + new Vector3(x, 0, z);
|
||||
|
||||
GameObject go = Instantiate(prefab, pos, Quaternion.identity, this.transform);
|
||||
transforms[i] = go.transform;
|
||||
|
||||
// Set alternating directions
|
||||
directions[i] = (i % 2 == 0) ? Vector3.forward : Vector3.back;
|
||||
|
||||
// CRITICAL OPTIMIZATION: Disable components that are too heavy for 10k objects
|
||||
if (go.TryGetComponent<Animator>(out var anim)) anim.enabled = false;
|
||||
if (go.TryGetComponent<CharacterController>(out var cc)) cc.enabled = false;
|
||||
|
||||
// Disable all other custom scripts
|
||||
MonoBehaviour[] scripts = go.GetComponents<MonoBehaviour>();
|
||||
foreach (var s in scripts)
|
||||
{
|
||||
if (s != this) s.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
transformAccessArray = new TransformAccessArray(transforms);
|
||||
isInitialized = true;
|
||||
Debug.Log($"Initialized {objectCount} objects with Job System.");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized) return;
|
||||
|
||||
// Create the movement job
|
||||
var job = new MovementJob
|
||||
{
|
||||
DeltaTime = Time.deltaTime,
|
||||
Speed = speed,
|
||||
Directions = directions
|
||||
};
|
||||
|
||||
// Schedule the job to run in parallel on all available CPU cores
|
||||
// transformAccessArray allows the job to modify Transform data directly
|
||||
JobHandle handle = job.Schedule(transformAccessArray);
|
||||
|
||||
// This ensures the job is finished before the frame ends
|
||||
// In a real scenario, you might want to call Complete() in LateUpdate or next frame
|
||||
// but for simple movement, scheduling and completing in Update is fine.
|
||||
handle.Complete();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
if (transformAccessArray.isCreated) transformAccessArray.Dispose();
|
||||
if (directions.IsCreated) directions.Dispose();
|
||||
isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile] // This attribute tells the Burst compiler to optimize this job into machine code
|
||||
struct MovementJob : IJobParallelForTransform
|
||||
{
|
||||
public float DeltaTime;
|
||||
public float Speed;
|
||||
[ReadOnly] public NativeArray<Vector3> Directions;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
// Directly modify the transform position in parallel
|
||||
transform.position += Directions[index] * Speed * DeltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/JobsMovementManager.cs.meta
Normal file
2
Assets/Scripts/Optimization/JobsMovementManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41899df442467714dbee462ae451773a
|
||||
153
Assets/Scripts/Optimization/MassiveSpawner.cs
Normal file
153
Assets/Scripts/Optimization/MassiveSpawner.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
// using GPUInstancerPro;
|
||||
// using Unity.Burst;
|
||||
// using Unity.Collections;
|
||||
// using Unity.Jobs;
|
||||
// using Unity.Mathematics;
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace Elbyss.Optimization
|
||||
// {
|
||||
// public class MassiveSpawner : MonoBehaviour
|
||||
// {
|
||||
// [Header("Spawn Settings")]
|
||||
// public GameObject prefab;
|
||||
// public GPUIProfile profile;
|
||||
// public int instanceCount = 100000;
|
||||
// public float spacing = 1.5f;
|
||||
//
|
||||
// [Header("Update Settings")]
|
||||
// public bool runUpdate = true;
|
||||
// public float movementSpeed = 1.0f;
|
||||
// public float amplitude = 2.0f;
|
||||
//
|
||||
// private int _rendererKey;
|
||||
// private NativeArray<Matrix4x4> _matrices;
|
||||
// private JobHandle _jobHandle;
|
||||
// private bool _isInitialized;
|
||||
//
|
||||
// private void OnEnable()
|
||||
// {
|
||||
// Initialize();
|
||||
// }
|
||||
//
|
||||
// private void OnDisable()
|
||||
// {
|
||||
// Dispose();
|
||||
// }
|
||||
//
|
||||
// private void OnValidate()
|
||||
// {
|
||||
// if (Application.isPlaying && _isInitialized)
|
||||
// {
|
||||
// // Re-initialize if count changes during play (optional, but good for testing)
|
||||
// Initialize();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void Initialize()
|
||||
// {
|
||||
// Dispose();
|
||||
// if (prefab == null) return;
|
||||
//
|
||||
// if (GPUICoreAPI.RegisterRenderer(this, prefab, profile, out _rendererKey))
|
||||
// {
|
||||
// _matrices = new NativeArray<Matrix4x4>(instanceCount, Allocator.Persistent);
|
||||
//
|
||||
// // Initial generation
|
||||
// GenerateMatrices(0);
|
||||
// _jobHandle.Complete();
|
||||
// GPUICoreAPI.SetTransformBufferData(_rendererKey, _matrices);
|
||||
//
|
||||
// _isInitialized = true;
|
||||
// Debug.Log($"[MassiveSpawner] Registered {instanceCount} instances with key {_rendererKey}");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogError("[MassiveSpawner] Failed to register renderer!");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void Update()
|
||||
// {
|
||||
// if (!_isInitialized || _rendererKey == 0) return;
|
||||
//
|
||||
// if (runUpdate)
|
||||
// {
|
||||
// // Complete previous frame's work if any
|
||||
// _jobHandle.Complete();
|
||||
//
|
||||
// // Apply updated matrices to GPUI
|
||||
// GPUICoreAPI.SetTransformBufferData(_rendererKey, _matrices);
|
||||
//
|
||||
// // Schedule next frame's work
|
||||
// GenerateMatrices(Time.time);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void GenerateMatrices(float time)
|
||||
// {
|
||||
// int side = Mathf.CeilToInt(Mathf.Sqrt(instanceCount));
|
||||
//
|
||||
// var job = new MatrixUpdateJob
|
||||
// {
|
||||
// matrices = _matrices,
|
||||
// side = side,
|
||||
// spacing = spacing,
|
||||
// time = time,
|
||||
// speed = movementSpeed,
|
||||
// amplitude = amplitude,
|
||||
// origin = transform.position
|
||||
// };
|
||||
//
|
||||
// _jobHandle = job.Schedule(instanceCount, 64);
|
||||
// }
|
||||
//
|
||||
// public void Dispose()
|
||||
// {
|
||||
// _jobHandle.Complete();
|
||||
// if (_rendererKey != 0)
|
||||
// {
|
||||
// GPUICoreAPI.DisposeRenderer(_rendererKey);
|
||||
// _rendererKey = 0;
|
||||
// }
|
||||
//
|
||||
// if (_matrices.IsCreated)
|
||||
// {
|
||||
// _matrices.Dispose();
|
||||
// }
|
||||
// _isInitialized = false;
|
||||
// }
|
||||
//
|
||||
// [BurstCompile]
|
||||
// struct MatrixUpdateJob : IJobParallelFor
|
||||
// {
|
||||
// public NativeArray<Matrix4x4> matrices;
|
||||
// public int side;
|
||||
// public float spacing;
|
||||
// public float time;
|
||||
// public float speed;
|
||||
// public float amplitude;
|
||||
// public Vector3 origin;
|
||||
//
|
||||
// public void Execute(int index)
|
||||
// {
|
||||
// int x = index % side;
|
||||
// int z = index / side;
|
||||
//
|
||||
// float xPos = x * spacing;
|
||||
// float zPos = z * spacing;
|
||||
//
|
||||
// // Add some animation to prove it's updating
|
||||
// float yPos = math.sin(time * speed + (xPos + zPos) * 0.1f) * amplitude;
|
||||
//
|
||||
// Vector3 pos = origin + new Vector3(xPos, yPos, zPos);
|
||||
//
|
||||
// // Simple rotation based on time
|
||||
// float angle = (time * speed * 10f + index) % 360f;
|
||||
// Quaternion rot = Quaternion.Euler(0, angle, 0);
|
||||
//
|
||||
// matrices[index] = Matrix4x4.TRS(pos, rot, Vector3.one);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
2
Assets/Scripts/Optimization/MassiveSpawner.cs.meta
Normal file
2
Assets/Scripts/Optimization/MassiveSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50831f537cbccac4c9bf4067b6b158c7
|
||||
34
Assets/Scripts/Optimization/PerformanceHUD.cs
Normal file
34
Assets/Scripts/Optimization/PerformanceHUD.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
namespace Elbyss.Optimization
|
||||
{
|
||||
public class PerformanceHUD : MonoBehaviour
|
||||
{
|
||||
private float deltaTime = 0.0f;
|
||||
private string displayFormat = "{0:0.0} ms ({1:0.} fps)";
|
||||
|
||||
private void Update()
|
||||
{
|
||||
deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
int w = Screen.width, h = Screen.height;
|
||||
|
||||
GUIStyle style = new GUIStyle();
|
||||
|
||||
Rect rect = new Rect(10, 10, w, h * 2 / 100);
|
||||
style.alignment = TextAnchor.UpperLeft;
|
||||
style.fontSize = h * 2 / 50;
|
||||
style.normal.textColor = new Color(0.0f, 1.0f, 0.5f, 1.0f);
|
||||
|
||||
float msec = deltaTime * 1000.0f;
|
||||
float fps = 1.0f / deltaTime;
|
||||
string text = string.Format(displayFormat, msec, fps);
|
||||
|
||||
GUI.Label(rect, text, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/PerformanceHUD.cs.meta
Normal file
2
Assets/Scripts/Optimization/PerformanceHUD.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c9a374a3ab089d41a6784b1ffad3b6f
|
||||
85
Assets/Scripts/Optimization/StressTestSpawner.cs
Normal file
85
Assets/Scripts/Optimization/StressTestSpawner.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using OnlyScove.Scripts;
|
||||
|
||||
namespace Elbyss.Optimization
|
||||
{
|
||||
public class StressTestSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Spawn Settings")]
|
||||
public GameObject prefabToSpawn;
|
||||
public int spawnLimit = 1000;
|
||||
public int spawnsPerFrame = 10;
|
||||
public float spacing = 2.0f;
|
||||
|
||||
[Header("Testing Mode")]
|
||||
public bool useAutoStateMachine = true;
|
||||
|
||||
[Header("Optimization Options")]
|
||||
public bool stripHeavyComponents = false;
|
||||
|
||||
private List<GameObject> spawnedObjects = new List<GameObject>();
|
||||
private int currentSpawnCount = 0;
|
||||
private bool isSpawning = false;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isSpawning && currentSpawnCount < spawnLimit)
|
||||
{
|
||||
for (int i = 0; i < spawnsPerFrame && currentSpawnCount < spawnLimit; i++)
|
||||
{
|
||||
SpawnObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Start Stress Test")]
|
||||
public void StartStressTest() => isSpawning = true;
|
||||
|
||||
[ContextMenu("Clear All")]
|
||||
public void ClearAll()
|
||||
{
|
||||
foreach (var obj in spawnedObjects) if (obj != null) Destroy(obj);
|
||||
spawnedObjects.Clear();
|
||||
currentSpawnCount = 0;
|
||||
isSpawning = false;
|
||||
}
|
||||
|
||||
private void SpawnObject()
|
||||
{
|
||||
if (prefabToSpawn == null) return;
|
||||
|
||||
int rowSize = Mathf.CeilToInt(Mathf.Sqrt(spawnLimit));
|
||||
float x = (currentSpawnCount % rowSize) * spacing;
|
||||
float z = (currentSpawnCount / rowSize) * spacing;
|
||||
Vector3 spawnPos = transform.position + new Vector3(x, 0, z);
|
||||
|
||||
GameObject newObj = Instantiate(prefabToSpawn, spawnPos, transform.rotation);
|
||||
|
||||
if (useAutoStateMachine)
|
||||
{
|
||||
var realInput = newObj.GetComponent<InputReader>();
|
||||
if (realInput != null) realInput.enabled = false;
|
||||
|
||||
var originalSM = newObj.GetComponent<PlayerStateMachine>();
|
||||
if (originalSM != null)
|
||||
{
|
||||
DestroyImmediate(originalSM);
|
||||
var autoSM = newObj.AddComponent<AutoPlayerStateMachine>();
|
||||
autoSM.alwaysMoveForward = true;
|
||||
autoSM.alwaysRun = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stripHeavyComponents)
|
||||
{
|
||||
if (newObj.TryGetComponent<Animator>(out var anim)) anim.enabled = false;
|
||||
if (newObj.TryGetComponent<CharacterController>(out var cc)) cc.enabled = false;
|
||||
if (newObj.TryGetComponent<PlayerStateMachine>(out var sm)) sm.enabled = false;
|
||||
}
|
||||
|
||||
spawnedObjects.Add(newObj);
|
||||
currentSpawnCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Optimization/StressTestSpawner.cs.meta
Normal file
2
Assets/Scripts/Optimization/StressTestSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a7c5ef310b7f354685dc6706be2d530
|
||||
8
Assets/Scripts/Player Controller.meta
Normal file
8
Assets/Scripts/Player Controller.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e0fd09e39d1b90458b7097e555a9f3f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
89
Assets/Scripts/Player Controller/InputReader.cs
Normal file
89
Assets/Scripts/Player Controller/InputReader.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class InputReader : MonoBehaviour
|
||||
{
|
||||
// Continuous Inputs
|
||||
public virtual Vector2 MoveInput { get; protected set; }
|
||||
public virtual Vector2 LookInput { get; protected set; }
|
||||
public virtual Vector2 ScrollInput { get; protected set; }
|
||||
public virtual bool IsSprintHeld { get; protected set; } // Left Shift
|
||||
public virtual bool IsAttackHeld { get; protected set; } // Left Mouse Button
|
||||
|
||||
// One-shot Events
|
||||
public event Action OnJumpEvent; // Space
|
||||
public event Action OnDodgeEvent; // Right Mouse Button (RMB)
|
||||
public event Action OnAttackEvent; // Left Mouse Button (LMB)
|
||||
public event Action OnCrouchEvent; // C Key
|
||||
public event Action OnInteractEvent; // E Key
|
||||
public event Action OnNextInteractEvent; // R Key
|
||||
public event Action OnPreviousInteractEvent; // Q Key
|
||||
|
||||
public void OnAttack(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed)
|
||||
{
|
||||
OnAttackEvent?.Invoke();
|
||||
IsAttackHeld = true;
|
||||
}
|
||||
if (context.canceled)
|
||||
{
|
||||
IsAttackHeld = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnMove(InputAction.CallbackContext context)
|
||||
{
|
||||
MoveInput = context.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
public void OnLook(InputAction.CallbackContext context)
|
||||
{
|
||||
LookInput = context.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
public void OnScroll(InputAction.CallbackContext context)
|
||||
{
|
||||
ScrollInput = context.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
public void OnSprint(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) IsSprintHeld = true;
|
||||
if (context.canceled) IsSprintHeld = false;
|
||||
}
|
||||
|
||||
public void OnJump(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnJumpEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnDodgeOrThrust(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnDodgeEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnCrouch(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnCrouchEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnInteract(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnInteractEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnNext(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnNextInteractEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnPrevious(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed) OnPreviousInteractEvent?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/InputReader.cs.meta
Normal file
2
Assets/Scripts/Player Controller/InputReader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5962d8f2c8e40e240a4a4907c7b539fa
|
||||
12
Assets/Scripts/Player Controller/ParkourAction.cs
Normal file
12
Assets/Scripts/Player Controller/ParkourAction.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Game_Definition
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ParkourAction", menuName = "Parkour System/New Parkour Action")]
|
||||
public class ParkourAction : ScriptableObject
|
||||
{
|
||||
[SerializeField] string animationName;
|
||||
[SerializeField] private float minHeight;
|
||||
[SerializeField] private float maxHeight;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/ParkourAction.cs.meta
Normal file
2
Assets/Scripts/Player Controller/ParkourAction.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 985468dedaf32f44191b2cc29c813c8c
|
||||
59
Assets/Scripts/Player Controller/ParkourController.cs
Normal file
59
Assets/Scripts/Player Controller/ParkourController.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class ParkourController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputReader inputReader;
|
||||
EnvironmentScanner environmentScanner;
|
||||
Animator animator;
|
||||
PlayerController playerController;
|
||||
|
||||
bool inAction;
|
||||
private void Awake()
|
||||
{
|
||||
inputReader = GetComponent<InputReader>();
|
||||
environmentScanner = GetComponent<EnvironmentScanner>();
|
||||
animator = GetComponent<Animator>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
inputReader.OnJumpEvent += HandleParkour;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
inputReader.OnJumpEvent -= HandleParkour;
|
||||
}
|
||||
|
||||
private void HandleParkour()
|
||||
{
|
||||
var hitData = environmentScanner.ObstacleCheck();
|
||||
|
||||
if (hitData.forwardHitFound)
|
||||
{
|
||||
StartCoroutine(DoParkourAction());
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator DoParkourAction()
|
||||
{
|
||||
inAction = true;
|
||||
playerController.SetControl(false);
|
||||
|
||||
animator.CrossFade("Step Up", 0.1f);
|
||||
yield return null;
|
||||
|
||||
var animationState = animator.GetNextAnimatorStateInfo(0);
|
||||
|
||||
yield return new WaitForSeconds(animationState.length);
|
||||
|
||||
playerController.SetControl(true);
|
||||
inAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0407ea3fbe445ac43b4f1ca3077ce283
|
||||
58
Assets/Scripts/Player Controller/PlayerAirDashState.cs
Normal file
58
Assets/Scripts/Player Controller/PlayerAirDashState.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerAirDashState : PlayerBaseState
|
||||
{
|
||||
private readonly int airDashHash = Animator.StringToHash("AirDash");
|
||||
private float dashDuration = 0.2f;
|
||||
private float dashTimer;
|
||||
private Vector3 dashDirection;
|
||||
|
||||
public PlayerAirDashState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
dashTimer = dashDuration;
|
||||
stateMachine.Anim.SetTrigger(airDashHash);
|
||||
|
||||
// Reset vertical velocity so we don't carry falling momentum into the dash
|
||||
stateMachine.VelocityY = 0f;
|
||||
|
||||
// Determine dash direction
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
if (input != Vector2.zero)
|
||||
{
|
||||
dashDirection = new Vector3(input.x, 0f, input.y).normalized;
|
||||
|
||||
if (stateMachine.Cam != null)
|
||||
{
|
||||
dashDirection = stateMachine.Cam.PlanarRotation * dashDirection;
|
||||
}
|
||||
|
||||
stateMachine.transform.rotation = Quaternion.LookRotation(dashDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
dashDirection = stateMachine.transform.forward;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
dashTimer -= deltaTime;
|
||||
|
||||
// Move horizontally, ignoring gravity for this brief moment
|
||||
stateMachine.Controller.Move(dashDirection * stateMachine.DashForce * deltaTime);
|
||||
|
||||
// When the air dash ends, return to falling
|
||||
if (dashTimer <= 0f)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerFallState(stateMachine));
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
public override void Exit() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43c3e5e400fea604fb432f84d5dd9ce1
|
||||
25
Assets/Scripts/Player Controller/PlayerBaseState.cs
Normal file
25
Assets/Scripts/Player Controller/PlayerBaseState.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public abstract class PlayerBaseState
|
||||
{
|
||||
protected PlayerStateMachine stateMachine;
|
||||
|
||||
// Constructor to pass the state machine reference
|
||||
public PlayerBaseState(PlayerStateMachine stateMachine)
|
||||
{
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
// Called once when entering the state
|
||||
public abstract void Enter();
|
||||
|
||||
// Called every frame (equivalent to Update)
|
||||
public abstract void Tick(float deltaTime);
|
||||
|
||||
// Called every physics frame (equivalent to FixedUpdate)
|
||||
public abstract void PhysicsTick(float fixedDeltaTime);
|
||||
|
||||
// Called once before switching to a new state
|
||||
public abstract void Exit();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerBaseState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerBaseState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af622221f1750284885366b02b8bfbba
|
||||
134
Assets/Scripts/Player Controller/PlayerController.cs
Normal file
134
Assets/Scripts/Player Controller/PlayerController.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerController : MonoBehaviour
|
||||
{
|
||||
private static readonly int MoveAmount = Animator.StringToHash("moveAmount");
|
||||
|
||||
[SerializeField] private InputReader inputReader;
|
||||
[SerializeField] private float rotationSpeed = 500f;
|
||||
[SerializeField] private float moveSpeed = 10f;
|
||||
[SerializeField] private float jumpHeight = 2f;
|
||||
[SerializeField] private float animationDamping = 0.2f;
|
||||
[SerializeField] private float groundCheckRadius = 0.2f;
|
||||
[SerializeField] private Vector3 groundCheckOffset;
|
||||
[SerializeField] private LayerMask groundMask;
|
||||
|
||||
CameraController cameraController;
|
||||
Animator animator;
|
||||
private CharacterController characterController;
|
||||
|
||||
Quaternion targetRotation;
|
||||
private float horizontal;
|
||||
private float vertical;
|
||||
bool isGrounded;
|
||||
private bool wasGrounded;
|
||||
private bool hasControl = true;
|
||||
private float ySpeed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Camera.main != null) cameraController = Camera.main.GetComponent<CameraController>();
|
||||
animator = GetComponent<Animator>();
|
||||
characterController = GetComponent<CharacterController>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
inputReader.OnJumpEvent += HandleJump;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
inputReader.OnJumpEvent -= HandleJump;
|
||||
}
|
||||
|
||||
private void HandleJump()
|
||||
{
|
||||
if (isGrounded && hasControl)
|
||||
{
|
||||
// Công thức tính vận tốc nhảy: v = sqrt(h * -2 * g)
|
||||
ySpeed = Mathf.Sqrt(jumpHeight * -2f * Physics.gravity.y);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
horizontal = inputReader.MoveInput.x;
|
||||
vertical = inputReader.MoveInput.y;
|
||||
float moveAmount = Mathf.Clamp01(Math.Abs(horizontal) + Math.Abs(vertical));
|
||||
|
||||
var moveInput = (new Vector3(horizontal, 0, vertical)).normalized;
|
||||
|
||||
var moveDirection = cameraController.PlanarRotation * moveInput;
|
||||
|
||||
if (!hasControl)
|
||||
return;
|
||||
|
||||
wasGrounded = isGrounded;
|
||||
GroundCheck();
|
||||
|
||||
// Phát hiện tiếp đất (Landing)
|
||||
if (isGrounded && !wasGrounded && ySpeed < -1f)
|
||||
{
|
||||
// Rung camera khi tiếp đất mạnh
|
||||
if (cameraController != null)
|
||||
{
|
||||
cameraController.Shake(0.2f, 0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
if (isGrounded && ySpeed < 0)
|
||||
{
|
||||
ySpeed = -2f; // Giữ nhân vật dính xuống mặt đất
|
||||
}
|
||||
else
|
||||
{
|
||||
ySpeed += Physics.gravity.y * Time.deltaTime;
|
||||
}
|
||||
|
||||
var velocity = moveDirection * moveSpeed;
|
||||
velocity.y = ySpeed;
|
||||
|
||||
characterController.Move(velocity * Time.deltaTime);
|
||||
|
||||
if (moveAmount > 0)
|
||||
{
|
||||
targetRotation = Quaternion.LookRotation(moveDirection);
|
||||
|
||||
}
|
||||
|
||||
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
|
||||
Time.deltaTime * rotationSpeed);
|
||||
|
||||
animator.SetFloat(MoveAmount, moveAmount, animationDamping, Time.deltaTime);
|
||||
}
|
||||
|
||||
void GroundCheck()
|
||||
{
|
||||
isGrounded =
|
||||
Physics.CheckSphere(transform.TransformPoint(groundCheckOffset), groundCheckRadius, groundMask);
|
||||
}
|
||||
|
||||
public void SetControl(bool control)
|
||||
{
|
||||
this.hasControl = control;
|
||||
characterController.enabled = hasControl;
|
||||
|
||||
if (!hasControl)
|
||||
{
|
||||
animator.SetFloat(MoveAmount, 0f);
|
||||
targetRotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = new Color(0, 1, 0, 0.5f);
|
||||
Gizmos.DrawSphere(transform.TransformPoint(groundCheckOffset), groundCheckRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd897e8bcaabdc8408bb6aaf7c037537
|
||||
97
Assets/Scripts/Player Controller/PlayerCrouchState.cs
Normal file
97
Assets/Scripts/Player Controller/PlayerCrouchState.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerCrouchState : PlayerBaseState
|
||||
{
|
||||
private readonly int isCrouchingHash = Animator.StringToHash("IsCrouching");
|
||||
private readonly int crouchSpeedHash = Animator.StringToHash("CrouchSpeed");
|
||||
private const float AnimatorDampTime = 0.1f;
|
||||
|
||||
public PlayerCrouchState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Anim.SetBool(isCrouchingHash, true);
|
||||
stateMachine.Input.OnCrouchEvent += OnCrouchToggle;
|
||||
stateMachine.Input.OnDodgeEvent += OnDodge;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
Vector2 moveInput = stateMachine.Input.MoveInput;
|
||||
float currentSpeed = 0f;
|
||||
float animValue = 0f;
|
||||
|
||||
if (moveInput == Vector2.zero)
|
||||
{
|
||||
animValue = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (stateMachine.Input.IsSprintHeld)
|
||||
{
|
||||
currentSpeed = stateMachine.SneakSpeed;
|
||||
animValue = 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSpeed = stateMachine.WalkSpeed;
|
||||
animValue = 1.0f;
|
||||
}
|
||||
|
||||
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y).normalized;
|
||||
|
||||
if (stateMachine.Cam != null)
|
||||
{
|
||||
moveDirection = stateMachine.Cam.PlanarRotation * moveDirection;
|
||||
}
|
||||
|
||||
// Apply horizontal movement
|
||||
Vector3 movement = moveDirection * currentSpeed;
|
||||
|
||||
// Apply gravity
|
||||
if (stateMachine.Controller.isGrounded)
|
||||
{
|
||||
stateMachine.VelocityY = -2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.VelocityY += stateMachine.Gravity * deltaTime;
|
||||
}
|
||||
|
||||
movement.y = stateMachine.VelocityY;
|
||||
|
||||
stateMachine.Controller.Move(movement * deltaTime);
|
||||
|
||||
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
|
||||
stateMachine.transform.rotation = Quaternion.Slerp(
|
||||
stateMachine.transform.rotation,
|
||||
targetRotation,
|
||||
deltaTime * 10f
|
||||
);
|
||||
}
|
||||
|
||||
stateMachine.Anim.SetFloat(crouchSpeedHash, animValue, AnimatorDampTime, deltaTime);
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Anim.SetBool(isCrouchingHash, false);
|
||||
stateMachine.Input.OnCrouchEvent -= OnCrouchToggle;
|
||||
stateMachine.Input.OnDodgeEvent -= OnDodge;
|
||||
}
|
||||
|
||||
private void OnCrouchToggle()
|
||||
{
|
||||
if (stateMachine.Input.MoveInput == Vector2.zero)
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
else
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
|
||||
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82b04241d720e444a937973caed8eb42
|
||||
86
Assets/Scripts/Player Controller/PlayerDashState.cs
Normal file
86
Assets/Scripts/Player Controller/PlayerDashState.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerDashState : PlayerBaseState
|
||||
{
|
||||
private readonly int dashHash = Animator.StringToHash("Dash"); // Trigger parameter
|
||||
private float dashDuration = 0.25f; // How long the burst lasts (tweak as needed)
|
||||
private float dashTimer;
|
||||
private Vector3 dashDirection;
|
||||
|
||||
public PlayerDashState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
dashTimer = dashDuration;
|
||||
|
||||
// Fire the Dash animation trigger
|
||||
stateMachine.Anim.SetTrigger(dashHash);
|
||||
|
||||
stateMachine.Input.OnJumpEvent += OnJump;
|
||||
|
||||
// Determine dash direction based on input, or default to forward if no input
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
if (input != Vector2.zero)
|
||||
{
|
||||
dashDirection = new Vector3(input.x, 0f, input.y).normalized;
|
||||
|
||||
if (stateMachine.Cam != null)
|
||||
{
|
||||
dashDirection = stateMachine.Cam.PlanarRotation * dashDirection;
|
||||
}
|
||||
|
||||
// Instantly snap rotation to face the dash direction
|
||||
stateMachine.transform.rotation = Quaternion.LookRotation(dashDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
dashDirection = stateMachine.transform.forward;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
dashTimer -= deltaTime;
|
||||
|
||||
// Apply high speed for the dash (Burst speed)
|
||||
stateMachine.Controller.Move(dashDirection * stateMachine.SprintSpeed * deltaTime);
|
||||
|
||||
// When the dash finishes, decide the next state
|
||||
if (dashTimer <= 0f)
|
||||
{
|
||||
if (stateMachine.Input.IsSprintHeld && stateMachine.Input.MoveInput != Vector2.zero)
|
||||
{
|
||||
// Kept holding Shift -> Go to Sprint
|
||||
stateMachine.SwitchState(new PlayerRunState(stateMachine));
|
||||
}
|
||||
else if (stateMachine.Input.MoveInput != Vector2.zero)
|
||||
{
|
||||
// Released Shift but still moving -> Go to Walk
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stopped moving -> Go to Idle
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent -= OnJump;
|
||||
}
|
||||
|
||||
private void OnJump()
|
||||
{
|
||||
if (stateMachine.IsGrounded)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.SprintSpeed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerDashState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerDashState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03721c0a319be8c4093b616e2f46afe8
|
||||
65
Assets/Scripts/Player Controller/PlayerDodgeState.cs
Normal file
65
Assets/Scripts/Player Controller/PlayerDodgeState.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerDodgeState : PlayerBaseState
|
||||
{
|
||||
private readonly int dodgeHash = Animator.StringToHash("Dodge");
|
||||
private float dodgeDuration = 0.4f; // Adjust based on your dodge animation length
|
||||
private float dodgeTimer;
|
||||
private Vector3 dodgeDirection;
|
||||
|
||||
public PlayerDodgeState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
dodgeTimer = dodgeDuration;
|
||||
stateMachine.Anim.SetTrigger(dodgeHash);
|
||||
|
||||
// Calculate dodge direction based on current input
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
if (input != Vector2.zero)
|
||||
{
|
||||
// Dodge in the input direction (Left, Right, Back, Forward)
|
||||
dodgeDirection = new Vector3(input.x, 0f, input.y).normalized;
|
||||
|
||||
if (stateMachine.Cam != null)
|
||||
{
|
||||
dodgeDirection = stateMachine.Cam.PlanarRotation * dodgeDirection;
|
||||
}
|
||||
|
||||
// Instantly rotate the player to face the dodge direction
|
||||
stateMachine.transform.rotation = Quaternion.LookRotation(dodgeDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no input, just roll straight forward
|
||||
dodgeDirection = stateMachine.transform.forward;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
dodgeTimer -= deltaTime;
|
||||
|
||||
// Apply movement force for the dodge (usually slightly slower than a Dash)
|
||||
stateMachine.Controller.Move(dodgeDirection * (stateMachine.DashForce * 0.8f) * deltaTime);
|
||||
|
||||
// Once the roll finishes, transition back to Idle or Move
|
||||
if (dodgeTimer <= 0f)
|
||||
{
|
||||
if (stateMachine.Input.MoveInput != Vector2.zero)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
public override void Exit() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71bce3b0373df774c8b1598487bcd4ee
|
||||
82
Assets/Scripts/Player Controller/PlayerFallState.cs
Normal file
82
Assets/Scripts/Player Controller/PlayerFallState.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerFallState : PlayerBaseState
|
||||
{
|
||||
private readonly int fallHash = Animator.StringToHash("Fall");
|
||||
private float fallSpeed;
|
||||
|
||||
public PlayerFallState(PlayerStateMachine stateMachine, float fallSpeed = -1f) : base(stateMachine)
|
||||
{
|
||||
if (fallSpeed < 0)
|
||||
{
|
||||
this.fallSpeed = stateMachine.WalkSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fallSpeed = fallSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Anim.SetTrigger(fallHash);
|
||||
stateMachine.Input.OnDodgeEvent += OnThrustPressed;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
|
||||
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
|
||||
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
|
||||
|
||||
Vector3 velocity = moveDirection * fallSpeed;
|
||||
velocity.y = stateMachine.VelocityY;
|
||||
|
||||
stateMachine.Controller.Move(velocity * deltaTime);
|
||||
|
||||
if (stateMachine.Input.IsSprintHeld)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerAirDashState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateMachine.IsGrounded)
|
||||
{
|
||||
// Landing Shake from PlayerController.cs
|
||||
if (!stateMachine.WasGrounded && stateMachine.VelocityY < -1f)
|
||||
{
|
||||
if (stateMachine.Cam != null)
|
||||
{
|
||||
stateMachine.Cam.Shake(0.2f, 0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
stateMachine.VelocityY = -2f;
|
||||
|
||||
if (input == Vector2.zero)
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
else
|
||||
{
|
||||
// Return to the appropriate movement state based on sprint input
|
||||
if (stateMachine.Input.IsSprintHeld)
|
||||
stateMachine.SwitchState(new PlayerRunState(stateMachine));
|
||||
else
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThrustPressed() => stateMachine.SwitchState(new PlayerThrustState(stateMachine));
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Input.OnDodgeEvent -= OnThrustPressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerFallState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerFallState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2b239715e32b6a4791f677e0159b862
|
||||
71
Assets/Scripts/Player Controller/PlayerIdleState.cs
Normal file
71
Assets/Scripts/Player Controller/PlayerIdleState.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerIdleState : PlayerBaseState
|
||||
{
|
||||
private readonly int speedHash = Animator.StringToHash("Speed");
|
||||
|
||||
public PlayerIdleState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent += OnJump;
|
||||
stateMachine.Input.OnDodgeEvent += OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent += OnCrouch;
|
||||
stateMachine.Input.OnInteractEvent += OnInteract;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
stateMachine.Anim.SetFloat(speedHash, 0f, stateMachine.AnimationDamping, deltaTime);
|
||||
|
||||
if (stateMachine.Input.MoveInput != Vector2.zero)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
|
||||
{
|
||||
stateMachine.VelocityY = -2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
|
||||
}
|
||||
|
||||
stateMachine.Controller.Move(new Vector3(0, stateMachine.VelocityY, 0) * deltaTime);
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent -= OnJump;
|
||||
stateMachine.Input.OnDodgeEvent -= OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent -= OnCrouch;
|
||||
stateMachine.Input.OnInteractEvent -= OnInteract;
|
||||
}
|
||||
|
||||
private void OnJump()
|
||||
{
|
||||
if (stateMachine.IsGrounded)
|
||||
{
|
||||
if (stateMachine.Scanner != null)
|
||||
{
|
||||
var hitData = stateMachine.Scanner.ObstacleCheck();
|
||||
if (hitData.forwardHitFound)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
|
||||
return;
|
||||
}
|
||||
}
|
||||
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.WalkSpeed));
|
||||
}
|
||||
}
|
||||
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
|
||||
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
|
||||
private void OnInteract() => stateMachine.SwitchState(new PlayerInteractState(stateMachine));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerIdleState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerIdleState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd83e34f11c7aaa4ab144ee59db04e8d
|
||||
38
Assets/Scripts/Player Controller/PlayerInteractState.cs
Normal file
38
Assets/Scripts/Player Controller/PlayerInteractState.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerInteractState : PlayerBaseState
|
||||
{
|
||||
public PlayerInteractState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
// Lấy vật thể đang được chọn (Index hiện tại)
|
||||
IInteractable interactable = stateMachine.GetInteractable();
|
||||
|
||||
if (interactable != null)
|
||||
{
|
||||
Debug.Log($"[Interaction] Interacting with: {interactable.InteractionPrompt}");
|
||||
interactable.OnInteract(stateMachine);
|
||||
|
||||
// Bạn có thể phát animation tương tác ở đây
|
||||
// stateMachine.Anim.CrossFadeInFixedTime("Interact", 0.1f);
|
||||
}
|
||||
|
||||
// Chuyển về trạng thái di chuyển hoặc đứng yên ngay lập tức
|
||||
if (stateMachine.Input.MoveInput == Vector2.zero)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime) { }
|
||||
public override void PhysicsTick(float fixedDeltaTime) { }
|
||||
public override void Exit() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b772d1c0c26c634fad5e71b43db9385
|
||||
52
Assets/Scripts/Player Controller/PlayerJumpState.cs
Normal file
52
Assets/Scripts/Player Controller/PlayerJumpState.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerJumpState : PlayerBaseState
|
||||
{
|
||||
private readonly int jumpHash = Animator.StringToHash("Jump");
|
||||
private float jumpSpeed;
|
||||
|
||||
public PlayerJumpState(PlayerStateMachine stateMachine, float jumpSpeed = -1f) : base(stateMachine)
|
||||
{
|
||||
if (jumpSpeed < 0)
|
||||
{
|
||||
this.jumpSpeed = stateMachine.WalkSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.jumpSpeed = jumpSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Anim.SetTrigger(jumpHash);
|
||||
|
||||
// Physic formula: v = sqrt(h * -2 * g)
|
||||
stateMachine.VelocityY = Mathf.Sqrt(stateMachine.JumpHeight * -2f * Physics.gravity.y);
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
|
||||
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
|
||||
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
|
||||
|
||||
Vector3 velocity = moveDirection * jumpSpeed;
|
||||
velocity.y = stateMachine.VelocityY;
|
||||
|
||||
stateMachine.Controller.Move(velocity * deltaTime);
|
||||
|
||||
if (stateMachine.VelocityY <= 0f)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerFallState(stateMachine, jumpSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
public override void Exit() {}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerJumpState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerJumpState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ab5bb519df10fe45a5af5f45111ed4d
|
||||
97
Assets/Scripts/Player Controller/PlayerMoveState.cs
Normal file
97
Assets/Scripts/Player Controller/PlayerMoveState.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerMoveState : PlayerBaseState
|
||||
{
|
||||
private readonly int speedHash = Animator.StringToHash("Speed");
|
||||
|
||||
public PlayerMoveState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent += OnJump;
|
||||
stateMachine.Input.OnDodgeEvent += OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent += OnCrouch;
|
||||
stateMachine.Input.OnInteractEvent += OnInteract;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
float moveAmount = Mathf.Clamp01(Mathf.Abs(input.x) + Mathf.Abs(input.y));
|
||||
|
||||
if (moveAmount <= 0.01f)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateMachine.Input.IsSprintHeld)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerDashState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
|
||||
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
|
||||
|
||||
Vector3 velocity = moveDirection * stateMachine.RunSpeed;
|
||||
|
||||
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
|
||||
{
|
||||
stateMachine.VelocityY = -2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
|
||||
}
|
||||
velocity.y = stateMachine.VelocityY;
|
||||
|
||||
stateMachine.Controller.Move(velocity * deltaTime);
|
||||
|
||||
if (moveDirection != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRot = Quaternion.LookRotation(moveDirection);
|
||||
stateMachine.transform.rotation = Quaternion.RotateTowards(
|
||||
stateMachine.transform.rotation,
|
||||
targetRot,
|
||||
stateMachine.RotationSpeed * deltaTime
|
||||
);
|
||||
}
|
||||
|
||||
stateMachine.Anim.SetFloat(speedHash, 0.7f, stateMachine.AnimationDamping, deltaTime);
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent -= OnJump;
|
||||
stateMachine.Input.OnDodgeEvent -= OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent -= OnCrouch;
|
||||
stateMachine.Input.OnInteractEvent -= OnInteract;
|
||||
}
|
||||
|
||||
private void OnJump()
|
||||
{
|
||||
if (stateMachine.IsGrounded)
|
||||
{
|
||||
if (stateMachine.Scanner != null)
|
||||
{
|
||||
var hitData = stateMachine.Scanner.ObstacleCheck();
|
||||
if (hitData.forwardHitFound)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
|
||||
return;
|
||||
}
|
||||
}
|
||||
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.RunSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
|
||||
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
|
||||
private void OnInteract() => stateMachine.SwitchState(new PlayerInteractState(stateMachine));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerMoveState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerMoveState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a11b0f0549f3dbd45bbdfd8114586a43
|
||||
53
Assets/Scripts/Player Controller/PlayerParkourState.cs
Normal file
53
Assets/Scripts/Player Controller/PlayerParkourState.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerParkourState : PlayerBaseState
|
||||
{
|
||||
private readonly int parkourHash = Animator.StringToHash("Step Up");
|
||||
private float animationDuration;
|
||||
private float timer;
|
||||
|
||||
public PlayerParkourState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
// Play the Parkour animation (Step Up)
|
||||
stateMachine.Anim.CrossFadeInFixedTime(parkourHash, 0.1f);
|
||||
|
||||
// We'll wait for the animation to finish.
|
||||
// In a real project, you might get the exact duration from the Animator.
|
||||
// For now, we'll assume a fixed duration or check state.
|
||||
timer = 0f;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
timer += deltaTime;
|
||||
|
||||
// Simple way to wait for animation: check normalized time of the current state
|
||||
var stateInfo = stateMachine.Anim.GetCurrentAnimatorStateInfo(0);
|
||||
if (stateInfo.shortNameHash == parkourHash && stateInfo.normalizedTime >= 1f)
|
||||
{
|
||||
if (stateMachine.Input.MoveInput == Vector2.zero)
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
else
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
}
|
||||
|
||||
// Safety timeout if animation doesn't play or something
|
||||
if (timer > 2f)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime)
|
||||
{
|
||||
// Usually during parkour we disable gravity or handle it specially
|
||||
stateMachine.VelocityY = 0;
|
||||
}
|
||||
|
||||
public override void Exit() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9b73ee252f157e49a08f8f7566dc6ac
|
||||
94
Assets/Scripts/Player Controller/PlayerRunState.cs
Normal file
94
Assets/Scripts/Player Controller/PlayerRunState.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerRunState : PlayerBaseState
|
||||
{
|
||||
private readonly int speedHash = Animator.StringToHash("Speed");
|
||||
|
||||
public PlayerRunState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent += OnJump;
|
||||
stateMachine.Input.OnDodgeEvent += OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent += OnCrouch;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
Vector2 input = stateMachine.Input.MoveInput;
|
||||
float moveAmount = Mathf.Clamp01(Mathf.Abs(input.x) + Mathf.Abs(input.y));
|
||||
|
||||
if (moveAmount <= 0.01f)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stateMachine.Input.IsSprintHeld)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerMoveState(stateMachine));
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 inputDir = new Vector3(input.x, 0, input.y).normalized;
|
||||
Vector3 moveDirection = stateMachine.Cam != null ? stateMachine.Cam.PlanarRotation * inputDir : inputDir;
|
||||
|
||||
Vector3 velocity = moveDirection * stateMachine.SprintSpeed;
|
||||
|
||||
if (stateMachine.IsGrounded && stateMachine.VelocityY < 0)
|
||||
{
|
||||
stateMachine.VelocityY = -2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
stateMachine.VelocityY += Physics.gravity.y * deltaTime;
|
||||
}
|
||||
velocity.y = stateMachine.VelocityY;
|
||||
|
||||
stateMachine.Controller.Move(velocity * deltaTime);
|
||||
|
||||
if (moveDirection != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRot = Quaternion.LookRotation(moveDirection);
|
||||
stateMachine.transform.rotation = Quaternion.RotateTowards(
|
||||
stateMachine.transform.rotation,
|
||||
targetRot,
|
||||
stateMachine.RotationSpeed * deltaTime
|
||||
);
|
||||
}
|
||||
|
||||
stateMachine.Anim.SetFloat(speedHash, 1f, stateMachine.AnimationDamping, deltaTime);
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
stateMachine.Input.OnJumpEvent -= OnJump;
|
||||
stateMachine.Input.OnDodgeEvent -= OnDodge;
|
||||
stateMachine.Input.OnCrouchEvent -= OnCrouch;
|
||||
}
|
||||
|
||||
private void OnJump()
|
||||
{
|
||||
if (stateMachine.IsGrounded)
|
||||
{
|
||||
if (stateMachine.Scanner != null)
|
||||
{
|
||||
var hitData = stateMachine.Scanner.ObstacleCheck();
|
||||
if (hitData.forwardHitFound)
|
||||
{
|
||||
stateMachine.SwitchState(new PlayerParkourState(stateMachine));
|
||||
return;
|
||||
}
|
||||
}
|
||||
stateMachine.SwitchState(new PlayerJumpState(stateMachine, stateMachine.SprintSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDodge() => stateMachine.SwitchState(new PlayerDodgeState(stateMachine));
|
||||
private void OnCrouch() => stateMachine.SwitchState(new PlayerCrouchState(stateMachine));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player Controller/PlayerRunState.cs.meta
Normal file
2
Assets/Scripts/Player Controller/PlayerRunState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef096feea261a8d449384a68f71470b3
|
||||
177
Assets/Scripts/Player Controller/PlayerStateMachine.cs
Normal file
177
Assets/Scripts/Player Controller/PlayerStateMachine.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController), typeof(InputReader), typeof(Animator))]
|
||||
public class PlayerStateMachine : MonoBehaviour
|
||||
{
|
||||
[field: Header("References")]
|
||||
[field: SerializeField] public CharacterController Controller { get; private set; }
|
||||
[field: SerializeField] public virtual InputReader Input { get; private set; }
|
||||
[field: SerializeField] public Animator Anim { get; private set; }
|
||||
[field: SerializeField] public EnvironmentScanner Scanner { get; private set; }
|
||||
public CameraController Cam { get; private set; }
|
||||
|
||||
[field: Header("Movement Settings")]
|
||||
[field: SerializeField] public float WalkSpeed { get; private set; } = 3f;
|
||||
[field: SerializeField] public float RunSpeed { get; private set; } = 6f;
|
||||
[field: SerializeField] public float SprintSpeed { get; private set; } = 9f; // 150% of RunSpeed
|
||||
[field: SerializeField] public float SneakSpeed { get; private set; } = 1.5f;
|
||||
[field: SerializeField] public float DashForce { get; private set; } = 10f;
|
||||
[field: SerializeField] public float RotationSpeed { get; private set; } = 500f;
|
||||
[field: SerializeField] public float AnimationDamping { get; private set; } = 0.2f;
|
||||
|
||||
[field: Header("Airborne Settings")]
|
||||
[field: SerializeField] public float JumpHeight { get; private set; } = 2f;
|
||||
[field: SerializeField] public float Gravity { get; private set; } = -9.81f;
|
||||
[field: SerializeField] public float ThrustDownwardForce { get; private set; } = -20f;
|
||||
|
||||
[field: Header("Ground Check")]
|
||||
[field: SerializeField] public float GroundCheckRadius { get; private set; } = 0.2f;
|
||||
[field: SerializeField] public Vector3 GroundCheckOffset { get; private set; }
|
||||
[field: SerializeField] public LayerMask GroundMask { get; private set; }
|
||||
|
||||
[field: Header("Interaction")]
|
||||
[field: SerializeField] public float InteractionRange { get; private set; } = 2f;
|
||||
[field: SerializeField] public LayerMask InteractionMask { get; private set; }
|
||||
|
||||
public float VelocityY { get; set; }
|
||||
public bool IsGrounded { get; private set; }
|
||||
public bool WasGrounded { get; private set; }
|
||||
|
||||
// Interaction system variables
|
||||
private List<IInteractable> interactablesNearby = new List<IInteractable>();
|
||||
private int currentInteractableIndex = 0;
|
||||
|
||||
public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None";
|
||||
|
||||
private PlayerBaseState currentState;
|
||||
private bool hasControl = true;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
Controller = GetComponent<CharacterController>();
|
||||
Input = GetComponent<InputReader>();
|
||||
Anim = GetComponentInChildren<Animator>();
|
||||
Scanner = GetComponent<EnvironmentScanner>();
|
||||
Cam = Camera.main?.GetComponent<CameraController>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SwitchState(new PlayerIdleState(this));
|
||||
|
||||
// Subscribe to cycle events
|
||||
Input.OnNextInteractEvent += OnNextInteract;
|
||||
Input.OnPreviousInteractEvent += OnPreviousInteract;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Input != null)
|
||||
{
|
||||
Input.OnNextInteractEvent -= OnNextInteract;
|
||||
Input.OnPreviousInteractEvent -= OnPreviousInteract;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
if (!hasControl) return;
|
||||
|
||||
WasGrounded = IsGrounded;
|
||||
CheckGround();
|
||||
UpdateInteractablesList();
|
||||
|
||||
currentState?.Tick(Time.deltaTime);
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!hasControl) return;
|
||||
currentState?.PhysicsTick(Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
private void CheckGround()
|
||||
{
|
||||
IsGrounded = Physics.CheckSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius, GroundMask);
|
||||
}
|
||||
|
||||
private void UpdateInteractablesList()
|
||||
{
|
||||
interactablesNearby.Clear();
|
||||
Collider[] colliders = Physics.OverlapSphere(transform.position + transform.forward * (InteractionRange / 2), InteractionRange, InteractionMask);
|
||||
foreach (var col in colliders)
|
||||
{
|
||||
if (col.TryGetComponent(out IInteractable interactable))
|
||||
{
|
||||
if (!interactablesNearby.Contains(interactable))
|
||||
interactablesNearby.Add(interactable);
|
||||
}
|
||||
}
|
||||
|
||||
if (interactablesNearby.Count == 0)
|
||||
{
|
||||
currentInteractableIndex = 0;
|
||||
}
|
||||
else if (currentInteractableIndex >= interactablesNearby.Count)
|
||||
{
|
||||
currentInteractableIndex = interactablesNearby.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNextInteract()
|
||||
{
|
||||
if (interactablesNearby.Count <= 1) return;
|
||||
currentInteractableIndex = (currentInteractableIndex + 1) % interactablesNearby.Count;
|
||||
Debug.Log($"[Interaction] Switched to: {interactablesNearby[currentInteractableIndex].InteractionPrompt}");
|
||||
}
|
||||
|
||||
private void OnPreviousInteract()
|
||||
{
|
||||
if (interactablesNearby.Count <= 1) return;
|
||||
currentInteractableIndex--;
|
||||
if (currentInteractableIndex < 0) currentInteractableIndex = interactablesNearby.Count - 1;
|
||||
Debug.Log($"[Interaction] Switched to: {interactablesNearby[currentInteractableIndex].InteractionPrompt}");
|
||||
}
|
||||
|
||||
public IInteractable GetInteractable()
|
||||
{
|
||||
if (interactablesNearby.Count == 0) return null;
|
||||
return interactablesNearby[currentInteractableIndex];
|
||||
}
|
||||
|
||||
public void SetGroundCheck(float radius, Vector3 offset)
|
||||
{
|
||||
GroundCheckRadius = radius;
|
||||
GroundCheckOffset = offset;
|
||||
}
|
||||
|
||||
public void SwitchState(PlayerBaseState newState)
|
||||
{
|
||||
currentState?.Exit();
|
||||
currentState = newState;
|
||||
currentState?.Enter();
|
||||
}
|
||||
|
||||
public void SetControl(bool control)
|
||||
{
|
||||
hasControl = control;
|
||||
Controller.enabled = control;
|
||||
if (!control)
|
||||
{
|
||||
Anim.SetFloat("Speed", 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = new Color(0, 1, 0, 0.5f);
|
||||
Gizmos.DrawSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius);
|
||||
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(transform.position + transform.forward * (InteractionRange / 2), InteractionRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 848ad6fdeb60b254497391392419b063
|
||||
43
Assets/Scripts/Player Controller/PlayerThrustState.cs
Normal file
43
Assets/Scripts/Player Controller/PlayerThrustState.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class PlayerThrustState : PlayerBaseState
|
||||
{
|
||||
private readonly int thrustHash = Animator.StringToHash("Thrust");
|
||||
|
||||
public PlayerThrustState(PlayerStateMachine stateMachine) : base(stateMachine) {}
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
stateMachine.Anim.SetTrigger(thrustHash);
|
||||
|
||||
// Immediately set a massive downward velocity
|
||||
stateMachine.VelocityY = stateMachine.ThrustDownwardForce;
|
||||
}
|
||||
|
||||
public override void Tick(float deltaTime)
|
||||
{
|
||||
// Keep applying heavy gravity just in case
|
||||
stateMachine.VelocityY += (stateMachine.Gravity * 2f) * deltaTime;
|
||||
|
||||
// Move the player straight down (no horizontal movement allowed during thrust)
|
||||
Vector3 fallMovement = new Vector3(0f, stateMachine.VelocityY, 0f);
|
||||
stateMachine.Controller.Move(fallMovement * deltaTime);
|
||||
|
||||
// When we smash into the ground...
|
||||
if (stateMachine.Controller.isGrounded)
|
||||
{
|
||||
stateMachine.VelocityY = -2f;
|
||||
|
||||
// TODO: Add impact effects, screen shake, or damage area here!
|
||||
|
||||
// Return to idle after landing
|
||||
stateMachine.SwitchState(new PlayerIdleState(stateMachine));
|
||||
}
|
||||
}
|
||||
|
||||
public override void PhysicsTick(float fixedDeltaTime) {}
|
||||
public override void Exit() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e76cfd208cc6f44b8b954147ee1defb
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user