Files
gold_dolphin/unity/Assets/UI/GameLostOverlay.cs
T

646 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
namespace GameFramework
{
/// <summary>
/// 游戏失败叠加层 —— 直接挂在 Gameplay 场景的 Canvas 上。
///
/// 使用方式:
/// 1. 在 Gameplay 场景里创建一个空物体,挂上本脚本
/// 2. 运行一次游戏(或点 Editor 按钮),脚本会自动构建子 UI
/// 3. 停止运行后,子 UI 留在场景中,可自由编辑样式/位置/精灵
/// 4. 运行时由 GameManager 调用 GameLostOverlay.Show() 激活
///
/// 所有 UI 元素均为 [SerializeField],可在 Inspector 中替换。
/// </summary>
public class GameLostOverlay : MonoBehaviour
{
private static GameLostOverlay _instance;
// ====== Canvas ======
[Header("Canvas(留空则自动创建)")]
[SerializeField] private Canvas lostCanvas;
// ====== UI 元素引用(全部可编辑) ======
[Header("背景蒙版")]
[SerializeField] private Image dimBackground;
[Header("标题 —— 你已迷失……")]
[SerializeField] private Text lostTitleText;
[SerializeField] private Image lostTitleImage; // 可用图片替代文字
[Header("装饰")]
[SerializeField] private Image leftDecoration;
[SerializeField] private Image rightDecoration;
[Header("主界面按钮")]
[SerializeField] private Button mainMenuButton;
[SerializeField] private Image mainMenuButtonImage;
[Header("音效")]
[SerializeField] private AudioData lostSFX;
[Header("标题渐现动效")]
[Tooltip("标题渐现总时长(秒)")]
[SerializeField] private float titleFadeDuration = 1.5f;
[Tooltip("水面波纹位置抖动幅度(像素)")]
[SerializeField] private float ripplePositionAmplitude = 8f;
[Tooltip("水面波纹缩放抖动幅度")]
[SerializeField] private float rippleScaleAmplitude = 0.08f;
[Tooltip("水面波纹频率(越大抖动越快)")]
[SerializeField] private float rippleFrequency = 15f;
[Tooltip("水波纹材质(留空则自动从 Shader 创建)")]
[SerializeField] private Material rippleMaterial;
[Tooltip("主界面按钮延迟出现时间(秒)")]
[SerializeField] private float buttonFadeDelay = 2f;
[Tooltip("按钮渐现时长(秒)")]
[SerializeField] private float buttonFadeDuration = 0.5f;
private bool _isVisible;
private Coroutine _titleAnimCoroutine;
private Coroutine _buttonFadeCoroutine;
private Material _rippleMaterial;
// ====================================================================
// 静态入口
// ====================================================================
/// <summary>
/// 显示失败叠加层。优先使用场景中已有的实例,
/// 没有则动态创建一个。
/// </summary>
public static void Show()
{
// 场景中已有(隐藏的)实例?
if (_instance != null)
{
_instance.Activate();
return;
}
// 尝试在场景中找到
var found = FindObjectOfType<GameLostOverlay>();
if (found != null)
{
_instance = found;
found.Activate();
return;
}
// 都没有 → 动态创建
var obj = new GameObject("GameLostOverlay");
var overlay = obj.AddComponent<GameLostOverlay>();
overlay.BuildAll();
overlay.Activate();
}
// ====================================================================
// 生命周期
// ====================================================================
void Awake()
{
_instance = this;
// 如果 Canvas 还没赋值,尝试从自身获取
if (lostCanvas == null)
lostCanvas = GetComponent<Canvas>();
// 初始隐藏
if (lostCanvas != null)
lostCanvas.enabled = false;
}
void Update()
{
if (!_isVisible) return;
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.Space) || Input.GetKeyDown(KeyCode.Escape))
{
ReturnToMainMenu();
}
}
// ====================================================================
// 激活 / 隐藏
// ====================================================================
private void Activate()
{
_isVisible = true;
if (lostCanvas != null)
lostCanvas.enabled = true;
// 自动查找标题元素(兜底:Inspector 未赋值时从 Canvas 子节点中找)
AutoFindTitleElement();
Debug.Log($"[GameLostOverlay] Activate - lostTitleImage={lostTitleImage != null}, lostTitleText={lostTitleText != null}, canvas={lostCanvas != null}");
// 启动标题渐现动效(水面波纹 + 渐现)
if (_titleAnimCoroutine != null)
StopCoroutine(_titleAnimCoroutine);
_titleAnimCoroutine = StartCoroutine(TitleRippleFadeIn());
// 绑定按钮 + 延迟渐现
if (mainMenuButton != null)
{
mainMenuButton.onClick.RemoveAllListeners();
mainMenuButton.onClick.AddListener(ReturnToMainMenu);
// 初始隐藏按钮
SetButtonAlpha(0f);
// 延迟后渐现
if (_buttonFadeCoroutine != null)
StopCoroutine(_buttonFadeCoroutine);
_buttonFadeCoroutine = StartCoroutine(ButtonFadeIn());
}
// 音效
if (AudioManager.Instance != null && lostSFX != null)
AudioManager.Instance.PlaySFX(lostSFX);
// 显示鼠标
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
// 确保 EventSystem 有输入模块(UI 点击必需)
EnsureEventSystemInputModule();
}
/// <summary>
/// 确保 EventSystem 有 StandaloneInputModule,否则 UI 按钮无法响应点击。
/// </summary>
private void EnsureEventSystemInputModule()
{
var es = UnityEngine.EventSystems.EventSystem.current;
if (es == null) return;
if (es.GetComponent<UnityEngine.EventSystems.StandaloneInputModule>() == null)
{
es.gameObject.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
Debug.Log("[GameLostOverlay] 已自动添加 StandaloneInputModule");
}
}
/// <summary>
/// 按钮延迟渐现协程:等待 buttonFadeDelay 秒后,用 buttonFadeDuration 秒渐现。
/// </summary>
private IEnumerator ButtonFadeIn()
{
yield return new WaitForSecondsRealtime(buttonFadeDelay);
float elapsed = 0f;
while (elapsed < buttonFadeDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / buttonFadeDuration);
SetButtonAlpha(t);
yield return null;
}
SetButtonAlpha(1f);
if (mainMenuButton != null)
mainMenuButton.Select();
}
/// <summary>
/// 设置按钮整体透明度(通过 Image.color.a)。
/// </summary>
private void SetButtonAlpha(float alpha)
{
if (mainMenuButtonImage != null)
{
var c = mainMenuButtonImage.color;
c.a = alpha;
mainMenuButtonImage.color = c;
}
// 同时处理按钮子物体的 Text
if (mainMenuButton != null)
{
var texts = mainMenuButton.GetComponentsInChildren<Text>(true);
foreach (var t in texts)
{
var c = t.color;
c.a = alpha;
t.color = c;
}
}
}
/// <summary>
/// 返回主界面。
/// </summary>
private void ReturnToMainMenu()
{
_isVisible = false;
Time.timeScale = 1f;
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
if (SceneLoader.Instance != null)
SceneLoader.Instance.LoadMainMenuScene();
else
SceneManager.LoadScene("MainMenu");
}
/// <summary>
/// 自动查找标题元素。搜索范围:
/// 1. 自身 transform 子节点
/// 2. 场景中所有 Canvas 的子节点
/// 3. 都找不到则动态创建
/// </summary>
private void AutoFindTitleElement()
{
if (lostTitleImage != null || lostTitleText != null) return;
// 1. 从自身子节点查找
TryFindTitleInHierarchy(transform);
if (lostTitleText != null || lostTitleImage != null) return;
// 2. 从 lostCanvas 子节点查找
if (lostCanvas != null)
{
TryFindTitleInHierarchy(lostCanvas.transform);
if (lostTitleText != null || lostTitleImage != null) return;
}
// 3. 从场景中所有 Canvas 查找
var allCanvases = FindObjectsOfType<Canvas>();
foreach (var c in allCanvases)
{
if (c == lostCanvas) continue;
TryFindTitleInHierarchy(c.transform);
if (lostTitleText != null || lostTitleImage != null) return;
}
// 4. 都找不到
Debug.LogWarning("[GameLostOverlay] 未找到标题元素,请在 Inspector 中拖入 lostTitleImage 或 lostTitleText。");
}
private void TryFindTitleInHierarchy(Transform root)
{
// 按路径查找
var titleObj = root.Find("CenterPanel/LostTitle");
if (titleObj != null)
{
lostTitleImage = titleObj.GetComponent<Image>();
if (lostTitleImage != null) return;
lostTitleText = titleObj.GetComponent<Text>();
if (lostTitleText != null) return;
}
// 按名称查找 ImagelostTitleImage 节点)
var allImages = root.GetComponentsInChildren<Image>(true);
foreach (var img in allImages)
{
var name = img.gameObject.name.ToLower();
if (name.Contains("lost") || name.Contains("title") || name.Contains("迷失"))
{
lostTitleImage = img;
return;
}
}
// 按文字内容查找 Text
var allTexts = root.GetComponentsInChildren<Text>(true);
foreach (var t in allTexts)
{
if (t.text.Contains("迷失") || t.text.Contains("Lost"))
{
lostTitleText = t;
return;
}
}
// 兜底:第一个非按钮 Text
foreach (var t in allTexts)
{
if (t.GetComponentInParent<Button>() == null)
{
lostTitleText = t;
return;
}
}
}
/// <summary>
/// 标题渐现动效:Shader 水波纹扭曲 + 透明度渐现。
/// 前 60% 时间波纹强烈,后 40% 时间波纹减弱至消失。
/// 使用 unscaledDeltaTime 不受 timeScale 暂停影响。
/// </summary>
private IEnumerator TitleRippleFadeIn()
{
float elapsed = 0f;
// 优先用图片(Shader 动效),其次用文字(抖动动效)
Image titleImg = lostTitleImage;
Text titleTxt = lostTitleText;
// ---- Image: Shader 水波纹 ----
if (titleImg != null)
{
// 创建/获取水波纹材质
if (_rippleMaterial == null)
{
// 优先用 Inspector 拖入的材质
if (rippleMaterial != null)
{
_rippleMaterial = rippleMaterial;
Debug.Log("[GameLostOverlay] 使用 Inspector 指定的水波纹材质。");
}
else
{
var shader = Shader.Find("GameFramework/UI/WaterRippleFade");
if (shader == null)
{
Debug.LogWarning("[GameLostOverlay] 找不到 WaterRippleFade Shader!请确认 Assets/UI/shaders/WaterRippleFade.shader 存在且无编译错误。回退到抖动动效。");
}
else
{
_rippleMaterial = new Material(shader);
_rippleMaterial.SetFloat("_RippleAmplitude", ripplePositionAmplitude / 100f);
_rippleMaterial.SetFloat("_RippleFrequency", rippleFrequency);
_rippleMaterial.SetFloat("_RippleSpeed", 3f);
Debug.Log($"[GameLostOverlay] 已创建水波纹材质 - Amp={ripplePositionAmplitude / 100f}, Freq={rippleFrequency}");
}
}
}
if (_rippleMaterial != null)
{
titleImg.material = _rippleMaterial;
_rippleMaterial.SetFloat("_RippleIntensity", 1f);
_rippleMaterial.SetFloat("_FadeAlpha", 0f);
}
while (elapsed < titleFadeDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / titleFadeDuration);
// 波纹强度:前 60% 强,后 40% 衰减至 0
float rippleIntensity;
if (t < 0.6f)
rippleIntensity = 1f;
else
rippleIntensity = Mathf.InverseLerp(1f, 0.6f, t);
// 渐现曲线(EaseOut
float fadeT = 1f - Mathf.Pow(1f - t, 2.5f);
if (_rippleMaterial != null)
{
_rippleMaterial.SetFloat("_RippleIntensity", rippleIntensity);
_rippleMaterial.SetFloat("_FadeAlpha", fadeT);
}
yield return null;
}
// 确保最终状态
if (_rippleMaterial != null)
{
_rippleMaterial.SetFloat("_RippleIntensity", 0f);
_rippleMaterial.SetFloat("_FadeAlpha", 1f);
}
yield break;
}
// ---- Text: 抖动动效(回退方案) ----
if (titleTxt != null)
{
var titleTransform = titleTxt.rectTransform;
var c = titleTxt.color;
c.a = 0f;
titleTxt.color = c;
Vector2 targetPosition = titleTransform.anchoredPosition;
Vector3 targetScale = titleTransform.localScale;
while (elapsed < titleFadeDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / titleFadeDuration);
float rippleIntensity = t < 0.6f ? 1f : Mathf.InverseLerp(1f, 0.6f, t);
float fadeT = 1f - Mathf.Pow(1f - t, 2.5f);
float jitterX = Random.Range(-1f, 1f) * ripplePositionAmplitude * rippleIntensity;
float jitterY = Random.Range(-1f, 1f) * ripplePositionAmplitude * rippleIntensity * 0.5f;
float jitterScale = Random.Range(-1f, 1f) * rippleScaleAmplitude * rippleIntensity;
titleTransform.anchoredPosition = targetPosition + new Vector2(jitterX, jitterY);
titleTransform.localScale = targetScale * (1f + jitterScale);
var col = titleTxt.color;
col.a = fadeT;
titleTxt.color = col;
yield return null;
}
titleTransform.anchoredPosition = targetPosition;
titleTransform.localScale = targetScale;
var finalCol = titleTxt.color;
finalCol.a = 1f;
titleTxt.color = finalCol;
}
}
// ====================================================================
// Editor 辅助:一键构建 UI 子节点(在场景编辑时调用)
// ====================================================================
#if UNITY_EDITOR
/// <summary>
/// 在 Unity 编辑器中调用此方法,自动构建所有 UI 子节点。
/// 构建完成后停止运行,即可在 Inspector 中编辑所有引用。
/// </summary>
[ContextMenu("构建 UIBuild UI")]
public void EditorBuildUI()
{
BuildAll();
// 构建完显示 Canvas 方便预览
if (lostCanvas != null)
lostCanvas.enabled = true;
Debug.Log("[GameLostOverlay] UI 构建完成,可在场景中编辑。");
}
#endif
// ====================================================================
// 动态构建 UI(首次使用 / 无预制体时)
// ====================================================================
/// <summary>
/// 构建整个 UI 结构。构建后所有引用自动填入 SerializeField。
/// </summary>
private void BuildAll()
{
// ---- Canvas ----
if (lostCanvas == null)
{
var canvasObj = new GameObject("LostCanvas");
canvasObj.transform.SetParent(transform, false);
lostCanvas = canvasObj.AddComponent<Canvas>();
lostCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
lostCanvas.sortingOrder = 100000;
var scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.matchWidthOrHeight = 0.5f;
canvasObj.AddComponent<GraphicRaycaster>();
}
var root = lostCanvas.gameObject;
// ---- 全屏暗色蒙版 ----
if (dimBackground == null)
dimBackground = CreateFullScreenImage(root, "DimBackground", new Color(0f, 0f, 0f, 0.6f));
// ---- 中央面板 ----
var centerPanel = FindOrCreateChild(root, "CenterPanel");
var cpRect = centerPanel.GetComponent<RectTransform>();
if (cpRect.anchorMin == Vector2.zero && cpRect.anchorMax == Vector2.zero)
{
cpRect.anchorMin = new Vector2(0.15f, 0.3f);
cpRect.anchorMax = new Vector2(0.85f, 0.75f);
cpRect.sizeDelta = Vector2.zero;
}
// ---- 左装饰 ----
if (leftDecoration == null)
leftDecoration = CreateDecoration(centerPanel, "LeftDecoration", true);
// ---- 右装饰 ----
if (rightDecoration == null)
rightDecoration = CreateDecoration(centerPanel, "RightDecoration", false);
// ---- 标题文字 ----
if (lostTitleText == null && lostTitleImage == null)
{
lostTitleText = CreateTitleText(centerPanel);
}
// ---- 主界面按钮 ----
if (mainMenuButton == null)
CreateMainMenuButton(centerPanel);
}
// ------ 构建子元素 ------
private Image CreateFullScreenImage(GameObject parent, string name, Color color)
{
var obj = new GameObject(name);
obj.transform.SetParent(parent.transform, false);
var rect = obj.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
var img = obj.AddComponent<Image>();
img.color = color;
return img;
}
private GameObject FindOrCreateChild(GameObject parent, string name)
{
var t = parent.transform.Find(name);
if (t != null) return t.gameObject;
var obj = new GameObject(name);
obj.transform.SetParent(parent.transform, false);
obj.AddComponent<RectTransform>();
return obj;
}
private Image CreateDecoration(GameObject parent, string name, bool isLeft)
{
var obj = new GameObject(name);
obj.transform.SetParent(parent.transform, false);
var rect = obj.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(120, 200);
rect.anchoredPosition = new Vector2(isLeft ? -300 : 300, 0);
var img = obj.AddComponent<Image>();
img.color = new Color(0.9f, 0.4f, 0.5f, 0.5f);
return img;
}
private Text CreateTitleText(GameObject parent)
{
var obj = new GameObject("LostTitle");
obj.transform.SetParent(parent.transform, false);
var rect = obj.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(0, 30);
rect.sizeDelta = new Vector2(800, 120);
var text = obj.AddComponent<Text>();
text.text = "你已迷失……";
text.fontSize = 72;
text.alignment = TextAnchor.MiddleCenter;
text.color = Color.white;
text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
text.fontStyle = FontStyle.Bold;
var shadow = obj.AddComponent<Shadow>();
shadow.effectColor = new Color(0f, 0f, 0f, 0.5f);
shadow.effectDistance = new Vector2(3, -3);
var outline = obj.AddComponent<Outline>();
outline.effectColor = new Color(0.3f, 0.1f, 0.1f, 0.6f);
outline.effectDistance = new Vector2(2, -2);
return text;
}
private void CreateMainMenuButton(GameObject parent)
{
var btnObj = new GameObject("MainMenuButton");
btnObj.transform.SetParent(parent.transform, false);
var btnRect = btnObj.AddComponent<RectTransform>();
btnRect.anchorMin = new Vector2(0.5f, 0.5f);
btnRect.anchorMax = new Vector2(0.5f, 0.5f);
btnRect.pivot = new Vector2(0.5f, 0.5f);
btnRect.anchoredPosition = new Vector2(0, -80);
btnRect.sizeDelta = new Vector2(200, 60);
var btnImage = btnObj.AddComponent<Image>();
btnImage.color = new Color(1f, 1f, 1f, 0.2f);
mainMenuButtonImage = btnImage;
var textObj = new GameObject("Text");
textObj.transform.SetParent(btnObj.transform, false);
var textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
var text = textObj.AddComponent<Text>();
text.text = "主界面";
text.fontSize = 28;
text.alignment = TextAnchor.MiddleCenter;
text.color = Color.white;
text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
mainMenuButton = btnObj.AddComponent<Button>();
var colors = mainMenuButton.colors;
colors.highlightedColor = new Color(1.2f, 1.2f, 1.2f);
colors.pressedColor = new Color(0.8f, 0.8f, 0.8f);
mainMenuButton.colors = colors;
}
}
}