This commit is contained in:
2026-07-07 03:34:56 +08:00
parent e7891d7e00
commit ecb20cf370
81 changed files with 2108 additions and 293 deletions
+8
View File
@@ -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
+7
View File
@@ -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
+133
View File
@@ -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:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: WSxO4H+lVy/+Blf5pfhG4hmfSru5p5O0gX9h1Xbssr3FMpnAolEEZqQ=
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -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:
+8
View File
@@ -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,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&lt;T&gt;);设计师驱动用对应 *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:
+8
View File
@@ -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&lt;EnemyAI&gt;)放在对应领域文件夹,
/// 继承本类即可,无需改动核心。
/// </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&lt;EnemyAI&gt;</c>)使用。
/// 自动 <c>GetComponent&lt;T&gt;()</c> 并加入对应集合,避免用 Transform 集合再手动转型。
/// 因 Unity 对泛型 MonoBehaviour 需具体闭包类型才能挂载,领域文件夹内写一行具体子类即可:
/// <code>public class EnemySetRegistrar : RuntimeSetRegistrar&lt;EnemyAI&gt; { }</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:
+8
View File
@@ -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:
+8
View File
@@ -3,6 +3,7 @@ using UnityEditor;
using UnityEngine.UI;
using TMPro;
using GameFramework;
using Architecture.Core;
/// <summary>
/// 一键搭建游戏 UIHUD 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);
+4 -1
View File
@@ -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);
+67
View File
@@ -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}
+11
View File
@@ -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:
+24 -6
View File
@@ -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
View File
@@ -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);
}
// 灵灯 CDSpiritLanternSystem 读取
// 灵灯 CDSpiritLanternSystem 已暴露公共 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);
}
}
+25 -40
View File
@@ -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 存在且无编译错误。回退到抖动动效。");
+51 -16
View File
@@ -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;
-75
View File
@@ -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();
}
}
}
+9 -11
View File
@@ -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
+8 -6
View File
@@ -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)
+12 -12
View File
@@ -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
+21 -6
View File
@@ -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()
{
+3
View File
@@ -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;
+12 -2
View File
@@ -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()
+27 -30
View File
@@ -1,11 +1,16 @@
using UnityEngine;
using IndianOceanAssets.Engine2_5D;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 敌人头顶血条 —— 挂在敌人 Prefab 上。
/// 血条可视化物体(BG、Fill)已在 Prefab 中预建,
/// 运行时直接引用,无需动态创建。
/// 血条可视化物体(BG、Fill)已在 Prefab 中预建,运行时直接引用,无需动态创建。
///
/// 解耦改造:直接引用同物体上的 HealthSystemRequireComponent 保证存在),
/// 订阅其 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;
// 同物体上的 HealthSystemRequireComponent 保证存在;非 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);
+6
View File
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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