修改结算界面v0.2

增加水波纹效果
This commit is contained in:
2026-07-05 03:09:42 +08:00
parent 904f854774
commit f5883eb451
16 changed files with 1734 additions and 57 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

@@ -0,0 +1,133 @@
fileFormatVersion: 2
guid: DywftHmlAXoLsWpW/3FmzSlaa7pipOD+KA4oePRPvSGjv0lXt9T9tIQ=
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
webStreaming: 0
priorityLevel: 0
uploadedMode: 2
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
maxPlaceholderSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
maxPlaceholderSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
maxPlaceholderSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+628
View File
@@ -0,0 +1,628 @@
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;
}
/// <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;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Wywd4SKrAXNc+Fwjq+o/hULULOb/iMpKr6vyL+R0z0MC7BduwO5pIPg=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+108 -17
View File
@@ -27,7 +27,14 @@ namespace GameFramework
[SerializeField] private GameState gameState = GameState.Playing;
[Header("过场动画")]
[Header("过场动画 - 失败(光圈缩小)")]
[Tooltip("光源缩小的持续时间(秒)")]
[SerializeField] private float lightShrinkDuration = 2f;
[Tooltip("失败后黑色遮罩淡出持续时间(秒)")]
[SerializeField] private float lostFadeOutDuration = 1f;
[Header("过场动画 - 胜利(光源扩大 + 切场景)")]
[Tooltip("光源扩大的持续时间(秒)")]
[SerializeField] private float lightExpandDuration = 1.5f;
@@ -56,27 +63,76 @@ namespace GameFramework
}
/// <summary>
/// 死亡过场协程:禁用控制 → 光源放大 → 淡出 → 跳转 Scoring
/// 使用 unscaledDeltaTime 确保不受 timeScale 影响
/// 失败过场协程:禁用控制 → 光源缩小至 0 → 淡出黑色显示失败叠加层
/// 不切换场景,留在 Gameplay
/// </summary>
private IEnumerator DeathTransition()
{
// 等待一帧,让 HUD 先更新血量显示(最后一滴血消失)
// 等待一帧,让 HUD 先更新血量显示
yield return null;
// 1. 查找玩家并禁用控制
var player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
// 禁用玩家移动
var playerController = player.GetComponent<PlayerController>();
if (playerController != null) playerController.enabled = false;
// 禁用相机跟随
var camFollow = Camera.main != null ? Camera.main.GetComponent<CameraFollow>() : null;
if (camFollow != null) camFollow.enabled = false;
// 2. 光源扩大
// 2. 光源缩小至完全不可见
var lightSource = player.GetComponent<LightSource>();
if (lightSource != null)
{
float startRadius = lightSource.Radius;
float startIntensity = lightSource.Intensity;
float elapsed = 0f;
while (elapsed < lightShrinkDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / lightShrinkDuration);
float smoothT = t * t * (3f - 2f * t); // SmoothStep
lightSource.SetRadius(Mathf.Lerp(startRadius, 0.1f, smoothT));
lightSource.SetIntensity(Mathf.Lerp(startIntensity, 0f, smoothT));
yield return null;
}
lightSource.SetRadius(0.1f);
lightSource.SetIntensity(0f);
}
else
{
yield return new WaitForSecondsRealtime(lightShrinkDuration);
}
}
else
{
yield return new WaitForSecondsRealtime(lightShrinkDuration);
}
// 3. 淡出黑色 + 显示失败叠加层(不切场景)
yield return StartCoroutine(ShowLostOverlay());
}
/// <summary>
/// 胜利过场协程:光源扩大 → 淡出 → 跳转 Scoring。
/// </summary>
private IEnumerator VictoryTransition()
{
// 胜利使用光源扩散 + 淡出 + 切场景(与失败不同)
yield return null;
var player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
var playerController = player.GetComponent<PlayerController>();
if (playerController != null) playerController.enabled = false;
var camFollow = Camera.main != null ? Camera.main.GetComponent<CameraFollow>() : null;
if (camFollow != null) camFollow.enabled = false;
var lightSource = player.GetComponent<LightSource>();
if (lightSource != null)
{
@@ -87,7 +143,7 @@ namespace GameFramework
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / lightExpandDuration);
float smoothT = t * t * (3f - 2f * t); // SmoothStep
float smoothT = t * t * (3f - 2f * t);
lightSource.SetRadius(Mathf.Lerp(startRadius, lightExpandTargetRadius, smoothT));
yield return null;
}
@@ -96,31 +152,66 @@ namespace GameFramework
}
else
{
// 没有光源,等待同样时间
yield return new WaitForSecondsRealtime(lightExpandDuration);
}
}
else
{
// 没有玩家,等待同样时间
yield return new WaitForSecondsRealtime(lightExpandDuration);
}
// 3. 淡出到黑色
yield return StartCoroutine(FadeOutAndLoad());
}
/// <summary>
/// 胜利过场协程(与死亡相同,可后续扩展不同效果)
/// 失败叠加层:淡入黑色 → 显示"你已迷失"UI → 黑色稍微淡出露出暗黑场景
/// 不切换场景,留在 Gameplay。
/// </summary>
private IEnumerator VictoryTransition()
private IEnumerator ShowLostOverlay()
{
// 胜利也使用相同的光源扩散 + 淡出效果
yield return DeathTransition();
// 创建全屏黑色遮罩
var canvasObj = new GameObject("FadeOverlay");
var canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 99999;
canvasObj.AddComponent<CanvasScaler>();
var overlay = canvasObj.AddComponent<Image>();
overlay.color = new Color(0, 0, 0, 0);
// 淡入到全黑
float elapsed = 0f;
while (elapsed < fadeOutDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / fadeOutDuration);
overlay.color = new Color(0, 0, 0, t);
yield return null;
}
overlay.color = new Color(0, 0, 0, 1);
yield return null;
// 在全黑之上显示失败叠加层("你已迷失……" + 主界面按钮)
GameLostOverlay.Show();
// 等待一帧让 UI 渲染
yield return null;
// 黑色遮罩稍微淡出,露出一点暗黑场景(配合叠加层的暗色蒙版)
elapsed = 0f;
while (elapsed < lostFadeOutDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / lostFadeOutDuration);
overlay.color = new Color(0, 0, 0, Mathf.Lerp(1f, 0.4f, t));
yield return null;
}
overlay.color = new Color(0, 0, 0, 0.4f);
}
/// <summary>
/// 创建全屏黑色遮罩并淡出,然后加载 Scoring 场景。
/// 创建全屏黑色遮罩并淡出,然后加载 Scoring 场景(仅胜利时使用)
/// </summary>
private IEnumerator FadeOutAndLoad()
{
@@ -148,7 +239,7 @@ namespace GameFramework
// 等待一帧确保淡出渲染完成
yield return null;
// 加载 Scoring 场景(直接使用 SceneManager,避免 SceneLoader 的异步加载可能卡住)
// 加载 Scoring 场景
Debug.Log("[GameManager] 过场完成,正在加载 Scoring 场景...");
SceneManager.LoadScene("Scoring");
}
+3 -19
View File
@@ -44,25 +44,9 @@ namespace GameFramework
void ShowGameOver()
{
// 显示结束画面
if (gameOverCanvas != null)
gameOverCanvas.enabled = true;
// 暂停游戏
if (TimeController.Instance != null)
TimeController.Instance.Pause();
// 显示鼠标
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
// 选中确认按钮
if (confirmButton != null)
confirmButton.Select();
// 播放音效
if (AudioManager.Instance != null && gameOverSFX != null)
AudioManager.Instance.PlaySFX(gameOverSFX);
// 失败时由 GameLostOverlay 接管 UI,这里不再显示旧 Canvas
// 暂停和鼠标由 GameManager + GameLostOverlay 统一处理
return;
}
void Update()
+4 -1
View File
@@ -69,7 +69,10 @@ namespace GameFramework
GameManager.onGameWin -= OnGameWin;
}
void OnGameOver() => ShowResult(GameResult.Lose);
void OnGameOver()
{
// 失败时由 GameLostOverlay 接管,不再显示旧失败面板
}
void OnGameWin() => ShowResult(GameResult.Win);
void Start()
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: DihO5Hn5BSq1DooGsTK42AqH2Xg0sfXWw0kKsd+N/WZctIGs9hgSDxI=
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: WaterRippleFade
m_Shader: {fileID: 4800000, guid: CH0X5HutAHIq8wKmwmbkjKJlHGH/OREG19hBK3xdhB3plPoFMK+U1dw=, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _ColorMask: 15
- _FadeAlpha: 1
- _RippleAmplitude: 0.02
- _RippleFrequency: 8
- _RippleIntensity: 1
- _RippleSpeed: 3
- _Stencil: 0
- _StencilComp: 8
- _StencilOp: 0
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _UseUIAlphaClip: 0
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: DS4b4HyvBX+XinHARY4wRfv/mtbQDOAEbu+aS8yJaASdJjfwaf5WGU4=
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: D39OsCyuBS8dp3uWcdpzjcAt7zjw8PRCsgCscGLJdFdF0dY6vfNo0bo=
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,155 @@
Shader "GameFramework/UI/WaterRippleFade"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
// ---- 水波纹参数 ----
_RippleAmplitude ("Ripple Amplitude", Range(0, 0.15)) = 0.02
_RippleFrequency ("Ripple Frequency", Range(1, 30)) = 8
_RippleSpeed ("Ripple Speed", Range(0, 10)) = 3
_RippleIntensity ("Ripple Intensity", Range(0, 1)) = 1
_FadeAlpha ("Fade Alpha", Range(0, 1)) = 1
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_ST;
CBUFFER_START(UnityPerMaterial)
float4 _Color;
float4 _ClipRect;
float _RippleAmplitude;
float _RippleFrequency;
float _RippleSpeed;
float _RippleIntensity;
float _FadeAlpha;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
// UI 裁剪函数(兼容内置 UI 系统)
float4 UnityGet2DClipping(float2 position, float4 clipRect)
{
float2 inside = step(clipRect.xy, position.xy) * step(position.xy, clipRect.zw);
return float4(inside.x * inside.y, inside.x * inside.y, inside.x * inside.y, inside.x * inside.y);
}
Varyings vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.worldPosition = input.positionOS;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.texcoord = TRANSFORM_TEX(input.texcoord, _MainTex);
output.color = input.color * _Color;
return output;
}
half4 frag(Varyings input) : SV_Target
{
// ---- 水波纹 UV 扭曲 ----
float2 uv = input.texcoord;
float time = _Time.y * _RippleSpeed;
float amp = _RippleAmplitude * _RippleIntensity;
// 三层正弦波叠加,模拟水面扰动
float wave1 = sin(uv.y * _RippleFrequency * 6.28 + time) * amp;
float wave2 = sin(uv.x * _RippleFrequency * 4.5 + time * 1.3) * amp * 0.6;
float wave3 = sin((uv.x + uv.y) * _RippleFrequency * 8.0 - time * 0.7) * amp * 0.3;
uv.x += wave1 + wave3;
uv.y += wave2 + wave3 * 0.5;
// 采样纹理
half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv) * input.color;
// ---- 渐现 Alpha ----
color.a *= _FadeAlpha;
// UI 裁剪
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(input.worldPosition.xy, _ClipRect).r;
#endif
#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif
return color;
}
ENDHLSL
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: CH0X5HutAHIq8wKmwmbkjKJlHGH/OREG19hBK3xdhB3plPoFMK+U1dw=
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: