重构
This commit is contained in:
@@ -347,6 +347,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -385,9 +386,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 0}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -355,6 +355,10 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 11400000, guid: 1690413250c5d464eb84008d45cb88cc, type: 2}
|
||||
onPlayerDiedEvent: {fileID: 11400000, guid: 8eee0c771c042c44a88f48756f3717b5, type: 2}
|
||||
healthVar: {fileID: 11400000, guid: 6952ca1b8005ae541bf172dd398d4cc7, type: 2}
|
||||
maxHealthVar: {fileID: 11400000, guid: 6952ca1b8005ae541bf172dd398d4cc7, type: 2}
|
||||
maxHealth: 5
|
||||
deathEffect: {fileID: 4806121257990350900, guid: f19c76183b5e22e44a73655dc18f1a92, type: 3}
|
||||
isPlayer: 1
|
||||
@@ -430,6 +434,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a44142f1063e1d4dbab7b851d31d258, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
echoKey: 101
|
||||
expandSpeed: 15
|
||||
maxRadius: 30
|
||||
@@ -441,6 +446,8 @@ MonoBehaviour:
|
||||
ringVisualWidth: 2.5
|
||||
ringYOffset: 0.1
|
||||
ringFadeTime: 3
|
||||
ringExpandSpeed: 15
|
||||
ringShader: {fileID: 4800000, guid: 67f78fa2aa44cba4cadd869535881366, type: 3}
|
||||
cooldown: 3
|
||||
--- !u!1001 &1156712605891213377
|
||||
PrefabInstance:
|
||||
|
||||
@@ -2,13 +2,23 @@ using UnityEngine;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using GameFramework;
|
||||
using Architecture.Core;
|
||||
using Architecture.Variables;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
// Handles health, damage, and death for entities
|
||||
public class HealthSystem : MonoBehaviour
|
||||
{
|
||||
/// <summary>玩家受伤时触发(用于受击特效等)</summary>
|
||||
public static Action onPlayerDamaged;
|
||||
[Header("SO 事件通道(替代 static onPlayerDamaged 事件)")]
|
||||
[SerializeField] private GameEvent onPlayerDamagedEvent;
|
||||
|
||||
[Header("SO 事件通道(玩家死亡,替代直接调用 GameManager.GameOver)")]
|
||||
[SerializeField] private GameEvent onPlayerDiedEvent;
|
||||
|
||||
[Header("SO 变量(供 EnemyHealthBar / HUD 订阅,替代反射读取私有字段)")]
|
||||
[SerializeField] private IntVariable healthVar;
|
||||
[SerializeField] private IntVariable maxHealthVar;
|
||||
|
||||
[Range(1, 100)]
|
||||
[SerializeField]
|
||||
@@ -31,6 +41,7 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
private bool _isDead = false; // 防止重复触发死亡
|
||||
private bool _isInvincible = false; // 无敌状态
|
||||
private float _invincibleTimer = 0f; // 无敌剩余时间(<=0 表示永久无敌)
|
||||
private bool _warnedDamaged = false; // 防止 onPlayerDamagedEvent 空引用告警刷屏
|
||||
|
||||
[Header("受伤无敌")]
|
||||
[Tooltip("玩家受伤后的无敌时间(秒)")]
|
||||
@@ -42,13 +53,33 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
/// <summary>是否处于无敌状态(只读)</summary>
|
||||
public bool IsInvincible => _isInvincible;
|
||||
|
||||
/// <summary>当前血量(只读)</summary>
|
||||
public int CurrentHealth => health;
|
||||
|
||||
/// <summary>最大血量(只读)</summary>
|
||||
public int MaxHealth => maxHealth;
|
||||
|
||||
/// <summary>血量变化事件(current, max)。敌人血条等本地订阅者用于刷新显示,替代反射读取私有字段。</summary>
|
||||
public event System.Action<int, int> OnHealthChanged;
|
||||
|
||||
// 编辑器实时校验:事件字段未接线时给出黄色警告三角 + 控制台告警
|
||||
private void OnValidate()
|
||||
{
|
||||
if (isPlayer && onPlayerDamagedEvent == null)
|
||||
Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Damaged Event 未接线({gameObject.name})。受击泛红不会出现。", this);
|
||||
if (isPlayer && onPlayerDiedEvent == null)
|
||||
Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Died Event 未接线({gameObject.name})。玩家死亡不会触发过场。", this);
|
||||
}
|
||||
|
||||
// Initializes health
|
||||
private void Start()
|
||||
{
|
||||
health = maxHealth;
|
||||
if (isPlayer)
|
||||
{
|
||||
if (maxHealthVar != null) maxHealthVar.Value = maxHealth;
|
||||
if (healthVar != null) healthVar.Value = health;
|
||||
}
|
||||
}
|
||||
|
||||
// Applies damage and checks for death
|
||||
@@ -58,10 +89,20 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
if (_isInvincible) return; // 无敌状态,免疫伤害
|
||||
|
||||
health -= damageAmount;
|
||||
if (isPlayer && healthVar != null) healthVar.Value = health;
|
||||
OnHealthChanged?.Invoke(health, maxHealth);
|
||||
|
||||
// 玩家受伤时触发事件(用于受击泛红特效)
|
||||
if (isPlayer && onPlayerDamaged != null)
|
||||
onPlayerDamaged.Invoke();
|
||||
if (isPlayer)
|
||||
{
|
||||
if (onPlayerDamagedEvent != null)
|
||||
onPlayerDamagedEvent.Raise();
|
||||
else if (!_warnedDamaged)
|
||||
{
|
||||
Debug.LogWarning("[HealthSystem] onPlayerDamagedEvent 未接线!受击泛红特效不会触发。请在玩家 HealthSystem 的 On Player Damaged Event 字段拖入 OnPlayerDamaged 资产。", this);
|
||||
_warnedDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放受伤音效
|
||||
if (isPlayer && AudioManager.Instance != null)
|
||||
@@ -78,8 +119,8 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
|
||||
if (isPlayer)
|
||||
{
|
||||
// 玩家死亡:触发过场动画,不立即销毁(由 GameManager 处理)
|
||||
GameManager.GameOver();
|
||||
// 玩家死亡:通过 OnPlayerDied SO 事件通知 GameManager(解耦,不再直接调用单例)
|
||||
onPlayerDiedEvent?.Raise();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -213,4 +254,4 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
private float lastRollTime; // Timestamp of last roll
|
||||
private bool isRolling; // Is player currently rolling?
|
||||
|
||||
/// <summary>冲刺冷却剩余/总时长(供 HUD 显示,替代反射读取私有字段)。</summary>
|
||||
public (float remaining, float total) RollCooldown
|
||||
=> (Mathf.Max(0f, (lastRollTime + rollCooldown) - Time.time), rollCooldown);
|
||||
|
||||
[SerializeField] private KeyCode rollKeyCode; // Key to trigger roll
|
||||
|
||||
// Enum to switch between sword or projectile attack types
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CXkZ4S2lVHyohUFSvtGvZhelAh5YZD1t21cOxId/XueCt0dvcSw2LVI=
|
||||
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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WSxO4H+lVy/+Blf5pfhG4hmfSru5p5O0gX9h1Xbssr3FMpnAolEEZqQ=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DXhN53n7Wn6X4mq4BzmVGcYyf4qgGr+dz9mS8D/RSs2VcTccKveruRE=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0}
|
||||
m_Name: EchoReleased
|
||||
m_EditorClassIdentifier: Assembly-CSharp:Architecture.Core:Vector3Event
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DCwXsH6lBn1xFo36tyRsjs+h4O+VaxEW/j5EUKPXE9wXY5c7QNmWpTw=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0}
|
||||
m_Name: Enemies
|
||||
m_EditorClassIdentifier: Assembly-CSharp:Architecture.Core:TransformRuntimeSet
|
||||
Items: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WXNNtH6sVHk/AtKjh3pZHW8Y0Ud+sV/2q1BsD/usbR4CLijRwhOQiIk=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0d892c58b6fddaa4cb129dc82a1599bf, type: 3}
|
||||
m_Name: OnGameOver
|
||||
m_EditorClassIdentifier:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CChMtHz5UioVBUTAdiP6msXTWBHbXvrlL/lrziGncpCasO91pyWEhyk=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0d892c58b6fddaa4cb129dc82a1599bf, type: 3}
|
||||
m_Name: OnGameWin
|
||||
m_EditorClassIdentifier:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XH5L5HukAH7cHD+m8JFQuv4+Mj+RudGvKdJlEfvVp4JP/PFYo2XNvkY=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0d892c58b6fddaa4cb129dc82a1599bf, type: 3}
|
||||
m_Name: OnPlayerDamaged
|
||||
m_EditorClassIdentifier:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DnwWtS6sUHnS++rBiFqf1RewyFloSSTeXLbE2N2AYG05n2Ydap4Fwj0=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0d892c58b6fddaa4cb129dc82a1599bf, type: 3}
|
||||
m_Name: OnPlayerDied
|
||||
m_EditorClassIdentifier:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: By9K4Cr+VHyn1VhN1u6wwvxVZTtp7QakSbea38aO5PM+1kQ2/WvqpTk=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: d031a592f871059438eecfab967281c5, type: 3}
|
||||
m_Name: PlayerHealth
|
||||
m_EditorClassIdentifier:
|
||||
_value: 5
|
||||
_defaultValue: 5
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CXMat3n8Uine4euE0m+/PP+65iSUYO/kdbR4DpS1kOnmtBMCoGxpDXU=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0}
|
||||
m_Name: Players
|
||||
m_EditorClassIdentifier: Assembly-CSharp:Architecture.Core:TransformRuntimeSet
|
||||
Items: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DXkfsH+oAC3hF+c5rKlzwRKUa/umXiwsRPwwZnPWvvBN2Q0hWBBPanE=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: d031a592f871059438eecfab967281c5, type: 3}
|
||||
m_Name: Score
|
||||
m_EditorClassIdentifier:
|
||||
_value: 0
|
||||
_defaultValue: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WnIXtyivW3to970CZlNapENiq9hQEjUh+OqHFQyk+JfklXrmuCSE5fk=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0}
|
||||
m_Name: ScoreChanged
|
||||
m_EditorClassIdentifier: Assembly-CSharp:Architecture.Core:IntEvent
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WXNNvCuoVXP90sjsVOaNxagClFTPQl2e6lNPzXvzsn7Deww9UFDHgQ4=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:yousandi.cn,2023:
|
||||
--- !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: 0}
|
||||
m_Name: ScoreSettled
|
||||
m_EditorClassIdentifier: Assembly-CSharp:Architecture.Core:IntEvent
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Wi9KtSqvUiiCMcmSH6Ragy/X4YLegcpMnbruBTQrei7BTu3lyGrSIoc=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CHwdtCmvUy1YMyYqJ0W+W9h32R9zeWPvveDocc+XzRh7umlTkIVFsPU=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Architecture.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 无参事件通道(ScriptableObject 资产)。
|
||||
/// 系统 / UI 在 Inspector 中拖入同一个资产实例即可实现解耦通信,
|
||||
/// 彻底替代 static Action 事件与单例方法调用。
|
||||
/// 注意:资产实例是项目级资源,跨场景天然存活,无需 DontDestroyOnLoad。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/Game Event", fileName = "NewGameEvent")]
|
||||
public class GameEvent : ScriptableObject
|
||||
{
|
||||
private readonly List<GameEventListener> _listeners = new List<GameEventListener>();
|
||||
private event System.Action _codeListeners;
|
||||
|
||||
public void Raise()
|
||||
{
|
||||
// Inspector 监听器(设计师配置的 UnityEvent 响应)优先于代码监听器触发,
|
||||
// 保证表现层(UI 动画/音效)在逻辑层之前响应事件。
|
||||
for (int i = _listeners.Count - 1; i >= 0; i--)
|
||||
_listeners[i].OnEventRaised();
|
||||
_codeListeners?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Inspector 监听器(GameEventListener 组件)注册。</summary>
|
||||
public void RegisterListener(GameEventListener listener)
|
||||
{
|
||||
if (!_listeners.Contains(listener)) _listeners.Add(listener);
|
||||
}
|
||||
|
||||
public void UnregisterListener(GameEventListener listener)
|
||||
{
|
||||
if (_listeners.Contains(listener)) _listeners.Remove(listener);
|
||||
}
|
||||
|
||||
/// <summary>代码监听器(Action)注册,便于 MonoBehaviour 在 OnEnable 中订阅,
|
||||
/// 是替换 static Action 事件的主要入口。</summary>
|
||||
public void Register(System.Action listener)
|
||||
{
|
||||
if (listener != null) _codeListeners += listener;
|
||||
}
|
||||
|
||||
public void Unregister(System.Action listener)
|
||||
{
|
||||
if (listener != null) _codeListeners -= listener;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无参事件监听器(设计师友好,纯 Inspector 配置)。
|
||||
/// 监听某个 GameEvent,触发时调用绑定的 UnityEvent 响应。
|
||||
/// </summary>
|
||||
public class GameEventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameEvent _event;
|
||||
[SerializeField] private UnityEvent _response;
|
||||
|
||||
private void OnEnable() => _event?.RegisterListener(this);
|
||||
private void OnDisable() => _event?.UnregisterListener(this);
|
||||
|
||||
public void OnEventRaised() => _response?.Invoke();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CClOsC78Vi9/cIog8UeuUzecgGTFlu5AQ5iyvdkeMcyG1n6ns9R5ds8=
|
||||
guid: Dy4XvCj+VnPPG8II9z1gbnWGNyb39F6pQKS/CfIEjQntIaaEOm2XcP4=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Architecture.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 带载荷的泛型事件通道。用于在调用方之间传递数据,而无需 static 事件或单例。
|
||||
/// 具体类型(IntEvent / FloatEvent 等)可在 Inspector 右键 Create。
|
||||
/// 系统内部订阅用 event.Register(Action<T>);设计师驱动用对应 *EventListener 组件。
|
||||
/// </summary>
|
||||
public abstract class GameEvent<T> : ScriptableObject
|
||||
{
|
||||
private readonly List<Action<T>> _listeners = new List<Action<T>>();
|
||||
|
||||
public void Raise(T payload)
|
||||
{
|
||||
for (int i = _listeners.Count - 1; i >= 0; i--)
|
||||
_listeners[i]?.Invoke(payload);
|
||||
}
|
||||
|
||||
public void Register(Action<T> listener)
|
||||
{
|
||||
if (!_listeners.Contains(listener)) _listeners.Add(listener);
|
||||
}
|
||||
|
||||
public void Unregister(Action<T> listener)
|
||||
{
|
||||
if (_listeners.Contains(listener)) _listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/Int Event", fileName = "NewIntEvent")]
|
||||
public class IntEvent : GameEvent<int> { }
|
||||
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/Float Event", fileName = "NewFloatEvent")]
|
||||
public class FloatEvent : GameEvent<float> { }
|
||||
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/Vector3 Event", fileName = "NewVector3Event")]
|
||||
public class Vector3Event : GameEvent<Vector3> { }
|
||||
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/String Event", fileName = "NewStringEvent")]
|
||||
public class StringEvent : GameEvent<string> { }
|
||||
|
||||
[CreateAssetMenu(menuName = "Architecture/Events/GameObject Event", fileName = "NewGameObjectEvent")]
|
||||
public class GameObjectEvent : GameEvent<GameObject> { }
|
||||
|
||||
// ===== 带载荷的监听器组件(Inspector 驱动)=====
|
||||
|
||||
public class IntEventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private IntEvent _event;
|
||||
[SerializeField] private UnityEvent<int> _response;
|
||||
private void OnEnable() => _event?.Register(Respond);
|
||||
private void OnDisable() => _event?.Unregister(Respond);
|
||||
private void Respond(int v) => _response?.Invoke(v);
|
||||
}
|
||||
|
||||
public class FloatEventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FloatEvent _event;
|
||||
[SerializeField] private UnityEvent<float> _response;
|
||||
private void OnEnable() => _event?.Register(Respond);
|
||||
private void OnDisable() => _event?.Unregister(Respond);
|
||||
private void Respond(float v) => _response?.Invoke(v);
|
||||
}
|
||||
|
||||
public class Vector3EventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Vector3Event _event;
|
||||
[SerializeField] private UnityEvent<Vector3> _response;
|
||||
private void OnEnable() => _event?.Register(Respond);
|
||||
private void OnDisable() => _event?.Unregister(Respond);
|
||||
private void Respond(Vector3 v) => _response?.Invoke(v);
|
||||
}
|
||||
|
||||
public class GameObjectEventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObjectEvent _event;
|
||||
[SerializeField] private UnityEvent<GameObject> _response;
|
||||
private void OnEnable() => _event?.Register(Respond);
|
||||
private void OnDisable() => _event?.Unregister(Respond);
|
||||
private void Respond(GameObject v) => _response?.Invoke(v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字符串事件监听器组件。补齐 Int/Float/Vector3/GameObject 之后遗漏的 String 通道监听器,
|
||||
/// 使策划能在 Inspector 里为 StringEvent 连线响应(例如传递场景名、提示文本)。
|
||||
/// </summary>
|
||||
public class StringEventListener : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private StringEvent _event;
|
||||
[SerializeField] private UnityEvent<string> _response;
|
||||
private void OnEnable() => _event?.Register(Respond);
|
||||
private void OnDisable() => _event?.Unregister(Respond);
|
||||
private void Respond(string v) => _response?.Invoke(v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Wn0Y43n4BSqQmCBSQfcVumlArfA4ONJNN2iHuAFNyRze987Ocx2MGyY=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: B31O5CulAS1WrdnBvd8elA4AXddHMwLYIMjEJbxVqMOjBnaYzy4S8LU=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Architecture.Core;
|
||||
using Architecture.Variables;
|
||||
|
||||
namespace Architecture.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 一键生成 P1/P2 所需的 SO 通信资产(变量 / 事件 / 运行时集合)。
|
||||
/// 运行方式:Unity 顶部菜单 Architecture > Bootstrap Core Assets。
|
||||
/// 生成的资产位于 Assets/Architecture/Assets/,可安全删改、重新生成(已存在则跳过)。
|
||||
///
|
||||
/// 注意:本脚本仅生成"空壳资产",具体系统需在 Inspector 中将对应字段拖入这些资产实例,
|
||||
/// 才能建立解耦链路(详见 Docs/解耦架构重构方案.md)。
|
||||
/// </summary>
|
||||
public static class AssetBootstrap
|
||||
{
|
||||
private const string Root = "Assets/Architecture/Assets";
|
||||
|
||||
[MenuItem("Architecture/Bootstrap Core Assets")]
|
||||
public static void Bootstrap()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(Root))
|
||||
AssetDatabase.CreateFolder("Assets/Architecture", "Assets");
|
||||
|
||||
// 共享变量(同时设置 _value 与 _defaultValue,避免跨 Play Mode 状态泄漏)
|
||||
CreateInt("PlayerHealth", 5);
|
||||
CreateInt("Score", 0);
|
||||
|
||||
// 无参事件通道
|
||||
Create<GameEvent>("OnPlayerDied");
|
||||
Create<GameEvent>("OnGameOver");
|
||||
Create<GameEvent>("OnGameWin");
|
||||
Create<GameEvent>("OnPlayerDamaged");
|
||||
|
||||
// 带载荷事件通道
|
||||
Create<Vector3Event>("EchoReleased");
|
||||
Create<IntEvent>("ScoreChanged");
|
||||
Create<IntEvent>("ScoreSettled");
|
||||
|
||||
// 运行时集合(当前场景实体注册表)
|
||||
Create<TransformRuntimeSet>("Players");
|
||||
Create<TransformRuntimeSet>("Enemies");
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[AssetBootstrap] 已生成核心 SO 资产到 {Root}");
|
||||
}
|
||||
|
||||
private static T Create<T>(string name) where T : ScriptableObject
|
||||
{
|
||||
var path = $"{Root}/{name}.asset";
|
||||
var existing = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (existing != null)
|
||||
{
|
||||
Debug.Log($"[AssetBootstrap] 已存在,跳过:{path}");
|
||||
return existing;
|
||||
}
|
||||
var asset = ScriptableObject.CreateInstance<T>();
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
return asset;
|
||||
}
|
||||
|
||||
private static void CreateInt(string name, int value)
|
||||
{
|
||||
var asset = Create<IntVariable>(name);
|
||||
var so = new SerializedObject(asset);
|
||||
so.FindProperty("_value").intValue = value;
|
||||
so.FindProperty("_defaultValue").intValue = value;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Dn4fvSL+Bi5qpJQPKTiLAFU3fgR+KLt4w0Ql57XB14ROp4pNfqn9n84=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Architecture.Variables;
|
||||
|
||||
namespace Architecture.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 变量资产的 Inspector 绘制器:在字段右侧实时显示当前值(含 Play 模式运行时值),
|
||||
/// 设计师无需打开脚本即可看到数值,降低沟通成本。
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(FloatVariable))]
|
||||
[CustomPropertyDrawer(typeof(IntVariable))]
|
||||
[CustomPropertyDrawer(typeof(BoolVariable))]
|
||||
[CustomPropertyDrawer(typeof(Vector3Variable))]
|
||||
public class VariableDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
var target = property.objectReferenceValue as ScriptableObject;
|
||||
if (target != null)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
so.Update();
|
||||
var valueProp = so.FindProperty("_value");
|
||||
Rect objRect = new Rect(position.x, position.y, position.width * 0.58f, position.height);
|
||||
Rect valRect = new Rect(position.x + position.width * 0.60f, position.y, position.width * 0.40f, position.height);
|
||||
EditorGUI.ObjectField(objRect, property, GUIContent.none);
|
||||
if (valueProp != null)
|
||||
{
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUI.PropertyField(valRect, valueProp, GUIContent.none);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.ObjectField(position, property, label);
|
||||
}
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: BnMa4CioUihgI9MvBm9J06Z6TNiVXXGlVT3vne3vk2CEiePGAFaObuc=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DXkasC+sU3ihXHypaAzLi0kYmkhZiiQpNsOF3Y4Ksr+Tt5M8BpbIw1w=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Architecture.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时集合:替代 FindObjectsOfType / 单例列表,零单例开销地跟踪场景实体。
|
||||
/// 实体在 OnEnable 注册、OnDisable 注销(见下方 RuntimeSetRegistrar)。
|
||||
/// 资产实例是项目级资源,跨场景存活,可安全被任何系统读取。
|
||||
/// </summary>
|
||||
public abstract class RuntimeSet<T> : ScriptableObject
|
||||
{
|
||||
public List<T> Items = new List<T>();
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
if (item != null && !Items.Contains(item)) Items.Add(item);
|
||||
}
|
||||
|
||||
public void Remove(T item)
|
||||
{
|
||||
if (item != null && Items.Contains(item)) Items.Remove(item);
|
||||
}
|
||||
|
||||
public int Count => Items.Count;
|
||||
public T this[int i] => Items[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用 Transform 集合,作为"当前场景实体"的轻量注册表(玩家、敌人等)。
|
||||
/// 领域专用集合(如 EnemyRuntimeSet : RuntimeSet<EnemyAI>)放在对应领域文件夹,
|
||||
/// 继承本类即可,无需改动核心。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Runtime Sets/Transform Set", fileName = "NewTransformSet")]
|
||||
public class TransformRuntimeSet : RuntimeSet<Transform> { }
|
||||
|
||||
/// <summary>
|
||||
/// 挂载到任意预制体,使其自动注册进指定 RuntimeSet。
|
||||
/// 这样系统只需遍历集合,永不需要 GameObject.Find / FindObjectsOfType。
|
||||
/// </summary>
|
||||
public class RuntimeSetRegistrar : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TransformRuntimeSet _set;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_set != null) _set.Add(transform);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_set != null) _set.Remove(transform);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 泛型注册器:领域专用强类型集合(如 <c>EnemyRuntimeSet : RuntimeSet<EnemyAI></c>)使用。
|
||||
/// 自动 <c>GetComponent<T>()</c> 并加入对应集合,避免用 Transform 集合再手动转型。
|
||||
/// 因 Unity 对泛型 MonoBehaviour 需具体闭包类型才能挂载,领域文件夹内写一行具体子类即可:
|
||||
/// <code>public class EnemySetRegistrar : RuntimeSetRegistrar<EnemyAI> { }</code>
|
||||
/// </summary>
|
||||
public class RuntimeSetRegistrar<T> : MonoBehaviour where T : Component
|
||||
{
|
||||
[SerializeField] private RuntimeSet<T> _set;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_set != null) _set.Add(GetComponent<T>());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_set != null) _set.Remove(GetComponent<T>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: By8XsSukWyonwLQZ/3RNaE8FAk+70HcGhhnyFRI8oY/tcLIntpkDvH4=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CXMW4CP8BnnBCuaEto3WYyr2C3RLoOC5Jd5fjwZGKcsuUekgm8W5KFY=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace Architecture.Variables
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享布尔变量(ScriptableObject 资产)。常用于暂停、开关等状态。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Variables/Bool", fileName = "NewBoolVariable")]
|
||||
public class BoolVariable : ScriptableObject, Architecture.IVariable
|
||||
{
|
||||
[SerializeField] private bool _value;
|
||||
|
||||
[Tooltip("进入 Play Mode 时的初始值,由 VariableRegistry 在每次启动游戏时自动恢复,防止跨局状态泄漏")]
|
||||
[SerializeField] private bool _defaultValue;
|
||||
|
||||
public bool Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (_value == value) return;
|
||||
_value = value;
|
||||
OnValueChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<bool> OnValueChanged;
|
||||
|
||||
public void SetValue(bool value) => Value = value;
|
||||
|
||||
/// <summary>将运行时值重置为 _defaultValue(每次进入 Play Mode 时由 VariableRegistry 自动调用)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
_value = _defaultValue;
|
||||
OnValueChanged?.Invoke(_value);
|
||||
}
|
||||
|
||||
[ContextMenu("Reset To Default")]
|
||||
public void Reset() => ResetToDefault();
|
||||
|
||||
private void OnEnable() => VariableRegistry.Register(this);
|
||||
private void OnDisable()
|
||||
{
|
||||
VariableRegistry.Unregister(this);
|
||||
OnValueChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DXsdsCquB3geiEyhIkY2aFLJpbGuThlr6ky9pgS1Nnrno7aw3ZWk4Uc=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace Architecture.Variables
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享浮点变量(ScriptableObject 资产)。
|
||||
/// 任何系统可读取 / 写入,并由 OnValueChanged 事件驱动 UI 刷新,
|
||||
/// 替代 MonoBehaviour 字段跨系统传递与逐帧反射读取。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Variables/Float", fileName = "NewFloatVariable")]
|
||||
public class FloatVariable : ScriptableObject, Architecture.IVariable
|
||||
{
|
||||
[SerializeField] private float _value;
|
||||
|
||||
[Tooltip("进入 Play Mode 时的初始值,由 VariableRegistry 在每次启动游戏时自动恢复,防止跨局状态泄漏")]
|
||||
[SerializeField] private float _defaultValue;
|
||||
|
||||
public float Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (Mathf.Approximately(_value, value)) return;
|
||||
_value = value;
|
||||
OnValueChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<float> OnValueChanged;
|
||||
|
||||
public void SetValue(float value) => Value = value;
|
||||
public void ApplyChange(float amount) => Value += amount;
|
||||
|
||||
/// <summary>将运行时值重置为 _defaultValue(每次进入 Play Mode 时由 VariableRegistry 自动调用)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
_value = _defaultValue;
|
||||
OnValueChanged?.Invoke(_value);
|
||||
}
|
||||
|
||||
[ContextMenu("Reset To Default")]
|
||||
public void Reset() => ResetToDefault();
|
||||
|
||||
private void OnEnable() => VariableRegistry.Register(this);
|
||||
private void OnDisable()
|
||||
{
|
||||
VariableRegistry.Unregister(this);
|
||||
OnValueChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DntJsS6uBnP85uXqLRZji/q5FaD10/2h7b+TePWWB+tWhBvOfF7TELg=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace Architecture.Variables
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享整数变量(ScriptableObject 资产)。常用于血量、得分、魂灵数等。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Variables/Int", fileName = "NewIntVariable")]
|
||||
public class IntVariable : ScriptableObject, Architecture.IVariable
|
||||
{
|
||||
[SerializeField] private int _value;
|
||||
|
||||
[Tooltip("进入 Play Mode 时的初始值,由 VariableRegistry 在每次启动游戏时自动恢复,防止跨局状态泄漏")]
|
||||
[SerializeField] private int _defaultValue;
|
||||
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (_value == value) return;
|
||||
_value = value;
|
||||
OnValueChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<int> OnValueChanged;
|
||||
|
||||
public void SetValue(int value) => Value = value;
|
||||
public void ApplyChange(int amount) => Value += amount;
|
||||
|
||||
/// <summary>将运行时值重置为 _defaultValue(每次进入 Play Mode 时由 VariableRegistry 自动调用)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
_value = _defaultValue;
|
||||
OnValueChanged?.Invoke(_value);
|
||||
}
|
||||
|
||||
[ContextMenu("Reset To Default")]
|
||||
public void Reset() => ResetToDefault();
|
||||
|
||||
private void OnEnable() => VariableRegistry.Register(this);
|
||||
private void OnDisable()
|
||||
{
|
||||
VariableRegistry.Unregister(this);
|
||||
OnValueChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: W3octHuoWnnZAVrTuCOazcbeHxKYqo5abD3dZ/KJEBmkydHqGDyV9qE=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Architecture
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享变量统一接口:所有 Variable 资产实现它,注册表才能统一重置。
|
||||
/// </summary>
|
||||
public interface IVariable
|
||||
{
|
||||
void ResetToDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 共享变量注册表 —— 运行时状态防泄漏的核心。
|
||||
///
|
||||
/// <b>问题</b>:ScriptableObject 资产在 Editor 下跨 Play Mode 域重载会持久化其序列化值。
|
||||
/// 例如 PlayerHealth.Value 在游玩中变为 0 后退出 Play Mode,若残留为 0,
|
||||
/// 下次进入 Play Mode 可能直接触发死亡(或分数残留、开关状态错乱)。
|
||||
///
|
||||
/// <b>解决</b>:每个 Variable 在 OnEnable 时自动注册;每次进入 Play Mode(域加载前)
|
||||
/// 由 [RuntimeInitializeOnLoadMethod] 统一调用 ResetToDefault(),
|
||||
/// 将运行时值恢复到设计器设置的 _defaultValue,杜绝跨局状态泄漏。
|
||||
///
|
||||
/// 注意:RuntimeInitializeLoadType.BeforeSceneLoad 在整个 Play Mode 生命周期内只触发一次,
|
||||
/// 因此同一局内跨场景加载(SceneManager.LoadScene)不会重置变量,分数等跨场景共享状态可正常保留。
|
||||
/// </summary>
|
||||
public static class VariableRegistry
|
||||
{
|
||||
private static readonly HashSet<IVariable> _variables = new HashSet<IVariable>();
|
||||
|
||||
public static void Register(IVariable variable)
|
||||
{
|
||||
_variables.Add(variable);
|
||||
// 注册即重置:解决 [RuntimeInitializeOnLoadMethod(BeforeSceneLoad)] 在 SO 资产
|
||||
// 尚未 OnEnable 注册前就执行 ResetAll(此时 _variables 为空)导致重置无效的问题。
|
||||
// 无论 ResetAll 何时触发,变量在「上线」瞬间都会被恢复到 _defaultValue,
|
||||
// 杜绝退出 Play Mode 后 PlayerHealth 残留 0、再次进入直接触发死亡的跨局泄漏。
|
||||
variable.ResetToDefault();
|
||||
}
|
||||
public static void Unregister(IVariable variable) => _variables.Remove(variable);
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
public static void ResetAll()
|
||||
{
|
||||
foreach (var v in _variables)
|
||||
v.ResetToDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DHMdty+tWygUNpW3ZTnsgECvyUsf7TZS/Bowuhd8nTjV1gGiSvKO6z4=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace Architecture.Variables
|
||||
{
|
||||
/// <summary>
|
||||
/// 共享 Vector3 变量(ScriptableObject 资产)。常用于位置、回声中心等。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Architecture/Variables/Vector3", fileName = "NewVector3Variable")]
|
||||
public class Vector3Variable : ScriptableObject, Architecture.IVariable
|
||||
{
|
||||
[SerializeField] private Vector3 _value;
|
||||
|
||||
[Tooltip("进入 Play Mode 时的初始值,由 VariableRegistry 在每次启动游戏时自动恢复,防止跨局状态泄漏")]
|
||||
[SerializeField] private Vector3 _defaultValue;
|
||||
|
||||
public Vector3 Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
_value = value;
|
||||
OnValueChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<Vector3> OnValueChanged;
|
||||
|
||||
public void SetValue(Vector3 value) => Value = value;
|
||||
|
||||
/// <summary>将运行时值重置为 _defaultValue(每次进入 Play Mode 时由 VariableRegistry 自动调用)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
_value = _defaultValue;
|
||||
OnValueChanged?.Invoke(_value);
|
||||
}
|
||||
|
||||
[ContextMenu("Reset To Default")]
|
||||
public void Reset() => ResetToDefault();
|
||||
|
||||
private void OnEnable() => VariableRegistry.Register(this);
|
||||
private void OnDisable()
|
||||
{
|
||||
VariableRegistry.Unregister(this);
|
||||
OnValueChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: BnIa4SyuVyiKWphMPOJ0zNHa5e8eoX06FNossjB+7+Ma8SaSRkVd14E=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -3,6 +3,7 @@ using UnityEditor;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using GameFramework;
|
||||
using Architecture.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 一键搭建游戏 UI:HUD Canvas + 游戏结果 Canvas + 主菜单标题。
|
||||
@@ -300,6 +301,13 @@ public static class UIBuilder
|
||||
so.FindProperty("loseConfirmButton").objectReferenceValue = loseBtn.GetComponent<Button>();
|
||||
so.FindProperty("loseTitleText").objectReferenceValue = loseTitle.GetComponent<Text>();
|
||||
so.FindProperty("loseScoreText").objectReferenceValue = loseScore.GetComponent<Text>();
|
||||
|
||||
// 连接 SO 事件通道(与 GameManager 发布方使用同一资产,建立解耦监听)
|
||||
// 注意:GameResultScreen 现已明确只负责「胜利」,不再订阅 OnGameOver(失败由 GameLostOverlay 接管)。
|
||||
var onGameWin = AssetDatabase.LoadAssetAtPath<GameEvent>("Assets/Architecture/Assets/OnGameWin.asset");
|
||||
if (onGameWin != null) so.FindProperty("onGameWinEvent").objectReferenceValue = onGameWin;
|
||||
else Debug.LogWarning("[UIBuilder] 未找到 OnGameWin 资产,请先运行 Architecture > Bootstrap Core Assets");
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
Debug.Log("[UIBuilder] ResultScreen Canvas 搭建完成");
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
[Tooltip("边缘发光材质(使用 AbyssEdgeGlow shader)。留空则自动创建。")]
|
||||
[SerializeField] private Material edgeGlowMaterial;
|
||||
|
||||
[Tooltip("边缘发光着色器(留空则尝试 Shader.Find 兜底;建议拖入 Assets/Light/shaders/AbyssEdgeGlow.shader 以避免构建裁剪)")]
|
||||
[SerializeField] private Shader edgeGlowShader;
|
||||
|
||||
private const string GeneratedName = "Generated_CliffWalls";
|
||||
|
||||
[ContextMenu("Build Cliff Walls")]
|
||||
@@ -105,7 +108,7 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
else
|
||||
{
|
||||
// 尝试自动创建 AbyssEdgeGlow 材质
|
||||
Shader edgeShader = Shader.Find("IndianOcean/AbyssEdgeGlow");
|
||||
Shader edgeShader = edgeGlowShader != null ? edgeGlowShader : Shader.Find("IndianOcean/AbyssEdgeGlow");
|
||||
if (edgeShader != null)
|
||||
{
|
||||
Material mat = new Material(edgeShader);
|
||||
|
||||
@@ -54,6 +54,9 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
[Tooltip("边缘发光材质(使用 AbyssEdgeGlow shader)。留空则自动创建。")]
|
||||
[SerializeField] Material edgeGlowMaterial;
|
||||
|
||||
[Tooltip("边缘发光着色器(留空则尝试 Shader.Find 兜底;建议拖入 Assets/Light/shaders/AbyssEdgeGlow.shader 以避免构建裁剪)")]
|
||||
[SerializeField] private Shader edgeGlowShader;
|
||||
|
||||
[Header("优化")]
|
||||
[Tooltip("Douglas-Peucker 简化容差(世界单位)。0 = 不简化。\n" +
|
||||
"建议 0.05~0.2,减少顶点数同时保持轮廓形状。")]
|
||||
@@ -177,7 +180,7 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
}
|
||||
else
|
||||
{
|
||||
Shader glowShader = Shader.Find("IndianOcean/AbyssEdgeGlow");
|
||||
Shader glowShader = edgeGlowShader != null ? edgeGlowShader : Shader.Find("IndianOcean/AbyssEdgeGlow");
|
||||
if (glowShader != null)
|
||||
{
|
||||
Material mat = new Material(glowShader);
|
||||
|
||||
@@ -3778,11 +3778,13 @@ MonoBehaviour:
|
||||
mainMenuButton: {fileID: 549601800}
|
||||
mainMenuButtonImage: {fileID: 549601797}
|
||||
lostSFX: {fileID: 0}
|
||||
onGameOverEvent: {fileID: 11400000, guid: 7bc1fd1afe8d2774fb5cc757ef12895b, type: 2}
|
||||
titleFadeDuration: 1.5
|
||||
ripplePositionAmplitude: 8
|
||||
rippleScaleAmplitude: 0.08
|
||||
rippleFrequency: 15
|
||||
rippleMaterial: {fileID: 2100000, guid: 2d4ef2f4a2f0eb2479ece51f60be054d, type: 2}
|
||||
waterRippleShader: {fileID: 0}
|
||||
buttonFadeDelay: 2
|
||||
buttonFadeDuration: 0.5
|
||||
--- !u!4 &73055215
|
||||
@@ -7504,6 +7506,8 @@ MonoBehaviour:
|
||||
skillLanternIcon: {fileID: 1435658950}
|
||||
skillLanternCD: {fileID: 2099612955}
|
||||
pauseButton: {fileID: 0}
|
||||
scoreChangedEvent: {fileID: 11400000, guid: f9b9156804ea63d44a675e3485b43f88, type: 2}
|
||||
playerHealthVar: {fileID: 11400000, guid: 6952ca1b8005ae541bf172dd398d4cc7, type: 2}
|
||||
--- !u!114 &160213092
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -25269,6 +25273,55 @@ BoxCollider:
|
||||
serializedVersion: 3
|
||||
m_Size: {x: 0.8, y: 3, z: 0.41948986}
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &535625614
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 7
|
||||
m_Component:
|
||||
- component: {fileID: 535625616}
|
||||
- component: {fileID: 535625615}
|
||||
m_Layer: 0
|
||||
m_HasEditorInfo: 1
|
||||
m_Name: DamageFlashOverlay
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &535625615
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 535625614}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 356da6cdceb3f0144b7782901b81314d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
flashAlpha: 0.8
|
||||
fadeDuration: 0.4
|
||||
innerRadius: 0.8
|
||||
onPlayerDamagedEvent: {fileID: 11400000, guid: 1690413250c5d464eb84008d45cb88cc, type: 2}
|
||||
--- !u!4 &535625616
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 535625614}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 424.2601, y: 195.52283, z: 80.32833}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &539669134
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -67656,6 +67709,11 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0bc49c6d42cb88a4c981d949a1cd87cd, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onGameOverEvent: {fileID: 11400000, guid: 7bc1fd1afe8d2774fb5cc757ef12895b, type: 2}
|
||||
onGameWinEvent: {fileID: 11400000, guid: c4daa9c5c7532f3409b33e98e40fec55, type: 2}
|
||||
onPlayerDiedEvent: {fileID: 11400000, guid: 8eee0c771c042c44a88f48756f3717b5, type: 2}
|
||||
enemiesSet: {fileID: 11400000, guid: f9b1d1725469e0c40a31cf48dbc6439e, type: 2}
|
||||
enemyManagerRef: {fileID: 340526138}
|
||||
gameState: 0
|
||||
lightShrinkDuration: 2
|
||||
lostFadeOutDuration: 1
|
||||
@@ -101243,6 +101301,14 @@ PrefabInstance:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 230515964846317157, guid: a60578acb930546489d4f5b6d598bce8, type: 3}
|
||||
propertyPath: ringShader
|
||||
value:
|
||||
objectReference: {fileID: 4800000, guid: 67f78fa2aa44cba4cadd869535881366, type: 3}
|
||||
- target: {fileID: 230515964846317157, guid: a60578acb930546489d4f5b6d598bce8, type: 3}
|
||||
propertyPath: echoReleasedEvent
|
||||
value:
|
||||
objectReference: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
- target: {fileID: 4224307220934746970, guid: a60578acb930546489d4f5b6d598bce8, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 8.063
|
||||
@@ -101334,3 +101400,4 @@ SceneRoots:
|
||||
- {fileID: 1115832006}
|
||||
- {fileID: 2106106797}
|
||||
- {fileID: 2084288743}
|
||||
- {fileID: 535625616}
|
||||
|
||||
@@ -931,6 +931,11 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0bc49c6d42cb88a4c981d949a1cd87cd, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onGameOverEvent: {fileID: 11400000, guid: 7bc1fd1afe8d2774fb5cc757ef12895b, type: 2}
|
||||
onGameWinEvent: {fileID: 11400000, guid: c4daa9c5c7532f3409b33e98e40fec55, type: 2}
|
||||
onPlayerDiedEvent: {fileID: 11400000, guid: 8eee0c771c042c44a88f48756f3717b5, type: 2}
|
||||
enemiesSet: {fileID: 11400000, guid: f9b1d1725469e0c40a31cf48dbc6439e, type: 2}
|
||||
enemyManagerRef: {fileID: 0}
|
||||
gameState: 0
|
||||
lightShrinkDuration: 2
|
||||
lostFadeOutDuration: 1
|
||||
@@ -1173,6 +1178,8 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 3e98896ae36c7a54ba83c4e9eb532752, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
scoreChangedEvent: {fileID: 11400000, guid: f9b9156804ea63d44a675e3485b43f88, type: 2}
|
||||
scoreSettledEvent: {fileID: 11400000, guid: eee0021c0c646f3469a86bef9c590cff, type: 2}
|
||||
defaultPlayerName: Player
|
||||
--- !u!4 &646400229
|
||||
Transform:
|
||||
@@ -4723,6 +4730,8 @@ MonoBehaviour:
|
||||
skillLanternIcon: {fileID: 0}
|
||||
skillLanternCD: {fileID: 0}
|
||||
pauseButton: {fileID: 0}
|
||||
scoreChangedEvent: {fileID: 0}
|
||||
playerHealthVar: {fileID: 0}
|
||||
--- !u!114 &1852443089
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -4947,6 +4956,8 @@ MonoBehaviour:
|
||||
skillLanternIcon: {fileID: 1675213501}
|
||||
skillLanternCD: {fileID: 1498112937}
|
||||
pauseButton: {fileID: 90840731}
|
||||
scoreChangedEvent: {fileID: 0}
|
||||
playerHealthVar: {fileID: 0}
|
||||
--- !u!114 &1890235558
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -268,6 +268,10 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 11400000, guid: 1690413250c5d464eb84008d45cb88cc, type: 2}
|
||||
onPlayerDiedEvent: {fileID: 11400000, guid: 8eee0c771c042c44a88f48756f3717b5, type: 2}
|
||||
healthVar: {fileID: 11400000, guid: 6952ca1b8005ae541bf172dd398d4cc7, type: 2}
|
||||
maxHealthVar: {fileID: 11400000, guid: 6952ca1b8005ae541bf172dd398d4cc7, type: 2}
|
||||
maxHealth: 5
|
||||
deathEffect: {fileID: 4806121257990350900, guid: f19c76183b5e22e44a73655dc18f1a92, type: 3}
|
||||
isPlayer: 1
|
||||
@@ -343,6 +347,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a44142f1063e1d4dbab7b851d31d258, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
echoKey: 101
|
||||
expandSpeed: 15
|
||||
maxRadius: 30
|
||||
@@ -355,6 +360,7 @@ MonoBehaviour:
|
||||
ringYOffset: 0.1
|
||||
ringFadeTime: 2
|
||||
ringExpandSpeed: 18
|
||||
ringShader: {fileID: 4800000, guid: 67f78fa2aa44cba4cadd869535881366, type: 3}
|
||||
cooldown: 10
|
||||
--- !u!114 &4208162987965796937
|
||||
MonoBehaviour:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using IndianOceanAssets.Engine2_5D;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -27,15 +28,32 @@ namespace GameFramework
|
||||
private float _currentAlpha;
|
||||
private bool _isFlashing;
|
||||
|
||||
[Header("SO 事件通道(替代 static HealthSystem.onPlayerDamaged 事件)")]
|
||||
[SerializeField] private GameEvent onPlayerDamagedEvent;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (onPlayerDamagedEvent == null)
|
||||
Debug.LogWarning("[DamageFlashOverlay] onPlayerDamagedEvent 未接线!请在场景中预置已接线的 DamageFlashOverlay 实例,否则受击不闪红。", this);
|
||||
else
|
||||
onPlayerDamagedEvent.Register(OnPlayerDamaged);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (onPlayerDamagedEvent != null)
|
||||
onPlayerDamagedEvent.Unregister(OnPlayerDamaged);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (onPlayerDamagedEvent == null)
|
||||
Debug.LogWarning($"[DamageFlashOverlay] On Player Damaged Event 未接线({gameObject.name})。受击泛红不会出现。", this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
CreateOverlay();
|
||||
HealthSystem.onPlayerDamaged += OnPlayerDamaged;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
HealthSystem.onPlayerDamaged -= OnPlayerDamaged;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
||||
+37
-75
@@ -2,12 +2,14 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using IndianOceanAssets.Engine2_5D;
|
||||
using Architecture.Core;
|
||||
using Architecture.Variables;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏内 HUD —— 魂灵计数 + 生命图标 + 技能CD。
|
||||
///
|
||||
///
|
||||
/// 布局:
|
||||
/// - 左上角:收集到的魂灵(icon + 数字)
|
||||
/// - 左下角:5个生命图标
|
||||
@@ -36,12 +38,14 @@ namespace GameFramework
|
||||
[Header("暂停")]
|
||||
[SerializeField] private Button pauseButton;
|
||||
|
||||
[Header("SO 事件通道 / 变量")]
|
||||
[SerializeField] private IntEvent scoreChangedEvent;
|
||||
[SerializeField] private IntVariable playerHealthVar;
|
||||
|
||||
private HealthSystem _playerHealth;
|
||||
private SpiritLanternSystem _lanternSystem;
|
||||
private EchoSystem _echoSystem;
|
||||
private PlayerController _playerController;
|
||||
private int _maxHealth = 5;
|
||||
private bool _subscribedScoreEvent = false;
|
||||
private int _lastSoulCount = -1;
|
||||
|
||||
void Start()
|
||||
@@ -62,59 +66,34 @@ namespace GameFramework
|
||||
if (soulIcon != null)
|
||||
SoulIconRect = soulIcon.rectTransform;
|
||||
|
||||
// 订阅分数事件(魂灵计数复用分数系统)
|
||||
if (ScoreManager.Instance != null)
|
||||
// 订阅分数事件(魂灵计数复用分数系统):无条件注册,事件资产始终存在;
|
||||
// 初始值若 ScoreManager 已就绪则取实时分,否则先显示 0,首个 ScoreChanged 会刷新。
|
||||
scoreChangedEvent?.Register(UpdateSoulCount);
|
||||
UpdateSoulCount(ScoreManager.Instance != null ? ScoreManager.Instance.CurrentScore : 0);
|
||||
|
||||
// 订阅玩家血量变量(替代反射读取私有字段 + Update 条件 Find 回退)
|
||||
if (playerHealthVar != null)
|
||||
{
|
||||
ScoreManager.onScoreChanged += UpdateSoulCount;
|
||||
_subscribedScoreEvent = true;
|
||||
UpdateSoulCount(ScoreManager.Instance.CurrentScore);
|
||||
playerHealthVar.OnValueChanged += UpdateLifeIcons;
|
||||
UpdateLifeIcons(playerHealthVar.Value);
|
||||
}
|
||||
|
||||
UpdateLifeIcons();
|
||||
|
||||
// 自动创建受击泛红特效(如果场景中没有)
|
||||
// 受击泛红特效:场景需预置已接线的 DamageFlashOverlay 实例(不再自动创建,
|
||||
// 否则会生成一个事件为 null 的实例,既无效果又掩盖「未接线」问题)
|
||||
if (FindObjectOfType<DamageFlashOverlay>() == null)
|
||||
{
|
||||
var overlayObj = new GameObject("DamageFlashOverlay");
|
||||
overlayObj.AddComponent<DamageFlashOverlay>();
|
||||
}
|
||||
Debug.LogWarning("[GameHUD] 场景中未找到 DamageFlashOverlay,受击泛红特效不会显示。请在场景中放置一个、并把它 On Player Damaged Event 字段接上 OnPlayerDamaged 资产。", this);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_subscribedScoreEvent)
|
||||
{
|
||||
ScoreManager.onScoreChanged -= UpdateSoulCount;
|
||||
_subscribedScoreEvent = false;
|
||||
}
|
||||
scoreChangedEvent?.Unregister(UpdateSoulCount);
|
||||
if (playerHealthVar != null)
|
||||
playerHealthVar.OnValueChanged -= UpdateLifeIcons;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 延迟订阅 ScoreManager(跨场景后 ScoreManager 可能才创建)
|
||||
if (!_subscribedScoreEvent && ScoreManager.Instance != null)
|
||||
{
|
||||
ScoreManager.onScoreChanged += UpdateSoulCount;
|
||||
_subscribedScoreEvent = true;
|
||||
UpdateSoulCount(ScoreManager.Instance.CurrentScore);
|
||||
Debug.Log("[GameHUD] 延迟订阅 ScoreManager.onScoreChanged 成功");
|
||||
}
|
||||
|
||||
// 延迟查找玩家(GameSpawnManager 可能比 HUD 晚创建玩家)
|
||||
if (_playerHealth == null)
|
||||
{
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
if (player != null)
|
||||
{
|
||||
_playerHealth = player.GetComponent<HealthSystem>();
|
||||
_lanternSystem = player.GetComponent<SpiritLanternSystem>();
|
||||
_echoSystem = player.GetComponent<EchoSystem>();
|
||||
_playerController = player.GetComponent<PlayerController>();
|
||||
Debug.Log("[GameHUD] 延迟找到玩家,已绑定组件");
|
||||
}
|
||||
}
|
||||
|
||||
UpdateLifeIcons();
|
||||
// 事件已在 Start 中无条件订阅,无需逐帧延迟订阅(避免反模式)。
|
||||
UpdateSkillCooldowns();
|
||||
}
|
||||
|
||||
@@ -140,17 +119,14 @@ namespace GameFramework
|
||||
|
||||
/// <summary>
|
||||
/// 更新生命图标(左下角)。
|
||||
/// 根据当前血量显示/隐藏对应图标。
|
||||
/// 直接读取 PlayerHealth 共享变量,由 OnValueChanged 事件驱动,无反射、无逐帧 Find。
|
||||
/// </summary>
|
||||
private void UpdateLifeIcons()
|
||||
private void UpdateLifeIcons(int value)
|
||||
{
|
||||
if (_playerHealth == null || lifeIcons == null || lifeIcons.Length == 0) return;
|
||||
|
||||
// 获取当前血量
|
||||
var healthField = typeof(HealthSystem).GetField("health",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
int currentHealth = healthField != null ? (int)healthField.GetValue(_playerHealth) : _maxHealth;
|
||||
if (lifeIcons == null || lifeIcons.Length == 0) return;
|
||||
if (playerHealthVar == null) return;
|
||||
|
||||
int currentHealth = value;
|
||||
for (int i = 0; i < lifeIcons.Length; i++)
|
||||
{
|
||||
if (lifeIcons[i] != null)
|
||||
@@ -160,43 +136,29 @@ namespace GameFramework
|
||||
|
||||
/// <summary>
|
||||
/// 更新技能CD遮罩(右下角)。
|
||||
/// 使用 Image.fillAmount 实现圆形CD效果。
|
||||
/// 通过各系统的公共只读属性读取冷却状态,替代反射读取私有字段。
|
||||
/// </summary>
|
||||
private void UpdateSkillCooldowns()
|
||||
{
|
||||
// 冲刺 CD(从 PlayerController 读取 rollCooldown + lastRollTime)
|
||||
// 冲刺 CD(从 PlayerController.RollCooldown 读取)
|
||||
if (_playerController != null)
|
||||
{
|
||||
var cdField = typeof(PlayerController).GetField("rollCooldown",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var lastField = typeof(PlayerController).GetField("lastRollTime",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
float sprintCD = cdField != null ? (float)cdField.GetValue(_playerController) : 1f;
|
||||
float lastRoll = lastField != null ? (float)lastField.GetValue(_playerController) : -999f;
|
||||
float remaining = Mathf.Max(0f, (lastRoll + sprintCD) - Time.time);
|
||||
UpdateSkillCD(skillSprintCD, skillSprintIcon, remaining, sprintCD);
|
||||
var cd = _playerController.RollCooldown;
|
||||
UpdateSkillCD(skillSprintCD, skillSprintIcon, cd.remaining, cd.total);
|
||||
}
|
||||
|
||||
// 摇铃 CD(从 EchoSystem 读取 cooldown + _lastEchoTime)
|
||||
// 摇铃 CD(从 EchoSystem.BellCooldown 读取)
|
||||
if (_echoSystem != null)
|
||||
{
|
||||
var cdField = typeof(EchoSystem).GetField("cooldown",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var lastField = typeof(EchoSystem).GetField("_lastEchoTime",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
float bellCD = cdField != null ? (float)cdField.GetValue(_echoSystem) : 2f;
|
||||
float lastTime = lastField != null ? (float)lastField.GetValue(_echoSystem) : -999f;
|
||||
float remaining = Mathf.Max(0f, (lastTime + bellCD) - Time.time);
|
||||
UpdateSkillCD(skillBellCD, skillBellIcon, remaining, bellCD);
|
||||
var cd = _echoSystem.BellCooldown;
|
||||
UpdateSkillCD(skillBellCD, skillBellIcon, cd.remaining, cd.total);
|
||||
}
|
||||
|
||||
// 灵灯 CD(从 SpiritLanternSystem 读取)
|
||||
// 灵灯 CD(SpiritLanternSystem 已暴露公共 Cooldown / CooldownRemaining,无反射)
|
||||
if (_lanternSystem != null)
|
||||
{
|
||||
float remaining = _lanternSystem.CooldownRemaining;
|
||||
var cdField = typeof(SpiritLanternSystem).GetField("cooldown",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
float maxCD = cdField != null ? (float)cdField.GetValue(_lanternSystem) : 3f;
|
||||
float maxCD = _lanternSystem.Cooldown;
|
||||
UpdateSkillCD(skillLanternCD, skillLanternIcon, remaining, maxCD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -12,14 +13,12 @@ namespace GameFramework
|
||||
/// 1. 在 Gameplay 场景里创建一个空物体,挂上本脚本
|
||||
/// 2. 运行一次游戏(或点 Editor 按钮),脚本会自动构建子 UI
|
||||
/// 3. 停止运行后,子 UI 留在场景中,可自由编辑样式/位置/精灵
|
||||
/// 4. 运行时由 GameManager 调用 GameLostOverlay.Show() 激活
|
||||
/// 4. 运行时订阅 OnGameOver 事件自激活(不再由 GameManager 直接调用)
|
||||
///
|
||||
/// 所有 UI 元素均为 [SerializeField],可在 Inspector 中替换。
|
||||
/// </summary>
|
||||
public class GameLostOverlay : MonoBehaviour
|
||||
{
|
||||
private static GameLostOverlay _instance;
|
||||
|
||||
// ====== Canvas ======
|
||||
[Header("Canvas(留空则自动创建)")]
|
||||
[SerializeField] private Canvas lostCanvas;
|
||||
@@ -43,6 +42,9 @@ namespace GameFramework
|
||||
[Header("音效")]
|
||||
[SerializeField] private AudioData lostSFX;
|
||||
|
||||
[Header("SO 事件通道(替代 GameManager 直接调用 Show,订阅 OnGameOver 自激活)")]
|
||||
[SerializeField] private GameEvent onGameOverEvent;
|
||||
|
||||
[Header("标题渐现动效")]
|
||||
[Tooltip("标题渐现总时长(秒)")]
|
||||
[SerializeField] private float titleFadeDuration = 1.5f;
|
||||
@@ -54,6 +56,9 @@ namespace GameFramework
|
||||
[SerializeField] private float rippleFrequency = 15f;
|
||||
[Tooltip("水波纹材质(留空则自动从 Shader 创建)")]
|
||||
[SerializeField] private Material rippleMaterial;
|
||||
|
||||
[Tooltip("水波纹着色器(留空则尝试 Shader.Find 兜底;建议拖入 Assets/UI/shaders/WaterRippleFade.shader 以避免构建裁剪导致运行时找不到)")]
|
||||
[SerializeField] private Shader waterRippleShader;
|
||||
[Tooltip("主界面按钮延迟出现时间(秒)")]
|
||||
[SerializeField] private float buttonFadeDelay = 2f;
|
||||
[Tooltip("按钮渐现时长(秒)")]
|
||||
@@ -65,46 +70,11 @@ namespace GameFramework
|
||||
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();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 生命周期
|
||||
// 生命周期(激活由 OnGameOver 事件驱动,见 OnEnable)
|
||||
// ====================================================================
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
|
||||
// 如果 Canvas 还没赋值,尝试从自身获取
|
||||
if (lostCanvas == null)
|
||||
lostCanvas = GetComponent<Canvas>();
|
||||
@@ -114,6 +84,21 @@ namespace GameFramework
|
||||
lostCanvas.enabled = false;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (onGameOverEvent != null)
|
||||
onGameOverEvent.Register(OnGameOver);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (onGameOverEvent != null)
|
||||
onGameOverEvent.Unregister(OnGameOver);
|
||||
}
|
||||
|
||||
/// <summary>收到 OnGameOver 事件后激活失败叠加层(替代 GameManager 直接调用 Show)。</summary>
|
||||
private void OnGameOver() => Activate();
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_isVisible) return;
|
||||
@@ -355,7 +340,7 @@ namespace GameFramework
|
||||
}
|
||||
else
|
||||
{
|
||||
var shader = Shader.Find("GameFramework/UI/WaterRippleFade");
|
||||
var shader = waterRippleShader != null ? waterRippleShader : Shader.Find("GameFramework/UI/WaterRippleFade");
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogWarning("[GameLostOverlay] 找不到 WaterRippleFade Shader!请确认 Assets/UI/shaders/WaterRippleFade.shader 存在且无编译错误。回退到抖动动效。");
|
||||
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using IndianOceanAssets.Engine2_5D;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -13,11 +14,31 @@ namespace GameFramework
|
||||
/// </summary>
|
||||
public class GameManager : PersistentSingleton<GameManager>
|
||||
{
|
||||
/// <summary>游戏结束(失败)时触发。</summary>
|
||||
public static Action onGameOver;
|
||||
[Header("SO 事件通道(替代 static Action 事件,Inspector 拖入对应资产)")]
|
||||
[SerializeField] private GameEvent onGameOverEvent;
|
||||
[SerializeField] private GameEvent onGameWinEvent;
|
||||
|
||||
/// <summary>游戏胜利时触发。</summary>
|
||||
public static Action onGameWin;
|
||||
[Header("SO 事件通道(玩家死亡事件,替代 HealthSystem 直接调用 GameOver)")]
|
||||
[SerializeField] private GameEvent onPlayerDiedEvent;
|
||||
|
||||
[Header("运行时集合(替代 FindObjectsOfType<EnemyAI>,需敌人 Prefab 挂 RuntimeSetRegistrar 指向该集合)")]
|
||||
[SerializeField] private TransformRuntimeSet enemiesSet;
|
||||
[SerializeField] private EnemyManager enemyManagerRef;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (onPlayerDiedEvent != null)
|
||||
onPlayerDiedEvent.Register(OnPlayerDied);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (onPlayerDiedEvent != null)
|
||||
onPlayerDiedEvent.Unregister(OnPlayerDied);
|
||||
}
|
||||
|
||||
/// <summary>玩家死亡事件回调:触发失败流程(设置状态 + 过场 + 广播 OnGameOver)。</summary>
|
||||
private void OnPlayerDied() => GameOver();
|
||||
|
||||
public static GameState GameState
|
||||
{
|
||||
@@ -49,7 +70,7 @@ namespace GameFramework
|
||||
{
|
||||
if (Instance == null) return;
|
||||
GameState = GameState.GameOver;
|
||||
onGameOver?.Invoke();
|
||||
Instance.onGameOverEvent?.Raise();
|
||||
Instance.StartCoroutine(Instance.DeathTransition());
|
||||
}
|
||||
|
||||
@@ -58,7 +79,7 @@ namespace GameFramework
|
||||
{
|
||||
if (Instance == null) return;
|
||||
GameState = GameState.Victory;
|
||||
onGameWin?.Invoke();
|
||||
Instance.onGameWinEvent?.Raise();
|
||||
Instance.StartCoroutine(Instance.VictoryTransition());
|
||||
}
|
||||
|
||||
@@ -141,17 +162,32 @@ namespace GameFramework
|
||||
/// </summary>
|
||||
private void DisableAllEnemyAI()
|
||||
{
|
||||
// 禁用所有 EnemyAI
|
||||
var enemyAIs = FindObjectsOfType<IndianOceanAssets.Engine2_5D.EnemyAI>();
|
||||
foreach (var ai in enemyAIs)
|
||||
ai.enabled = false;
|
||||
int disabled = 0;
|
||||
|
||||
// 优先用 Enemies 运行时集合(敌人 Prefab 需挂 RuntimeSetRegistrar 并指向该集合,
|
||||
// 在 OnEnable/OnDisable 时自动注册/注销自身 Transform)
|
||||
if (enemiesSet != null)
|
||||
{
|
||||
foreach (var t in enemiesSet.Items)
|
||||
{
|
||||
if (t == null) continue;
|
||||
var ai = t.GetComponent<IndianOceanAssets.Engine2_5D.EnemyAI>();
|
||||
if (ai != null) { ai.enabled = false; disabled++; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 过渡期兜底:尚未接入 Enemies 集合时回退(Prefab 接入 RuntimeSetRegistrar 后可删除此分支)
|
||||
var enemyAIs = FindObjectsOfType<IndianOceanAssets.Engine2_5D.EnemyAI>();
|
||||
foreach (var ai in enemyAIs) ai.enabled = false;
|
||||
disabled = enemyAIs.Length;
|
||||
}
|
||||
|
||||
// 禁用 EnemyManager(停止生成/管理逻辑)
|
||||
var enemyManager = FindObjectOfType<IndianOceanAssets.Engine2_5D.EnemyManager>();
|
||||
if (enemyManager != null)
|
||||
enemyManager.enabled = false;
|
||||
if (enemyManagerRef != null)
|
||||
enemyManagerRef.enabled = false;
|
||||
|
||||
Debug.Log($"[GameManager] 已禁用 {enemyAIs.Length} 个 EnemyAI + EnemyManager");
|
||||
Debug.Log($"[GameManager] 已禁用 {disabled} 个 EnemyAI + EnemyManager");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -230,8 +266,7 @@ namespace GameFramework
|
||||
|
||||
yield return null;
|
||||
|
||||
// 在全黑之上显示失败叠加层("你已迷失……" + 主界面按钮)
|
||||
GameLostOverlay.Show();
|
||||
// 失败叠加层("你已迷失……")由 GameLostOverlay 订阅 OnGameOver 事件自显示,此处不再直接调用。
|
||||
|
||||
// 等待一帧让 UI 渲染
|
||||
yield return null;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏结束画面。
|
||||
/// 监听 GameManager.onGameOver 事件,显示结束 Canvas 并暂停游戏。
|
||||
/// 玩家按确认键或点击按钮后跳转到排行榜场景。
|
||||
/// </summary>
|
||||
public class GameOverScreen : MonoBehaviour
|
||||
{
|
||||
[Header("Canvas")]
|
||||
[SerializeField] Canvas gameOverCanvas;
|
||||
|
||||
[Header("Button")]
|
||||
[SerializeField] Button confirmButton;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] AudioData gameOverSFX;
|
||||
|
||||
[Header("Input")]
|
||||
[SerializeField] KeyCode confirmKey = KeyCode.Return;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
GameManager.onGameOver += ShowGameOver;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GameManager.onGameOver -= ShowGameOver;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 初始隐藏
|
||||
if (gameOverCanvas != null)
|
||||
gameOverCanvas.enabled = false;
|
||||
|
||||
if (confirmButton != null)
|
||||
confirmButton.onClick.AddListener(OnConfirmClick);
|
||||
}
|
||||
|
||||
void ShowGameOver()
|
||||
{
|
||||
// 失败时由 GameLostOverlay 接管 UI,这里不再显示旧 Canvas
|
||||
// 暂停和鼠标由 GameManager + GameLostOverlay 统一处理
|
||||
return;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 游戏结束画面可见时,按确认键跳转
|
||||
if (gameOverCanvas != null && gameOverCanvas.enabled)
|
||||
{
|
||||
if (Input.GetKeyDown(confirmKey) || Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
OnConfirmClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnConfirmClick()
|
||||
{
|
||||
// 先解除暂停
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Unpause();
|
||||
|
||||
// 跳转到排行榜场景
|
||||
if (SceneLoader.Instance != null)
|
||||
SceneLoader.Instance.LoadScoringScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -10,9 +11,9 @@ namespace GameFramework
|
||||
public enum GameResult { Win, Lose }
|
||||
|
||||
/// <summary>
|
||||
/// 游戏结果界面(支持胜利/失败)。
|
||||
/// 监听 GameManager.onGameOver 和 onGameWin 事件,
|
||||
/// 显示对应面板并暂停游戏。确认后跳转到排行榜场景。
|
||||
/// 游戏结果界面(仅负责「胜利」结算)。
|
||||
/// 监听 OnGameWin 事件,显示胜利面板并暂停游戏,确认后跳转到排行榜场景。
|
||||
/// 「失败」由 GameLostOverlay 接管(订阅 OnGameOver),本屏不处理失败,避免空响应。
|
||||
/// </summary>
|
||||
public class GameResultScreen : MonoBehaviour
|
||||
{
|
||||
@@ -38,6 +39,9 @@ namespace GameFramework
|
||||
[Header("Input")]
|
||||
[SerializeField] private KeyCode confirmKey = KeyCode.Return;
|
||||
|
||||
[Header("SO 事件通道(胜利事件;失败由 GameLostOverlay 接管)")]
|
||||
[SerializeField] private GameEvent onGameWinEvent;
|
||||
|
||||
private bool _isVisible;
|
||||
|
||||
/// <summary>
|
||||
@@ -59,20 +63,14 @@ namespace GameFramework
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
GameManager.onGameOver += OnGameOver;
|
||||
GameManager.onGameWin += OnGameWin;
|
||||
onGameWinEvent?.Register(OnGameWin);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GameManager.onGameOver -= OnGameOver;
|
||||
GameManager.onGameWin -= OnGameWin;
|
||||
onGameWinEvent?.Unregister(OnGameWin);
|
||||
}
|
||||
|
||||
void OnGameOver()
|
||||
{
|
||||
// 失败时由 GameLostOverlay 接管,不再显示旧失败面板
|
||||
}
|
||||
void OnGameWin() => ShowResult(GameResult.Win);
|
||||
|
||||
void Start()
|
||||
|
||||
@@ -59,10 +59,10 @@ Material:
|
||||
- _DecalMeshDepthBias: 0
|
||||
- _DecalMeshViewBias: 0
|
||||
- _DrawOrder: 0
|
||||
- _FadeAlpha: 0.96674436
|
||||
- _FadeAlpha: 1
|
||||
- _RippleAmplitude: 0.0412
|
||||
- _RippleFrequency: 10.5
|
||||
- _RippleIntensity: 0.64074624
|
||||
- _RippleIntensity: -0
|
||||
- _RippleSpeed: 3
|
||||
- _Shininess: 0.2
|
||||
- _Stencil: 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -13,24 +14,25 @@ namespace GameFramework
|
||||
[SerializeField] Text scoreText;
|
||||
[SerializeField] string format = "{0}";
|
||||
|
||||
[Header("SO 事件通道(替代 static ScoreManager.onScoreChanged 事件)")]
|
||||
[SerializeField] private IntEvent scoreChangedEvent;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
ScoreManager.onScoreChanged += UpdateText;
|
||||
scoreChangedEvent?.Register(UpdateText);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
ScoreManager.onScoreChanged -= UpdateText;
|
||||
scoreChangedEvent?.Unregister(UpdateText);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (scoreText == null)
|
||||
scoreText = GetComponent<Text>();
|
||||
if (ScoreManager.Instance != null)
|
||||
UpdateText(ScoreManager.Instance.CurrentScore);
|
||||
else
|
||||
UpdateText(0);
|
||||
// 初始值走事件:订阅后首个 ScoreChanged 会刷新显示;分数初始即为 0,先显示 0。
|
||||
UpdateText(0);
|
||||
}
|
||||
|
||||
void UpdateText(int score)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
@@ -32,7 +33,7 @@ namespace GameFramework
|
||||
|
||||
/// <summary>
|
||||
/// 得分管理器(持久单例)。
|
||||
/// 维护当前局得分,通过事件通知 UI,并管理 Top-10 排行榜存档。
|
||||
/// 维护当前局得分,通过 SO 事件通道通知 UI,并管理 Top-10 排行榜存档。
|
||||
/// </summary>
|
||||
public class ScoreManager : PersistentSingleton<ScoreManager>
|
||||
{
|
||||
@@ -42,10 +43,9 @@ namespace GameFramework
|
||||
// 当前局得分
|
||||
int currentScore = 0;
|
||||
|
||||
// 当得分变化时触发,参数为最新得分(动画过程中的中间值也会触发)
|
||||
public static event Action<int> onScoreChanged;
|
||||
// 当得分完成最终增加时触发,参数为最终得分
|
||||
public static event Action<int> onScoreSettled;
|
||||
[Header("SO 事件通道(替代 static onScoreChanged / onScoreSettled 事件,Inspector 拖入对应资产)")]
|
||||
[SerializeField] private IntEvent scoreChangedEvent;
|
||||
[SerializeField] private IntEvent scoreSettledEvent;
|
||||
|
||||
/// <summary>当前得分(只读)。</summary>
|
||||
public int CurrentScore => currentScore;
|
||||
@@ -75,8 +75,8 @@ namespace GameFramework
|
||||
public void SetScore(int value)
|
||||
{
|
||||
currentScore = Mathf.Max(0, value);
|
||||
onScoreChanged?.Invoke(currentScore);
|
||||
onScoreSettled?.Invoke(currentScore);
|
||||
scoreChangedEvent?.Raise(currentScore);
|
||||
scoreSettledEvent?.Raise(currentScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,8 +85,8 @@ namespace GameFramework
|
||||
public void ResetScore()
|
||||
{
|
||||
currentScore = 0;
|
||||
onScoreChanged?.Invoke(0);
|
||||
onScoreSettled?.Invoke(0);
|
||||
scoreChangedEvent?.Raise(0);
|
||||
scoreSettledEvent?.Raise(0);
|
||||
}
|
||||
|
||||
IEnumerator ScoreCountUpCoroutine(int from, int to)
|
||||
@@ -100,12 +100,12 @@ namespace GameFramework
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
int display = Mathf.RoundToInt(Mathf.Lerp(from, to, t));
|
||||
onScoreChanged?.Invoke(display);
|
||||
scoreChangedEvent?.Raise(display);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
onScoreChanged?.Invoke(to);
|
||||
onScoreSettled?.Invoke(to);
|
||||
scoreChangedEvent?.Raise(to);
|
||||
scoreSettledEvent?.Raise(to);
|
||||
}
|
||||
|
||||
#region 排行榜
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using GameFramework;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
@@ -20,11 +21,8 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
/// </summary>
|
||||
public class EchoSystem : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 E 释放回声(摇铃)时触发,参数为释放时的玩家世界位置。
|
||||
/// 敌人聆听系统等可订阅此事件。
|
||||
/// </summary>
|
||||
public static event Action<Vector3> OnEchoReleased;
|
||||
[Header("SO 事件通道(替代 static OnEchoReleased 事件,参数为释放时玩家世界位置)")]
|
||||
[SerializeField] private Vector3Event echoReleasedEvent;
|
||||
|
||||
[Header("按键")]
|
||||
[SerializeField] private KeyCode echoKey = KeyCode.E;
|
||||
@@ -101,6 +99,7 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
private float _lastEchoTime = -999f;
|
||||
private float _ringAlpha = 0f;
|
||||
private float _ringRadius = 0f;
|
||||
private bool _warnedEcho = false; // 防止 echoReleasedEvent 空引用告警刷屏
|
||||
|
||||
private void Start()
|
||||
{
|
||||
@@ -219,7 +218,13 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
}
|
||||
|
||||
// 通知订阅者(敌人聆听等)
|
||||
OnEchoReleased?.Invoke(p);
|
||||
if (echoReleasedEvent != null)
|
||||
echoReleasedEvent.Raise(p);
|
||||
else if (!_warnedEcho)
|
||||
{
|
||||
Debug.LogWarning("[EchoSystem] echoReleasedEvent 未接线!敌人不会响应摇铃。请在 EchoSystem 的 Echo Released Event 字段拖入 EchoReleased 资产。", this);
|
||||
_warnedEcho = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExpanding()
|
||||
@@ -309,6 +314,16 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
|
||||
public bool IsActive => _state != State.Idle;
|
||||
|
||||
/// <summary>摇铃冷却剩余/总时长(供 HUD 显示,替代反射读取私有字段)。</summary>
|
||||
public (float remaining, float total) BellCooldown
|
||||
=> (Mathf.Max(0f, (_lastEchoTime + cooldown) - Time.time), cooldown);
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (echoReleasedEvent == null)
|
||||
Debug.LogWarning($"[EchoSystem] Echo Released Event 未接线({gameObject.name})。按 E 摇铃时敌人不会响应。", this);
|
||||
}
|
||||
|
||||
// ===== 编辑器可视化 =====
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
/// <summary>冷却剩余时间(供 UI 使用)</summary>
|
||||
public float CooldownRemaining => Mathf.Max(0f, (_lastPlaceTime + cooldown) - Time.time);
|
||||
|
||||
/// <summary>冷却总时长(供 UI 计算 CD 比例,替代反射读取私有字段)</summary>
|
||||
public float Cooldown => cooldown;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_remainingLanterns = maxLanterns;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using Architecture.Core;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
@@ -46,6 +47,9 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
[Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝铃铛位置移动")]
|
||||
[SerializeField] private float listenRange = 15f;
|
||||
|
||||
[Header("SO 事件通道(替代 static EchoSystem.OnEchoReleased 事件)")]
|
||||
[SerializeField] private Vector3Event echoReleasedEvent;
|
||||
|
||||
[Header("铃铛追击")]
|
||||
[Tooltip("到达铃铛位置后的容差距离")]
|
||||
[SerializeField] private float bellArriveDistance = 1f;
|
||||
@@ -105,6 +109,12 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
private HealthSystem _playerHealth; // 玩家血量引用
|
||||
private Camera _mainCam; // 主相机引用(用于Billboard)
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (echoReleasedEvent == null)
|
||||
Debug.LogWarning($"[EnemyAI] Echo Released Event 未接线({gameObject.name})。该敌人不会响应摇铃。", this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerTarget == null)
|
||||
@@ -127,12 +137,12 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EchoSystem.OnEchoReleased += OnBell;
|
||||
echoReleasedEvent?.Register(OnBell);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EchoSystem.OnEchoReleased -= OnBell;
|
||||
echoReleasedEvent?.Unregister(OnBell);
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using UnityEngine;
|
||||
using IndianOceanAssets.Engine2_5D;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人头顶血条 —— 挂在敌人 Prefab 上。
|
||||
/// 血条可视化物体(BG、Fill)已在 Prefab 中预建,
|
||||
/// 运行时直接引用,无需动态创建。
|
||||
/// 血条可视化物体(BG、Fill)已在 Prefab 中预建,运行时直接引用,无需动态创建。
|
||||
///
|
||||
/// 解耦改造:直接引用同物体上的 HealthSystem(RequireComponent 保证存在),
|
||||
/// 订阅其 OnHealthChanged 事件刷新,彻底移除对私有字段的反射读取。
|
||||
/// 注意:不使用"全局共享 IntVariable"承载敌人血量——否则所有敌人血条会显示同一份血量,
|
||||
/// 正确的解法是"每个敌人读自己的 HealthSystem"(同物体组件引用,非 Find / 反射 / 跨对象)。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(HealthSystem))]
|
||||
public class EnemyHealthBar : MonoBehaviour
|
||||
@@ -33,52 +38,44 @@ namespace IndianOceanAssets.Engine2_5D
|
||||
[Tooltip("满血时是否隐藏血条")]
|
||||
[SerializeField] private bool hideWhenFull = false;
|
||||
|
||||
private HealthSystem _healthSystem;
|
||||
private int _lastHealth = -1;
|
||||
private int _maxHealth;
|
||||
// 同物体上的 HealthSystem(RequireComponent 保证存在;非 Find / 反射 / 跨对象引用)
|
||||
private HealthSystem _healthSource;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_healthSystem = GetComponent<HealthSystem>();
|
||||
if (_healthSystem == null)
|
||||
if (fillRenderer == null || bgRenderer == null)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 maxHealth(通过反射读取私有字段)
|
||||
var field = typeof(HealthSystem).GetField("maxHealth",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
_maxHealth = (int)field.GetValue(_healthSystem);
|
||||
_healthSource = GetComponent<HealthSystem>();
|
||||
if (_healthSource != null)
|
||||
{
|
||||
_healthSource.OnHealthChanged += UpdateBar;
|
||||
UpdateBar(_healthSource.CurrentHealth, _healthSource.MaxHealth);
|
||||
}
|
||||
else
|
||||
_maxHealth = 3;
|
||||
|
||||
UpdateBar();
|
||||
{
|
||||
UpdateBar(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
private void OnDestroy()
|
||||
{
|
||||
UpdateBar();
|
||||
if (_healthSource != null)
|
||||
_healthSource.OnHealthChanged -= UpdateBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前血量更新血条显示。
|
||||
/// 根据当前血量更新血条显示(事件驱动,无反射、无每帧 LateUpdate)。
|
||||
/// </summary>
|
||||
private void UpdateBar()
|
||||
private void UpdateBar(int currentHealth, int maxHealth)
|
||||
{
|
||||
if (_healthSystem == null || fillRenderer == null || bgRenderer == null) return;
|
||||
if (fillRenderer == null || bgRenderer == null) return;
|
||||
|
||||
// 获取当前血量(反射)
|
||||
var healthField = typeof(HealthSystem).GetField("health",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
int currentHealth = healthField != null ? (int)healthField.GetValue(_healthSystem) : _maxHealth;
|
||||
|
||||
// 血量没变化则跳过(性能优化)
|
||||
if (currentHealth == _lastHealth) return;
|
||||
_lastHealth = currentHealth;
|
||||
|
||||
float ratio = Mathf.Clamp01((float)currentHealth / _maxHealth);
|
||||
if (maxHealth <= 0) maxHealth = 1;
|
||||
float ratio = Mathf.Clamp01((float)currentHealth / maxHealth);
|
||||
|
||||
// 更新填充条缩放(居中对齐)
|
||||
fillRenderer.transform.localScale = new Vector3(barWidth * ratio, barHeight, 1f);
|
||||
|
||||
@@ -439,6 +439,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -477,11 +478,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 2100000, guid: a2207e01903058a4198862e6a7fb73c7, type: 2}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -351,6 +351,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -389,11 +390,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 2100000, guid: a2207e01903058a4198862e6a7fb73c7, type: 2}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -351,6 +351,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -389,11 +390,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 2100000, guid: a2207e01903058a4198862e6a7fb73c7, type: 2}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -439,6 +439,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -477,11 +478,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 2100000, guid: a2207e01903058a4198862e6a7fb73c7, type: 2}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -351,6 +351,7 @@ MonoBehaviour:
|
||||
maxSightDistance: 4
|
||||
loseSightTime: 3
|
||||
listenRange: 15
|
||||
echoReleasedEvent: {fileID: 11400000, guid: 3f85d8e63d6b82342ba94a4b413c21fa, type: 2}
|
||||
bellArriveDistance: 1
|
||||
bellChaseTimeout: 10
|
||||
bellPatrolTime: 5
|
||||
@@ -389,11 +390,16 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f9e36f6d2e0ea0541bad29e784e0841b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onPlayerDamagedEvent: {fileID: 0}
|
||||
onPlayerDiedEvent: {fileID: 0}
|
||||
healthVar: {fileID: 0}
|
||||
maxHealthVar: {fileID: 0}
|
||||
maxHealth: 3
|
||||
deathEffect: {fileID: 0}
|
||||
isPlayer: 0
|
||||
dissolveMaterial: {fileID: 2100000, guid: a2207e01903058a4198862e6a7fb73c7, type: 2}
|
||||
fadeDuration: 2
|
||||
damageInvincibleDuration: 2
|
||||
--- !u!114 &1893771889199554109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
Reference in New Issue
Block a user