Major changes to editor tools, and adding new layer for buildable towers
This commit is contained in:
parent
a4e28bc93f
commit
b44eeaeeff
21 changed files with 2867 additions and 89 deletions
|
|
@ -0,0 +1,195 @@
|
|||
// Assets/_Project/Scripts/Editor/Levels/LevelAuthoringPlayModeHook.cs
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using TD.Levels;
|
||||
|
||||
namespace TD.Levels.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Static editor-only hook that intercepts Play mode entry and validates the
|
||||
/// scene's LevelAuthoring against its baked LevelData. Catches the common
|
||||
/// failure mode of "edited the scene, forgot to bake, played stale data".
|
||||
///
|
||||
/// Decision tree (executes in PlayModeStateChange.ExitingEditMode, before
|
||||
/// Play actually begins, so we can abort cleanly):
|
||||
/// 1. No LevelAuthoring in scene -> proceed silently
|
||||
/// 2. Multiple LevelAuthoring -> error dialog, abort
|
||||
/// 3. targetAsset null -> 2-button "Continue Anyway / Cancel"
|
||||
/// 4. AuthoringHash empty -> 2-button "Bake & Play / Cancel"
|
||||
/// 5. Hashes match -> proceed silently
|
||||
/// 6. Hashes differ -> 3-button "Bake & Play / Play Anyway / Cancel"
|
||||
///
|
||||
/// Aborts via EditorApplication.isPlaying = false during ExitingEditMode.
|
||||
/// During this window EditorApplication.isPlayingOrWillChangePlaymode is
|
||||
/// true but isPlaying is false; setting isPlaying = false at this point
|
||||
/// cancels the Play attempt cleanly.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
internal static class LevelAuthoringPlayModeHook
|
||||
{
|
||||
// Static constructor runs on every editor reload (script recompile or
|
||||
// domain reload), wiring the event handler. It's idempotent because
|
||||
// we always remove first (no-op if not subscribed) then add.
|
||||
static LevelAuthoringPlayModeHook()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private static void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
// We only intercept the moment we're about to enter Play mode.
|
||||
// Other phases (EnteredPlayMode, ExitingPlayMode, EnteredEditMode)
|
||||
// are not our concern.
|
||||
if (state != PlayModeStateChange.ExitingEditMode)
|
||||
return;
|
||||
|
||||
// Find LevelAuthoring instances in the scene. We only consider
|
||||
// active ones; an inactive LevelAuthoring is treated as
|
||||
// "not present" because its volumes won't be discovered by the
|
||||
// bake's scoped scan either.
|
||||
//
|
||||
// FindObjectsByType is the Unity 6+ replacement for the deprecated
|
||||
// FindObjectsOfType. We use the no-sort-mode overload: in Unity 6.4+
|
||||
// the FindObjectsSortMode parameter has been deprecated (Unity is
|
||||
// moving from InstanceID toward EntityId, and stable sort order
|
||||
// can't be guaranteed in the new world). Passing it now produces
|
||||
// a CS0618 warning. We don't need stable order here -- a single
|
||||
// expected result, or zero, or "more than one" handled as an error.
|
||||
var authorings = Object.FindObjectsByType<LevelAuthoring>(
|
||||
FindObjectsInactive.Exclude);
|
||||
|
||||
// Case 1: No LevelAuthoring in scene -> proceed silently.
|
||||
// This is the normal case for menu scenes, bootstrap scenes, etc.
|
||||
if (authorings.Length == 0)
|
||||
return;
|
||||
|
||||
// Case 2: Multiple LevelAuthoring -> error and abort.
|
||||
// The bake assumes one per scene (it walks _LevelAuthoring's
|
||||
// subtree), so playing with two would be ambiguous.
|
||||
if (authorings.Length > 1)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Multiple LevelAuthoring components",
|
||||
$"Found {authorings.Length} LevelAuthoring components in this scene. " +
|
||||
"Only one is allowed per scene. Remove the extras before entering Play mode.",
|
||||
"OK");
|
||||
AbortPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
var authoring = authorings[0];
|
||||
|
||||
// Case 3: targetAsset is null. We can't dirty-check without one,
|
||||
// because there's no LevelData to compare the current scene's
|
||||
// hash against. Ask the user if they want to play anyway.
|
||||
if (authoring.targetAsset == null)
|
||||
{
|
||||
bool proceed = EditorUtility.DisplayDialog(
|
||||
"No LevelData target assigned",
|
||||
$"The LevelAuthoring on '{authoring.gameObject.name}' has no targetAsset assigned, " +
|
||||
"so its scene state cannot be checked against baked data.\n\n" +
|
||||
"Continue into Play mode anyway?",
|
||||
"Continue Anyway",
|
||||
"Cancel");
|
||||
|
||||
if (!proceed)
|
||||
AbortPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 4: Asset has never been baked (hash empty/null).
|
||||
// Offer Bake & Play, since there's no useful "play with stale data"
|
||||
// option when there's no baked data at all.
|
||||
string bakedHash = authoring.targetAsset.AuthoringHash;
|
||||
if (string.IsNullOrEmpty(bakedHash))
|
||||
{
|
||||
bool bakeAndPlay = EditorUtility.DisplayDialog(
|
||||
"Level has never been baked",
|
||||
$"'{authoring.targetAsset.name}' has no baked data yet. " +
|
||||
"Bake now and then enter Play mode?",
|
||||
"Bake && Play",
|
||||
"Cancel");
|
||||
|
||||
if (!bakeAndPlay)
|
||||
{
|
||||
AbortPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Lean A: if Bake & Play is requested and the bake fails,
|
||||
// abort Play. The bake errors will already be in the console.
|
||||
bool bakeOk = LevelBakePipeline.Bake(authoring);
|
||||
if (!bakeOk)
|
||||
{
|
||||
Debug.LogError("[PlayModeHook] Bake failed; aborting Play. See above for errors.");
|
||||
AbortPlay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the hash of the current scene state and compare to baked.
|
||||
// ComputeAuthoringHash(LevelAuthoring) is a pure function over the
|
||||
// same canonical input string the bake uses (it delegates to the
|
||||
// same private hash routine), so equal strings means equivalent
|
||||
// bakes.
|
||||
string currentHash = LevelBakePipeline.ComputeAuthoringHash(authoring);
|
||||
|
||||
// Case 5: Hashes match -> baked data is in sync with the scene.
|
||||
// Proceed silently; this is the happy path.
|
||||
if (currentHash == bakedHash)
|
||||
return;
|
||||
|
||||
// Case 6: Hashes differ -> 3-button dialog with the explicit
|
||||
// "Play Anyway" escape hatch. DisplayDialogComplex returns:
|
||||
// 0 = primary button (Bake & Play)
|
||||
// 1 = "alt" button (Cancel)
|
||||
// 2 = secondary (Play Anyway)
|
||||
// The button-to-return-code mapping is fixed by Unity's API, so
|
||||
// we have to map our intent onto it carefully.
|
||||
int choice = EditorUtility.DisplayDialogComplex(
|
||||
"Scene differs from baked data",
|
||||
$"The scene has been edited since '{authoring.targetAsset.name}' was last baked. " +
|
||||
"Playing now will use stale baked data.\n\n" +
|
||||
"Bake the level before playing, play with stale data anyway, or cancel?",
|
||||
"Bake && Play", // ok / button 0
|
||||
"Cancel", // alt / button 1
|
||||
"Play Anyway"); // secondary / button 2
|
||||
|
||||
switch (choice)
|
||||
{
|
||||
case 0: // Bake & Play
|
||||
bool ok = LevelBakePipeline.Bake(authoring);
|
||||
if (!ok)
|
||||
{
|
||||
Debug.LogError("[PlayModeHook] Bake failed; aborting Play. See above for errors.");
|
||||
AbortPlay();
|
||||
}
|
||||
return;
|
||||
|
||||
case 2: // Play Anyway
|
||||
// Lean C: log a console warning so debugging weirdness
|
||||
// later has a breadcrumb. Don't persist anything; the
|
||||
// user explicitly chose this.
|
||||
Debug.LogWarning(
|
||||
$"[PlayModeHook] Entering Play mode with stale baked data for " +
|
||||
$"'{authoring.targetAsset.name}'. Last bake: {authoring.targetAsset.LastBakeTimestamp}.");
|
||||
return;
|
||||
|
||||
case 1: // Cancel
|
||||
default:
|
||||
AbortPlay();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the Play mode transition. Must be called during
|
||||
/// ExitingEditMode for the cancel to take effect cleanly.
|
||||
/// </summary>
|
||||
private static void AbortPlay()
|
||||
{
|
||||
EditorApplication.isPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue