chore: initial commit
This commit is contained in:
8
Assets/Scripts/Animation.meta
Normal file
8
Assets/Scripts/Animation.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee09651b4ef333d4d826baf6fc2d3963
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Animation/.gitkeep
Normal file
0
Assets/Scripts/Animation/.gitkeep
Normal file
17
Assets/Scripts/Animation/BaseGames.Animation.asmdef
Normal file
17
Assets/Scripts/Animation/BaseGames.Animation.asmdef
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Animation",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Animation",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Animation/BaseGames.Animation.asmdef.meta
Normal file
7
Assets/Scripts/Animation/BaseGames.Animation.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc40540b9a6e12a4c81b5b0ee9132a3f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Animation/_Placeholder.cs
Normal file
3
Assets/Scripts/Animation/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Animation { }
|
||||
|
||||
11
Assets/Scripts/Animation/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Animation/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14d8a3e3d7371e54eb25a5d4dded4645
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Audio.meta
Normal file
8
Assets/Scripts/Audio.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01d2e18a4097b5f408e821b493e20415
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Audio/.gitkeep
Normal file
0
Assets/Scripts/Audio/.gitkeep
Normal file
57
Assets/Scripts/Audio/AudioConfigSO.cs
Normal file
57
Assets/Scripts/Audio/AudioConfigSO.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 音频全局配置 SO:区域 BGM 映射、Boss BGM 映射、特殊曲目。
|
||||
/// 资产路径:Assets/ScriptableObjects/Audio/AUD_Config.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Audio/AudioConfig")]
|
||||
public class AudioConfigSO : ScriptableObject
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct ZoneBGMEntry
|
||||
{
|
||||
public string ZoneId;
|
||||
public AudioClip BGMClip;
|
||||
public float FadeDuration;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct BossBGMEntry
|
||||
{
|
||||
public string BossId;
|
||||
public AudioClip BGMClip;
|
||||
}
|
||||
|
||||
[Header("区域 BGM 映射")]
|
||||
public ZoneBGMEntry[] ZoneBGMs;
|
||||
|
||||
[Header("Boss BGM 映射")]
|
||||
public BossBGMEntry[] BossBGMs;
|
||||
|
||||
[Header("特殊曲目")]
|
||||
public AudioClip MainMenuBGM;
|
||||
public AudioClip GameOverSting;
|
||||
public AudioClip VictoryStingBGM;
|
||||
[Min(0.1f)]
|
||||
public float VictoryStingDuration = 4f;
|
||||
|
||||
public AudioClip GetZoneBGM(string zoneId)
|
||||
{
|
||||
if (ZoneBGMs == null) return null;
|
||||
foreach (var e in ZoneBGMs)
|
||||
if (e.ZoneId == zoneId) return e.BGMClip;
|
||||
return null;
|
||||
}
|
||||
|
||||
public AudioClip GetBossBGM(string bossId)
|
||||
{
|
||||
if (BossBGMs == null) return null;
|
||||
foreach (var e in BossBGMs)
|
||||
if (e.BossId == bossId) return e.BGMClip;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/AudioConfigSO.cs.meta
Normal file
11
Assets/Scripts/Audio/AudioConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af379c0184a345441914e3167376d39c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
200
Assets/Scripts/Audio/AudioManager.cs
Normal file
200
Assets/Scripts/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 音频管理器。
|
||||
/// 职责:BGM 双 Source 交叉淡入淡出、SFX 多源轮转池、AudioMixer 快照切换、音量控制。
|
||||
/// 挂在 Persistent 场景 [AudioManager] GameObject 上。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-500)]
|
||||
public class AudioManager : MonoBehaviour, IAudioService
|
||||
{
|
||||
[Header("AudioMixer")]
|
||||
[SerializeField] private AudioMixer _mixer;
|
||||
|
||||
[Header("BGM Sources(双 Source 交叉淡入淡出)")]
|
||||
[SerializeField] private AudioSource _bgmSourceA;
|
||||
[SerializeField] private AudioSource _bgmSourceB;
|
||||
|
||||
[Header("SFX Pool(建议 6 个,均路由到 SFX MixerGroup)")]
|
||||
[SerializeField] private AudioSource[] _sfxSources;
|
||||
|
||||
[Header("Event Channels - Subscribe")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
|
||||
private AudioSource _activeBGMSource;
|
||||
private AudioSource _inactiveBGMSource;
|
||||
private Coroutine _crossfadeCoroutine;
|
||||
private int _sfxRoundRobin;
|
||||
|
||||
// ── 遗留单例(已废弃;新代码请使用 ServiceLocator.Get<IAudioService>())────────────
|
||||
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
|
||||
public static AudioManager Instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
if (Instance != null) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
_activeBGMSource = _bgmSourceA;
|
||||
_inactiveBGMSource = _bgmSourceB;
|
||||
|
||||
// ServiceLocator 注册(覆盖 GameServiceRegistrar 的 NullAudioService 兜底)
|
||||
ServiceLocator.Register<IAudioService>(this);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onPlayerDied != null)
|
||||
_onPlayerDied.OnEventRaised += HandlePlayerDied;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onPlayerDied != null)
|
||||
_onPlayerDied.OnEventRaised -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
// ── IAudioService string-key API(Phase 2 接入 AudioEventSO 后完整实现)─────────────
|
||||
/// <summary>
|
||||
/// 按 Addressable key 播放 BGM。Phase 2 接入 AudioEventSO 前为占位警告。
|
||||
/// </summary>
|
||||
public void PlayBGM(string key)
|
||||
=> Debug.LogWarning($"[AudioManager] PlayBGM(key) 尚未接入 AudioEventSO(Phase 2)。key={key}");
|
||||
|
||||
/// <summary>
|
||||
/// 按 Addressable key 播放 SFX。Phase 2 接入 AudioEventSO 前为占位警告。
|
||||
/// </summary>
|
||||
public void PlaySFX(string key)
|
||||
=> Debug.LogWarning($"[AudioManager] PlaySFX(key) 尚未接入 AudioEventSO(Phase 2)。key={key}");
|
||||
|
||||
// ── 音量控制 ─────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 将指定 Exposed Parameter 设置为 0-1 线性值(内部转换为 dB)。
|
||||
/// 唯一音量写入入口(同时满足 IAudioService.SetVolume 接口)。
|
||||
/// </summary>
|
||||
public void SetVolume(string exposedParam, float linear)
|
||||
=> _mixer.SetFloat(exposedParam, LinearToDecibel(linear));
|
||||
|
||||
/// <summary>读取 GlobalSettings 并应用所有音量初始值。</summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// TODO: 从 SettingsManager / PlayerPrefs 读取保存的音量值并应用
|
||||
}
|
||||
|
||||
// ── BGM ──────────────────────────────────────────────────────────────────
|
||||
/// <summary>播放 BGM,使用双 AudioSource 交叉淡入淡出。</summary>
|
||||
public void PlayBGM(AudioClip clip, float fadeOutDur = 1f, float fadeInDur = 1f)
|
||||
{
|
||||
if (clip == null) return;
|
||||
if (_crossfadeCoroutine != null) StopCoroutine(_crossfadeCoroutine);
|
||||
_crossfadeCoroutine = StartCoroutine(CrossfadeCoroutine(clip, fadeOutDur, fadeInDur));
|
||||
}
|
||||
|
||||
/// <summary>停止 BGM(带淡出)。</summary>
|
||||
public void StopBGM(float fadeDuration = 1f)
|
||||
{
|
||||
if (_crossfadeCoroutine != null) StopCoroutine(_crossfadeCoroutine);
|
||||
_crossfadeCoroutine = StartCoroutine(FadeOutCoroutine(_activeBGMSource, fadeDuration));
|
||||
}
|
||||
|
||||
// ── SFX ──────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 一次性播放 SFX,使用轮转多源池避免高密度战斗时音效相互戳断。
|
||||
/// </summary>
|
||||
public void PlaySFX(AudioClip clip, float volumeScale = 1f)
|
||||
{
|
||||
if (clip == null) return;
|
||||
var src = NextSFXSource();
|
||||
src.volume = volumeScale;
|
||||
src.PlayOneShot(clip);
|
||||
}
|
||||
|
||||
/// <summary>2D 游戏中位置无衰减,统一委托多源池播放。</summary>
|
||||
public void PlaySFXAtPosition(AudioClip clip, Vector2 pos, float volumeScale = 1f)
|
||||
=> PlaySFX(clip, volumeScale);
|
||||
|
||||
// ── 快照切换 ─────────────────────────────────────────────────────────────
|
||||
/// <summary>切换 AudioMixer 快照(如 Default / Paused / Dead / BossFight)。</summary>
|
||||
public void TransitionToSnapshot(string snapshotName, float transitionTime = 0.5f)
|
||||
{
|
||||
var snapshot = _mixer.FindSnapshot(snapshotName);
|
||||
if (snapshot != null)
|
||||
snapshot.TransitionTo(transitionTime);
|
||||
else
|
||||
Debug.LogWarning($"[AudioManager] Snapshot '{snapshotName}' not found in mixer.");
|
||||
}
|
||||
|
||||
// ── 内部实现 ─────────────────────────────────────────────────────────────
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
TransitionToSnapshot("Dead", 1.5f);
|
||||
}
|
||||
|
||||
private AudioSource NextSFXSource()
|
||||
{
|
||||
if (_sfxSources == null || _sfxSources.Length == 0) return _bgmSourceA;
|
||||
return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length];
|
||||
}
|
||||
|
||||
private IEnumerator CrossfadeCoroutine(AudioClip newClip, float fadeOutDur, float fadeInDur)
|
||||
{
|
||||
// 淡出当前活跃 Source
|
||||
float startVolume = _activeBGMSource.volume;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < fadeOutDur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
_activeBGMSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / fadeOutDur);
|
||||
yield return null;
|
||||
}
|
||||
_activeBGMSource.Stop();
|
||||
_activeBGMSource.volume = 0f;
|
||||
|
||||
// 切换到非活跃 Source 播放新曲目
|
||||
var temp = _activeBGMSource;
|
||||
_activeBGMSource = _inactiveBGMSource;
|
||||
_inactiveBGMSource = temp;
|
||||
|
||||
_activeBGMSource.clip = newClip;
|
||||
_activeBGMSource.volume = 0f;
|
||||
_activeBGMSource.Play();
|
||||
|
||||
// 淡入新 Source
|
||||
elapsed = 0f;
|
||||
while (elapsed < fadeInDur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
_activeBGMSource.volume = Mathf.Lerp(0f, 1f, elapsed / fadeInDur);
|
||||
yield return null;
|
||||
}
|
||||
_activeBGMSource.volume = 1f;
|
||||
_crossfadeCoroutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator FadeOutCoroutine(AudioSource source, float duration)
|
||||
{
|
||||
float startVolume = source.volume;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
source.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration);
|
||||
yield return null;
|
||||
}
|
||||
source.Stop();
|
||||
source.volume = 0f;
|
||||
_crossfadeCoroutine = null;
|
||||
}
|
||||
|
||||
private static float LinearToDecibel(float linear)
|
||||
=> linear > 0.0001f ? 20f * Mathf.Log10(linear) : -80f;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/AudioManager.cs.meta
Normal file
11
Assets/Scripts/Audio/AudioManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1230831ab62bdd84fbeb7df03e20c254
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/Scripts/Audio/AudioMixerKeys.cs
Normal file
15
Assets/Scripts/Audio/AudioMixerKeys.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// AudioMixer Exposed Parameter 字符串常量。
|
||||
/// 与 Assets/Audio/MainMixer.mixer 中的 Exposed Parameters 保持同步。
|
||||
/// 参数范围:-80 ~ 0 dB(代码通过 LinearToDecibel 转换后写入)。
|
||||
/// </summary>
|
||||
public static class AudioMixerKeys
|
||||
{
|
||||
public const string Master = "MasterVolume";
|
||||
public const string BGM = "BGMVolume";
|
||||
public const string SFX = "SFXVolume";
|
||||
public const string Ambient = "AmbientVolume";
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/AudioMixerKeys.cs.meta
Normal file
11
Assets/Scripts/Audio/AudioMixerKeys.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8df8e995946cb24bb61b96c362bf58b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Scripts/Audio/AudioZone.cs
Normal file
24
Assets/Scripts/Audio/AudioZone.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域音效触发器:玩家进入 Collider2D 时广播 _onRegionEntered 事件频道。
|
||||
/// 挂在区域边界的 GameObject 上,Collider2D 设置为 Is Trigger。
|
||||
/// _zoneId 须与 AudioConfigSO.ZoneBGMs 中的 ZoneId 一致。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class AudioZone : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _zoneId;
|
||||
[SerializeField] private StringEventChannelSO _onRegionEntered;
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!other.CompareTag("Player")) return;
|
||||
if (_onRegionEntered != null)
|
||||
_onRegionEntered.Raise(_zoneId);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/AudioZone.cs.meta
Normal file
11
Assets/Scripts/Audio/AudioZone.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11367ad39af3b9649af572aaa55891ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
100
Assets/Scripts/Audio/BGMController.cs
Normal file
100
Assets/Scripts/Audio/BGMController.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>BGM 状态机内部状态。</summary>
|
||||
public enum MusicState
|
||||
{
|
||||
Exploration, // 默认:区域探索 BGM
|
||||
Boss, // Boss 战:Boss 主题 BGM
|
||||
Victory, // Boss 击败后短暂胜利音乐
|
||||
None, // 过场/死亡/主菜单时由 BGMController 直接切换
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BGM 控制器:订阅世界/Boss/游戏状态事件,指挥 AudioManager 切换 BGM 和快照。
|
||||
/// 挂在 Persistent 场景 [AudioManager] 子对象上。
|
||||
/// </summary>
|
||||
public class BGMController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private AudioManager _audioManager;
|
||||
[SerializeField] private AudioConfigSO _config;
|
||||
|
||||
[Header("Event Channels - Subscribe")]
|
||||
[SerializeField] private GameStateEventChannelSO _onGameStateChanged;
|
||||
[SerializeField] private BoolEventChannelSO _onBossFightToggled; // true=开始, false=结束
|
||||
[SerializeField] private StringEventChannelSO _onRegionEntered;
|
||||
|
||||
private MusicState _musicState = MusicState.Exploration;
|
||||
private string _currentRegion = string.Empty;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onBossFightToggled != null) _onBossFightToggled.OnEventRaised += OnBossFightToggled;
|
||||
if (_onRegionEntered != null) _onRegionEntered.OnEventRaised += OnRegionEntered;
|
||||
if (_onGameStateChanged != null) _onGameStateChanged.OnEventRaised += HandleStateChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onBossFightToggled != null) _onBossFightToggled.OnEventRaised -= OnBossFightToggled;
|
||||
if (_onRegionEntered != null) _onRegionEntered.OnEventRaised -= OnRegionEntered;
|
||||
if (_onGameStateChanged != null) _onGameStateChanged.OnEventRaised -= HandleStateChanged;
|
||||
}
|
||||
|
||||
private void OnBossFightToggled(bool started)
|
||||
{
|
||||
if (started)
|
||||
{
|
||||
_musicState = MusicState.Boss;
|
||||
var clip = _config != null ? _config.GetBossBGM(_currentRegion) : null;
|
||||
_audioManager.PlayBGM(clip, fadeOutDur: 1f, fadeInDur: 0.5f);
|
||||
_audioManager.TransitionToSnapshot("BossFight", 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(PlayVictoryThenRestore());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PlayVictoryThenRestore()
|
||||
{
|
||||
_musicState = MusicState.Victory;
|
||||
_audioManager.PlayBGM(_config != null ? _config.VictoryStingBGM : null,
|
||||
fadeOutDur: 0.3f, fadeInDur: 0.1f);
|
||||
float dur = _config != null ? _config.VictoryStingDuration : 4f;
|
||||
yield return new WaitForSecondsRealtime(dur);
|
||||
_musicState = MusicState.Exploration;
|
||||
OnRegionEntered(_currentRegion);
|
||||
_audioManager.TransitionToSnapshot("Default", 1.0f);
|
||||
}
|
||||
|
||||
private void OnRegionEntered(string regionId)
|
||||
{
|
||||
if (regionId == _currentRegion) return;
|
||||
_currentRegion = regionId;
|
||||
if (_musicState == MusicState.Exploration)
|
||||
{
|
||||
var clip = _config != null ? _config.GetZoneBGM(regionId) : null;
|
||||
_audioManager.PlayBGM(clip, fadeOutDur: 1f, fadeInDur: 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStateChanged(GameStateId state)
|
||||
{
|
||||
// ⚠️ GameStateId 是 struct,不能用 switch;使用 if/else + GameStates 常量
|
||||
if (state == GameStates.MainMenu)
|
||||
_audioManager.PlayBGM(_config != null ? _config.MainMenuBGM : null,
|
||||
fadeOutDur: 0.5f, fadeInDur: 1.0f);
|
||||
else if (state == GameStates.Paused)
|
||||
_audioManager.TransitionToSnapshot("Paused", 0.2f);
|
||||
else if (state == GameStates.Dead)
|
||||
_audioManager.TransitionToSnapshot("Dead", 1.5f);
|
||||
else if (state == GameStates.Gameplay)
|
||||
_audioManager.TransitionToSnapshot("Default", 0.3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/BGMController.cs.meta
Normal file
11
Assets/Scripts/Audio/BGMController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eed9bb7d17336ae4c8e2636fbeeef6bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Assets/Scripts/Audio/BaseGames.Audio.asmdef
Normal file
18
Assets/Scripts/Audio/BaseGames.Audio.asmdef
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Audio",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Audio",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Combat"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Audio/BaseGames.Audio.asmdef.meta
Normal file
7
Assets/Scripts/Audio/BaseGames.Audio.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdbbbb51c06a54142b8bf1f9966fc408
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/Scripts/Audio/CombatSFXController.cs
Normal file
85
Assets/Scripts/Audio/CombatSFXController.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 订阅战斗/死亡事件,通过 AudioManager 播放对应 SFX。
|
||||
/// 挂载在 Persistent 场景的 [Systems] GameObject 上。
|
||||
/// </summary>
|
||||
public class CombatSFXController : MonoBehaviour
|
||||
{
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
|
||||
[Header("Default Hit SFX")]
|
||||
[SerializeField] private AudioClip _defaultHitSFX;
|
||||
|
||||
[Header("Per-Type Hit SFX (optional, overrides default)")]
|
||||
[SerializeField] private AudioClip _sparkHitSFX;
|
||||
[SerializeField] private AudioClip _slashHitSFX;
|
||||
[SerializeField] private AudioClip _bloodHitSFX;
|
||||
[SerializeField] private AudioClip _magicHitSFX;
|
||||
[SerializeField] private AudioClip _heavyHitSFX;
|
||||
[SerializeField] private AudioClip _critHitSFX;
|
||||
[SerializeField] private AudioClip _parryHitSFX;
|
||||
[SerializeField] private AudioClip _fireHitSFX;
|
||||
[SerializeField] private AudioClip _iceHitSFX;
|
||||
|
||||
[Header("Death SFX")]
|
||||
[SerializeField] private AudioClip _playerDeathSFX;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised += HandleHit;
|
||||
|
||||
if (_onPlayerDied != null)
|
||||
_onPlayerDied.OnEventRaised += HandlePlayerDied;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised -= HandleHit;
|
||||
|
||||
if (_onPlayerDied != null)
|
||||
_onPlayerDied.OnEventRaised -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
private void HandleHit(HitInfo info)
|
||||
{
|
||||
AudioClip clip = ResolveHitClip(info.DamageInfo.FxType);
|
||||
if (clip == null) return;
|
||||
|
||||
AudioManager.Instance.PlaySFXAtPosition(clip, info.HitPoint);
|
||||
}
|
||||
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
if (_playerDeathSFX == null) return;
|
||||
AudioManager.Instance.PlaySFX(_playerDeathSFX);
|
||||
}
|
||||
|
||||
private AudioClip ResolveHitClip(HitFxType fxType)
|
||||
{
|
||||
AudioClip perType = fxType switch
|
||||
{
|
||||
HitFxType.Spark => _sparkHitSFX,
|
||||
HitFxType.Slash => _slashHitSFX,
|
||||
HitFxType.Blood => _bloodHitSFX,
|
||||
HitFxType.Magic => _magicHitSFX,
|
||||
HitFxType.Heavy => _heavyHitSFX,
|
||||
HitFxType.Crit => _critHitSFX,
|
||||
HitFxType.Parry => _parryHitSFX,
|
||||
HitFxType.Fire => _fireHitSFX,
|
||||
HitFxType.Ice => _iceHitSFX,
|
||||
_ => null
|
||||
};
|
||||
|
||||
return perType != null ? perType : _defaultHitSFX;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Audio/CombatSFXController.cs.meta
Normal file
11
Assets/Scripts/Audio/CombatSFXController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9ab0a72bbabda44888a0bec8186bc27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Audio/_Placeholder.cs
Normal file
3
Assets/Scripts/Audio/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Audio { }
|
||||
|
||||
11
Assets/Scripts/Audio/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Audio/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7d6fe0521388084e83314560a394951
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Camera.meta
Normal file
8
Assets/Scripts/Camera.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0461abd63ce9f1a4cbe77b584ad89dbe
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Camera/.gitkeep
Normal file
0
Assets/Scripts/Camera/.gitkeep
Normal file
17
Assets/Scripts/Camera/BaseGames.Camera.asmdef
Normal file
17
Assets/Scripts/Camera/BaseGames.Camera.asmdef
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Camera",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Camera",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"Unity.Cinemachine"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Camera/BaseGames.Camera.asmdef.meta
Normal file
7
Assets/Scripts/Camera/BaseGames.Camera.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b9cbc0f2e569d64a862f3b7f417c7b6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/Scripts/Camera/CameraBlendProfileSO.cs
Normal file
25
Assets/Scripts/Camera/CameraBlendProfileSO.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Camera/BlendProfile")]
|
||||
public class CameraBlendProfileSO : ScriptableObject
|
||||
{
|
||||
public CinemachineBlendDefinition.Styles Style = CinemachineBlendDefinition.Styles.EaseInOut;
|
||||
public float BlendTime = 0.5f;
|
||||
[Tooltip("Style = Custom 时使用")]
|
||||
public AnimationCurve CustomCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
/// <summary>转换为 Cinemachine 混合定义。</summary>
|
||||
public CinemachineBlendDefinition ToBlendDefinition()
|
||||
{
|
||||
return new CinemachineBlendDefinition
|
||||
{
|
||||
Style = this.Style,
|
||||
Time = this.BlendTime,
|
||||
CustomCurve = this.Style == CinemachineBlendDefinition.Styles.Custom ? CustomCurve : null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraBlendProfileSO.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraBlendProfileSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04f7b183b6d364d4ea85283d30339db7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
22
Assets/Scripts/Camera/CameraConfigSO.cs
Normal file
22
Assets/Scripts/Camera/CameraConfigSO.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Camera/CameraConfig")]
|
||||
public class CameraConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("跟随")]
|
||||
public float FollowDamping = 0.15f;
|
||||
public float LookAheadTime = 0.3f;
|
||||
public float LookAheadSmoothing = 0.1f;
|
||||
public Vector2 DeadZoneSize = new Vector2(1f, 0.5f);
|
||||
public Vector2 SoftZoneSize = new Vector2(2.5f, 2f);
|
||||
|
||||
[Header("偏移")]
|
||||
public float LookDownOffset = -1.5f;
|
||||
public float LookUpOffset = 1.5f;
|
||||
|
||||
[Header("画面抖动默认强度")]
|
||||
public float DefaultImpulseStrength = 0.3f;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraConfigSO.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b358a30ac16c6a34fb673ede0a288e48
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
87
Assets/Scripts/Camera/CameraStateController.cs
Normal file
87
Assets/Scripts/Camera/CameraStateController.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机状态单例控制器。管理房间相机切换、限位器更新与屏幕抖动。
|
||||
/// 须放置在持久化场景中。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class CameraStateController : MonoBehaviour
|
||||
{
|
||||
public static CameraStateController Instance { get; private set; }
|
||||
|
||||
[Header("引用")]
|
||||
[SerializeField] private CinemachineBrain _brain;
|
||||
[SerializeField] private CinemachineImpulseSource _impulseSource;
|
||||
|
||||
[Header("默认混合配置")]
|
||||
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
|
||||
|
||||
// ── 注册表 ────────────────────────────────────────────────────────────
|
||||
private readonly HashSet<RoomCamera> _registeredCameras = new HashSet<RoomCamera>();
|
||||
private RoomCamera _activeCamera;
|
||||
|
||||
// ── Unity Lifecycle ───────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>向控制器注册一个房间相机(可选,也可由触发器直接调用 SwitchRoom)。</summary>
|
||||
public void RegisterRoomCamera(RoomCamera camera)
|
||||
{
|
||||
if (camera != null) _registeredCameras.Add(camera);
|
||||
}
|
||||
|
||||
/// <summary>注销房间相机。</summary>
|
||||
public void UnregisterRoomCamera(RoomCamera camera)
|
||||
{
|
||||
if (camera != null) _registeredCameras.Remove(camera);
|
||||
}
|
||||
|
||||
/// <summary>切换到目标房间相机,并应用对应的混合配置。</summary>
|
||||
public void SwitchRoom(RoomCamera targetCamera)
|
||||
{
|
||||
if (targetCamera == null || targetCamera == _activeCamera) return;
|
||||
|
||||
// 应用混合配置到 Brain
|
||||
if (_brain != null)
|
||||
{
|
||||
var profile = targetCamera.BlendProfile ?? _defaultBlendProfile;
|
||||
if (profile != null)
|
||||
_brain.DefaultBlend = profile.ToBlendDefinition();
|
||||
}
|
||||
|
||||
// 禁用旧相机、启用新相机
|
||||
_activeCamera?.Deactivate();
|
||||
_activeCamera = targetCamera;
|
||||
_activeCamera.Activate();
|
||||
}
|
||||
|
||||
/// <summary>触发屏幕抖动。</summary>
|
||||
public void TriggerImpulse(Vector3 velocity)
|
||||
{
|
||||
if (_impulseSource != null)
|
||||
_impulseSource.GenerateImpulse(velocity);
|
||||
}
|
||||
|
||||
/// <summary>以默认强度触发屏幕抖动。</summary>
|
||||
public void TriggerImpulse(float strength = 0.3f)
|
||||
=> TriggerImpulse(Vector3.down * strength);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraStateController.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraStateController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49f718c655d71394ea13e312a2dd9eed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
Assets/Scripts/Camera/CameraTriggerZone.cs
Normal file
42
Assets/Scripts/Camera/CameraTriggerZone.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机区域切换触发器。玩家进入时通知 CameraStateController 切换到目标房间相机。
|
||||
/// [ExecuteAlways] 确保编辑器中 Gizmo 立即更新。
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(BoxCollider2D))]
|
||||
public class CameraTriggerZone : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RoomCamera _targetCamera;
|
||||
[SerializeField] private string _playerTag = "Player";
|
||||
|
||||
private BoxCollider2D _collider;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_collider = GetComponent<BoxCollider2D>();
|
||||
_collider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!other.CompareTag(_playerTag)) return;
|
||||
if (_targetCamera != null)
|
||||
CameraStateController.Instance?.SwitchRoom(_targetCamera);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (_collider == null) _collider = GetComponent<BoxCollider2D>();
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.25f);
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Gizmos.DrawCube(_collider.offset, _collider.size);
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.8f);
|
||||
Gizmos.DrawWireCube(_collider.offset, _collider.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraTriggerZone.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraTriggerZone.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 108d2b73047255f44a823dbcdea4a7fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/Camera/RoomCamera.cs
Normal file
33
Assets/Scripts/Camera/RoomCamera.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 单房间虚拟相机。激活时提升优先级,停用时降为 0。
|
||||
/// 挂载在每个房间的 CinemachineCamera GameObject 上。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CinemachineCamera))]
|
||||
public class RoomCamera : MonoBehaviour
|
||||
{
|
||||
[Header("房间设置")]
|
||||
[SerializeField] private RoomVisibleArea _visibleArea;
|
||||
[SerializeField] private Vector2 _cameraOffset = Vector2.zero;
|
||||
[SerializeField] private CameraBlendProfileSO _blendProfile;
|
||||
[SerializeField] private int _activePriority = 15;
|
||||
|
||||
private CinemachineCamera _vcam;
|
||||
|
||||
private void Awake() => _vcam = GetComponent<CinemachineCamera>();
|
||||
private void OnEnable() => _vcam.Priority = _activePriority;
|
||||
private void OnDisable() => _vcam.Priority = 0;
|
||||
|
||||
public PolygonCollider2D ConfinerCollider => _visibleArea?.Collider;
|
||||
public Vector2 CameraOffset => _cameraOffset;
|
||||
public CameraBlendProfileSO BlendProfile => _blendProfile;
|
||||
|
||||
/// <summary>在 CameraStateController 管理的激活流程中调用。</summary>
|
||||
public void Activate() => gameObject.SetActive(true);
|
||||
public void Deactivate() => gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/RoomCamera.cs.meta
Normal file
11
Assets/Scripts/Camera/RoomCamera.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af7e12583264b8c4da8dcd69df274793
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
30
Assets/Scripts/Camera/RoomVisibleArea.cs
Normal file
30
Assets/Scripts/Camera/RoomVisibleArea.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记房间的可见区域(多边形)。供 CinemachineConfiner2D 使用。
|
||||
/// [ExecuteAlways] 确保编辑器中碰撞体立即更新。
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(PolygonCollider2D))]
|
||||
public class RoomVisibleArea : MonoBehaviour
|
||||
{
|
||||
private PolygonCollider2D _collider;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_collider = GetComponent<PolygonCollider2D>();
|
||||
_collider.isTrigger = true;
|
||||
}
|
||||
|
||||
public PolygonCollider2D Collider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_collider == null) _collider = GetComponent<PolygonCollider2D>();
|
||||
return _collider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/RoomVisibleArea.cs.meta
Normal file
11
Assets/Scripts/Camera/RoomVisibleArea.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38af2eabab7039c4a919181e4c507d12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Camera/_Placeholder.cs
Normal file
3
Assets/Scripts/Camera/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Camera { }
|
||||
|
||||
11
Assets/Scripts/Camera/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Camera/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5097f44608b602a46a8b8304e2edf090
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Combat.meta
Normal file
8
Assets/Scripts/Combat.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 878e5c61f6bb064449bf2399b70026b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
17
Assets/Scripts/Combat/BaseGames.Combat.asmdef
Normal file
17
Assets/Scripts/Combat/BaseGames.Combat.asmdef
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Combat",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Combat",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Parry"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Combat/BaseGames.Combat.asmdef.meta
Normal file
7
Assets/Scripts/Combat/BaseGames.Combat.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8746e0f9f33d5d84ea0b598962cc36ae
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
82
Assets/Scripts/Combat/CombatEnums.cs
Normal file
82
Assets/Scripts/Combat/CombatEnums.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
// ── 元素/物理属性 ───────────────────────────────────────────────────────
|
||||
public enum DamageType { Normal, True, Fire, Poison, Ice, Lightning, Void }
|
||||
|
||||
// ── 来源分类 ────────────────────────────────────────────────────────────
|
||||
public enum DamageCategory
|
||||
{
|
||||
NormalAttack = 0,
|
||||
SoulSkill = 1,
|
||||
SpiritSkill = 2,
|
||||
Projectile = 3,
|
||||
EnvironmentTrap = 4,
|
||||
StatusEffect = 5,
|
||||
FallDamage = 6,
|
||||
Reflected = 7,
|
||||
}
|
||||
|
||||
// ── 行为标志 ────────────────────────────────────────────────────────────
|
||||
[Flags]
|
||||
public enum DamageFlags
|
||||
{
|
||||
None = 0,
|
||||
Unblockable = 1 << 0,
|
||||
CanBeParried = 1 << 1,
|
||||
IgnoreIFrame = 1 << 2,
|
||||
PerfectParryOnly = 1 << 3,
|
||||
IsProjectile = 1 << 4,
|
||||
CanClash = 1 << 5,
|
||||
ForceBreak = 1 << 6,
|
||||
NoKnockback = 1 << 7,
|
||||
}
|
||||
|
||||
// ── 交互标签 ────────────────────────────────────────────────────────────
|
||||
[Flags]
|
||||
public enum DamageTags : uint
|
||||
{
|
||||
None = 0,
|
||||
MeleeHit = 1 << 0,
|
||||
RangedHit = 1 << 1,
|
||||
SkillHit = 1 << 2,
|
||||
ElementFire = 1 << 3,
|
||||
ElementPoison = 1 << 4,
|
||||
ElementVoid = 1 << 5,
|
||||
AfterParry = 1 << 6,
|
||||
ChargedAttack = 1 << 7,
|
||||
SkyFormOnly = 1 << 8,
|
||||
EarthFormOnly = 1 << 9,
|
||||
DeathFormOnly = 1 << 10,
|
||||
BreakLight = 1 << 11,
|
||||
BreakMedium = 1 << 12,
|
||||
BreakHeavy = 1 << 13,
|
||||
BreakBreaker = 1 << 14,
|
||||
}
|
||||
|
||||
public enum HitFxType { Spark, Slash, Blood, Magic, Heavy, Crit, Void, Heal, Parry, Fire, Ice }
|
||||
|
||||
// ── 攻击方打断等级 ──────────────────────────────────────────────────────
|
||||
public enum BreakLevel
|
||||
{
|
||||
None = 0,
|
||||
Light = 1,
|
||||
Medium = 2,
|
||||
Heavy = 3,
|
||||
Breaker = 4,
|
||||
}
|
||||
|
||||
// ── 承受方霸体等级 ──────────────────────────────────────────────────────
|
||||
public enum PoiseLevel
|
||||
{
|
||||
None = 0,
|
||||
Light = 1,
|
||||
Medium = 2,
|
||||
Heavy = 3,
|
||||
Unbreakable = 100,
|
||||
}
|
||||
|
||||
// ── 攻击方向(PlayerCombat / WeaponSO 使用)────────────────────────────
|
||||
public enum AttackDirection { Ground, Up, Down, Air }
|
||||
}
|
||||
11
Assets/Scripts/Combat/CombatEnums.cs.meta
Normal file
11
Assets/Scripts/Combat/CombatEnums.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca3858c58d2156f4fbc2d295c444bd40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/Scripts/Combat/CombatInterfaces.cs
Normal file
48
Assets/Scripts/Combat/CombatInterfaces.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 可受击实体接口。PlayerController 和 EnemyBase 实现此接口。
|
||||
/// HurtBox.Awake 通过 GetComponentInParent<IDamageable>() 注入。
|
||||
/// </summary>
|
||||
public interface IDamageable
|
||||
{
|
||||
bool IsInvincible { get; }
|
||||
int Defense { get; }
|
||||
void TakeDamage(DamageInfo info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可持有霸体的实体接口。HurtBox 在 ReceiveDamage 中做等级比较。
|
||||
/// </summary>
|
||||
public interface IPoiseSource
|
||||
{
|
||||
PoiseLevel GetCurrentPoiseLevel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 护盾接口(玩家专属)。由 PlayerController.Awake() 注入 HurtBox。
|
||||
/// AbsorbDamage 返回穿透量(0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
|
||||
/// </summary>
|
||||
public interface IShieldable
|
||||
{
|
||||
bool HasShield { get; }
|
||||
int AbsorbDamage(int amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可破坏机关/障碍物接口。HitBox 在命中非 HurtBox 对象时尝试调用。
|
||||
/// </summary>
|
||||
public interface IBreakable
|
||||
{
|
||||
void TryInteract(DamageInfo info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可施加状态效果的实体接口(避免 Combat 直接引用 StatusEffects 程序集)。
|
||||
/// StatusEffectManager 实现此接口;HurtBox.ReceiveDamage 步骤 8 通过此接口调用。
|
||||
/// </summary>
|
||||
public interface IStatusEffectable
|
||||
{
|
||||
void ApplyStatusEffect(DamageType type);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/CombatInterfaces.cs.meta
Normal file
11
Assets/Scripts/Combat/CombatInterfaces.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7881dd1e194f2944a87ec5d50686740e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
82
Assets/Scripts/Combat/DamageInfo.cs
Normal file
82
Assets/Scripts/Combat/DamageInfo.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 单次伤害信息。流水线:RawDamage → Amount(护盾修改)→ FinalDamage(防御减免后)。
|
||||
/// ⚠️ 非 readonly struct — Builder 就地写入字段。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct DamageInfo
|
||||
{
|
||||
public int RawDamage; // HitBox 设定的原始值(Builder.SetRaw 写入一次)
|
||||
public int Amount; // 流水线中被护盾/防御修改
|
||||
public int FinalDamage; // HurtBox 写入,最终 HP 扣除量
|
||||
public Vector2 KnockbackDirection;
|
||||
public float KnockbackForce;
|
||||
public float HitStunDuration;
|
||||
public DamageType Type;
|
||||
public DamageCategory Category;
|
||||
public DamageFlags Flags;
|
||||
public DamageTags Tags;
|
||||
public Vector2 SourcePosition;
|
||||
public int SourceLayer;
|
||||
public HitFxType FxType;
|
||||
public BreakLevel Break;
|
||||
public string SourceId;
|
||||
public string SkillId;
|
||||
|
||||
// ── Builder ──────────────────────────────────────────────────────────
|
||||
public class Builder
|
||||
{
|
||||
private DamageInfo _d;
|
||||
|
||||
public Builder() { }
|
||||
|
||||
// SetRaw 同步初始化 Amount(Amount 始终以 RawDamage 为起点)
|
||||
public Builder SetRaw(int v) { _d.RawDamage = v; _d.Amount = v; return this; }
|
||||
public Builder SetType(DamageType v) { _d.Type = v; return this; }
|
||||
public Builder SetCategory(DamageCategory v){ _d.Category = v; return this; }
|
||||
public Builder SetFlags(DamageFlags v) { _d.Flags = v; return this; }
|
||||
public Builder SetTags(DamageTags v) { _d.Tags = v; return this; }
|
||||
public Builder SetSkillId(string v) { _d.SkillId = v; return this; }
|
||||
public Builder SetSourceId(string v) { _d.SourceId = v; return this; }
|
||||
public Builder SetKnockback(Vector2 dir, float force)
|
||||
{ _d.KnockbackDirection = dir; _d.KnockbackForce = force; return this; }
|
||||
public Builder SetStun(float dur) { _d.HitStunDuration = dur; return this; }
|
||||
public Builder SetFx(HitFxType v) { _d.FxType = v; return this; }
|
||||
public Builder SetBreak(BreakLevel v) { _d.Break = v; return this; }
|
||||
public Builder SetSourcePos(Vector2 v) { _d.SourcePosition = v; return this; }
|
||||
public Builder SetLayer(int v) { _d.SourceLayer = v; return this; }
|
||||
public DamageInfo Build() => _d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ⚡ 零堆分配工厂(热路径首选)。直接从 DamageSourceSO 填入基础字段。
|
||||
/// KnockbackDirection / SourcePosition / SourceLayer 等运行时字段由调用方就地赋值。
|
||||
/// </summary>
|
||||
public static DamageInfo From(DamageSourceSO so)
|
||||
{
|
||||
int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier);
|
||||
return new DamageInfo
|
||||
{
|
||||
RawDamage = baseAmt,
|
||||
Amount = baseAmt,
|
||||
Type = so.Type,
|
||||
Category = so.Category,
|
||||
Flags = so.Flags,
|
||||
Tags = so.Tags,
|
||||
HitStunDuration = so.HitStunDuration,
|
||||
FxType = so.FxType,
|
||||
Break = so.BreakLevel,
|
||||
SourceId = so.sourceId,
|
||||
SkillId = so.skillId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>伤害事件频道(EVT_DamageDealt)。</summary>
|
||||
[UnityEngine.CreateAssetMenu(menuName = "Events/DamageDealt")]
|
||||
public class DamageInfoEventChannelSO : BaseEventChannelSO<DamageInfo> { }
|
||||
}
|
||||
11
Assets/Scripts/Combat/DamageInfo.cs.meta
Normal file
11
Assets/Scripts/Combat/DamageInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78a4c2420f838e74aa97697d5da09b72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Assets/Scripts/Combat/DamageSourceSO.cs
Normal file
52
Assets/Scripts/Combat/DamageSourceSO.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击数据源 SO。描述单次攻击的基础伤害参数。
|
||||
/// ⚡ 热路径使用零分配工厂:DamageInfo.From(sourceSO)。
|
||||
/// 仅需链式覆盖多字段时才使用 CreateBuilder()。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Combat/DamageSource")]
|
||||
public class DamageSourceSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string sourceId;
|
||||
public string skillId;
|
||||
|
||||
[Header("Base")]
|
||||
public int BaseDamage = 10;
|
||||
public float DamageMultiplier = 1.0f;
|
||||
public DamageType Type = DamageType.Normal;
|
||||
public DamageCategory Category = DamageCategory.NormalAttack;
|
||||
public DamageFlags Flags = DamageFlags.CanBeParried;
|
||||
public DamageTags Tags = DamageTags.MeleeHit;
|
||||
|
||||
[Header("Physics")]
|
||||
public float KnockbackForce = 5f;
|
||||
public float HitStunDuration = 0.1f;
|
||||
public BreakLevel BreakLevel = BreakLevel.Light;
|
||||
|
||||
[Header("FX")]
|
||||
public HitFxType FxType = HitFxType.Slash;
|
||||
|
||||
[Header("Combo")]
|
||||
public float ComboWindowDuration = 0.4f;
|
||||
public float CancelWindowEnd = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// 链式 Builder(特殊场景使用,热路径改用 DamageInfo.From(this))。
|
||||
/// </summary>
|
||||
public DamageInfo.Builder CreateBuilder() => new DamageInfo.Builder()
|
||||
.SetRaw(Mathf.RoundToInt(BaseDamage * DamageMultiplier))
|
||||
.SetType(Type)
|
||||
.SetCategory(Category)
|
||||
.SetFlags(Flags)
|
||||
.SetTags(Tags)
|
||||
.SetStun(HitStunDuration)
|
||||
.SetFx(FxType)
|
||||
.SetBreak(BreakLevel)
|
||||
.SetSourceId(sourceId)
|
||||
.SetSkillId(skillId);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/DamageSourceSO.cs.meta
Normal file
11
Assets/Scripts/Combat/DamageSourceSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96b10c11e6173394a8fa8d9c614b0035
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
96
Assets/Scripts/Combat/HitBox.cs
Normal file
96
Assets/Scripts/Combat/HitBox.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
|
||||
/// Phase 1 简化:直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
|
||||
/// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class HitBox : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private DamageSourceSO _defaultSource;
|
||||
[SerializeField] private float _hitCooldown = 0.1f;
|
||||
|
||||
private DamageSourceSO _currentSource;
|
||||
private Transform _attackerTransform;
|
||||
private bool _isActive;
|
||||
|
||||
/// <summary>命中确认委托(PlayerCombat / EnemyCombat 订阅)。</summary>
|
||||
public System.Action<DamageInfo> OnHitConfirmed;
|
||||
|
||||
/// <summary>
|
||||
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
|
||||
/// ⚠️ 不存在 Activate(float duration) 重载。
|
||||
/// </summary>
|
||||
public void Activate(DamageSourceSO source = null, Transform attacker = null)
|
||||
{
|
||||
_currentSource = source ?? _defaultSource;
|
||||
_attackerTransform = attacker ?? transform;
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
public void Deactivate() => _isActive = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 确保 Collider2D 是 Trigger
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (!col.isTrigger)
|
||||
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_isActive = false;
|
||||
_hitCooldownTimers.Clear();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!_isActive) return;
|
||||
if (_currentSource == null)
|
||||
{
|
||||
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this);
|
||||
return;
|
||||
}
|
||||
if (!CheckCooldown(other)) return;
|
||||
|
||||
Vector2 knockDir = ((Vector2)other.bounds.center
|
||||
- (Vector2)_attackerTransform.position).normalized;
|
||||
|
||||
// ⚡ 零 GC:struct 工厂,就地赋值运行时字段
|
||||
var info = DamageInfo.From(_currentSource);
|
||||
info.KnockbackDirection = knockDir;
|
||||
info.KnockbackForce = _currentSource.KnockbackForce;
|
||||
info.SourcePosition = _attackerTransform.position;
|
||||
info.SourceLayer = _attackerTransform.gameObject.layer;
|
||||
|
||||
// ① 命中 HurtBox
|
||||
var hurtBox = other.GetComponent<HurtBox>();
|
||||
if (hurtBox != null)
|
||||
{
|
||||
hurtBox.ReceiveDamage(info);
|
||||
OnHitConfirmed?.Invoke(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// ② 命中 IBreakable(机关/障碍物)
|
||||
other.GetComponent<IBreakable>()?.TryInteract(info);
|
||||
}
|
||||
|
||||
// ── 同目标多帧命中冷却 ────────────────────────────────────────────────
|
||||
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new();
|
||||
|
||||
private bool CheckCooldown(Collider2D other)
|
||||
{
|
||||
float now = Time.time;
|
||||
if (_hitCooldownTimers.TryGetValue(other, out float last) && now - last < _hitCooldown)
|
||||
return false;
|
||||
_hitCooldownTimers[other] = now;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitBox.cs.meta
Normal file
11
Assets/Scripts/Combat/HitBox.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a655e2461396a8348a32a13144438e8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs
Normal file
8
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Events/HitConfirmed")]
|
||||
public class HitConfirmedEventChannelSO : BaseEventChannelSO<HitInfo> { }
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs.meta
Normal file
11
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e5ffa3ce0537845b1b601c267d76ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
17
Assets/Scripts/Combat/HitInfo.cs
Normal file
17
Assets/Scripts/Combat/HitInfo.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 命中信息(HurtBox.ReceiveDamage 广播给 VFX/Audio/Feedback)。
|
||||
/// </summary>
|
||||
public struct HitInfo
|
||||
{
|
||||
public DamageInfo DamageInfo;
|
||||
public Vector3 HitPoint;
|
||||
public Vector3 HitNormal;
|
||||
public Transform HitTransform;
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitInfo.cs.meta
Normal file
11
Assets/Scripts/Combat/HitInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5933018bd81bef48be815337eef02af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
106
Assets/Scripts/Combat/HurtBox.cs
Normal file
106
Assets/Scripts/Combat/HurtBox.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Parry;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 受击盒组件。实现完整 8 步伤害流水线(架构 06_CombatModule §5)。
|
||||
/// 挂载在角色根节点或指定子节点上,Collider2D 需设 IsTrigger = true,
|
||||
/// Layer = PlayerHurtBox 或 EnemyHurtBox。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class HurtBox : MonoBehaviour
|
||||
{
|
||||
// ── 伤害接受方(Awake 注入)──────────────────────────────────────────
|
||||
private IDamageable _owner;
|
||||
private IShieldable _shieldable; // 由 PlayerController.Awake() 注入
|
||||
private ParrySystem _parrySystem; // Phase 2 由 PlayerController.Awake() 注入
|
||||
private IPoiseSource _poiseSource; // Phase 2 由 EnemyBase.Awake() 注入
|
||||
|
||||
private bool _isHurtBoxInvincible;
|
||||
private bool _isActive = true;
|
||||
|
||||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||||
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
|
||||
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
|
||||
|
||||
// ── 注入接口 ──────────────────────────────────────────────────────────
|
||||
public void SetShieldable(IShieldable shieldable) => _shieldable = shieldable;
|
||||
public void SetParrySystem(ParrySystem ps) => _parrySystem = ps;
|
||||
public void SetPoiseSource(IPoiseSource src) => _poiseSource = src;
|
||||
public void SetInvincible(bool value) => _isHurtBoxInvincible = value;
|
||||
public void SetActive(bool value) => _isActive = value;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_owner = GetComponentInParent<IDamageable>();
|
||||
if (_owner == null)
|
||||
Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接受伤害(由 HitBox.OnTriggerEnter2D 直接调用)。
|
||||
/// ⚠️ 方法名必须为 ReceiveDamage。
|
||||
/// </summary>
|
||||
public void ReceiveDamage(DamageInfo info)
|
||||
{
|
||||
if (!_isActive || _owner == null) return;
|
||||
|
||||
// 1. 无敌帧检查
|
||||
if ((_owner.IsInvincible || _isHurtBoxInvincible)
|
||||
&& !info.Flags.HasFlag(DamageFlags.IgnoreIFrame)) return;
|
||||
|
||||
// 2. 弹反检查(Phase 1 _parrySystem == null 跳过)
|
||||
// ParrySystem 只暴露窗口状态,伤害数据留在 Combat 层,无跨程序集数据依赖。
|
||||
if (_parrySystem != null && info.Flags.HasFlag(DamageFlags.CanBeParried))
|
||||
if (_parrySystem.ConsumeParry()) return;
|
||||
|
||||
// 3. 霸体检查(Phase 1 _poiseSource == null 跳过)
|
||||
if (!info.Flags.HasFlag(DamageFlags.ForceBreak) && _poiseSource != null)
|
||||
{
|
||||
PoiseLevel curPoise = _poiseSource.GetCurrentPoiseLevel();
|
||||
if (curPoise == PoiseLevel.Unbreakable) return;
|
||||
if ((int)info.Break < (int)curPoise)
|
||||
{
|
||||
_onHitConfirmed?.Raise(new HitInfo
|
||||
{
|
||||
DamageInfo = info,
|
||||
HitPoint = transform.position,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 护盾层拦截(玩家专属,在防御减免前)
|
||||
if (_shieldable != null && _shieldable.HasShield)
|
||||
{
|
||||
int passThrough = _shieldable.AbsorbDamage(info.Amount);
|
||||
if (passThrough <= 0) return;
|
||||
info.Amount = passThrough;
|
||||
}
|
||||
|
||||
// 5. 计算 FinalDamage(防御减免,最低 1)
|
||||
int finalDamage = UnityEngine.Mathf.Max(1, info.Amount - _owner.Defense);
|
||||
info.Amount = finalDamage;
|
||||
info.FinalDamage = finalDamage;
|
||||
|
||||
// 6. 调用 _owner.TakeDamage
|
||||
_owner.TakeDamage(info);
|
||||
|
||||
// 7. 全局广播
|
||||
_onDamageDealt?.Raise(info);
|
||||
_onHitConfirmed?.Raise(new HitInfo
|
||||
{
|
||||
DamageInfo = info,
|
||||
HitPoint = transform.position,
|
||||
});
|
||||
|
||||
// 8. 状态效果触发(DoT — Fire / Poison)
|
||||
// 使用接口避免对 StatusEffects 程序集的直接依赖
|
||||
if (_owner is UnityEngine.MonoBehaviour mb)
|
||||
{
|
||||
mb.GetComponent<IStatusEffectable>()?.ApplyStatusEffect(info.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/HurtBox.cs.meta
Normal file
11
Assets/Scripts/Combat/HurtBox.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b7a233d7f70aa4f86b473412b826de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Scripts/Combat/ShieldComponent.cs
Normal file
20
Assets/Scripts/Combat/ShieldComponent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 护盾组件(Phase 1 存根)。实现 IShieldable 接口供 HurtBox 注入。
|
||||
/// Phase 2 实现完整护盾逻辑(护盾值、再生、破盾事件)。
|
||||
/// </summary>
|
||||
public class ShieldComponent : MonoBehaviour, IShieldable
|
||||
{
|
||||
public bool HasShield { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尝试以护盾吸收伤害。
|
||||
/// 返回穿透量(0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
|
||||
/// Phase 1:护盾不存在,全量穿透。
|
||||
/// </summary>
|
||||
public int AbsorbDamage(int amount) => amount;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/ShieldComponent.cs.meta
Normal file
11
Assets/Scripts/Combat/ShieldComponent.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f362045054d7c1945841c4ccbcb356e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Combat/StatusEffects.meta
Normal file
8
Assets/Scripts/Combat/StatusEffects.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37363fb905d771d45b74c104305f07dd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Combat/StatusEffects/.gitkeep
Normal file
0
Assets/Scripts/Combat/StatusEffects/.gitkeep
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Combat.StatusEffects",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Combat.StatusEffects",
|
||||
"references": [
|
||||
"BaseGames.Combat"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd947c06a464c1b4492d0417d28a8ccb
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs
Normal file
20
Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Combat.StatusEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态效果管理器(Phase 1 桩)。
|
||||
/// 实现 IStatusEffectable 接口,由 HurtBox 通过接口调用,避免程序集循环依赖。
|
||||
/// Phase 2 实现完整的效果叠加、持续时间、DoT 伤害计算。
|
||||
/// </summary>
|
||||
public class StatusEffectManager : MonoBehaviour, IStatusEffectable
|
||||
{
|
||||
// Phase 1:空实现
|
||||
public void ApplyStatusEffect(DamageType type) { }
|
||||
}
|
||||
|
||||
// ── Phase 1 占位效果类型 ──────────────────────────────────────────────────
|
||||
public class FireEffect { }
|
||||
public class PoisonEffect { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 708938b7c3d75b244abcbd30ed589461
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs
Normal file
3
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Combat.StatusEffects { }
|
||||
|
||||
11
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1dfc988231a6ac14a9aa035ba1719ab0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Combat/_Placeholder.cs
Normal file
3
Assets/Scripts/Combat/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Combat { }
|
||||
|
||||
11
Assets/Scripts/Combat/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Combat/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9c4356cd693b604bb0889f9538eb13e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Core.meta
Normal file
8
Assets/Scripts/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c662316bc2330ed4ab6607d5f3a731d5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Core/Assets.meta
Normal file
8
Assets/Scripts/Core/Assets.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f475eab1c9fa38649bf17b8b68d06d68
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/Scripts/Core/Assets/AddressKeyRegistry.cs
Normal file
84
Assets/Scripts/Core/Assets/AddressKeyRegistry.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时 Addressable Key 注册层(架构 13_AssetPoolModule §9)。
|
||||
/// 供 DLC / 扩展包在运行时动态注册额外地址键,不修改编译期常量类 <see cref="AddressKeys"/>。
|
||||
///
|
||||
/// 用法:
|
||||
/// // 注册(DLC 模块 Awake 时)
|
||||
/// AddressKeyRegistry.TryRegister("DLC_WeaponScythe", "DLC/WPN_Scythe");
|
||||
///
|
||||
/// // 查询(GlobalObjectPool.SpawnInternal 内部调用)
|
||||
/// if (AddressKeyRegistry.TryResolve(key, out var addr)) { ... }
|
||||
/// </summary>
|
||||
public static class AddressKeyRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, string> _registry = new();
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个运行时 key → Addressable 地址映射。
|
||||
/// 若 key 已存在则跳过,返回 false。
|
||||
/// </summary>
|
||||
public static bool TryRegister(string key, string address)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(address))
|
||||
{
|
||||
Debug.LogWarning($"[AddressKeyRegistry] TryRegister: key 或 address 不能为空。key={key}, address={address}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_registry.ContainsKey(key))
|
||||
{
|
||||
Debug.LogWarning($"[AddressKeyRegistry] key 已存在,跳过注册:{key}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_registry[key] = address;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制覆盖注册(用于测试 / 热更新覆盖)。
|
||||
/// </summary>
|
||||
public static void ForceRegister(string key, string address)
|
||||
{
|
||||
_registry[key] = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 key,返回对应的 Addressable 地址字符串。
|
||||
/// 若 key 未注册则返回原 key(兼容直接使用静态常量的调用方)。
|
||||
/// </summary>
|
||||
public static string Resolve(string key)
|
||||
{
|
||||
return _registry.TryGetValue(key, out var address) ? address : key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试解析 key,成功时 out address 为注册的地址字符串,返回 true。
|
||||
/// 若 key 未注册则返回 false,address 为 null。
|
||||
/// </summary>
|
||||
public static bool TryResolve(string key, out string address)
|
||||
{
|
||||
return _registry.TryGetValue(key, out address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定 key 的注册(场景卸载/DLC 卸载时调用)。
|
||||
/// </summary>
|
||||
public static void Unregister(string key) => _registry.Remove(key);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有注册(仅用于测试)。
|
||||
/// </summary>
|
||||
public static void Clear() => _registry.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前注册的所有 key 数量(调试用)。
|
||||
/// </summary>
|
||||
public static int Count => _registry.Count;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Assets/AddressKeyRegistry.cs.meta
Normal file
11
Assets/Scripts/Core/Assets/AddressKeyRegistry.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a64caba864b15a499c94f65e356e14e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Assets/Scripts/Core/Assets/AddressKeys.cs
Normal file
52
Assets/Scripts/Core/Assets/AddressKeys.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace BaseGames.Core.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// 所有 Addressable 地址字符串的静态常量类。
|
||||
/// 禁止在代码中直接使用字面字符串,统一引用此处的 const。
|
||||
/// 与 Assets 的 Addressable 标签分配保持同步(AddressKeyValidator 工具验证)。
|
||||
/// </summary>
|
||||
public static class AddressKeys
|
||||
{
|
||||
// ── Scenes ──────────────────────────────────────────────────────
|
||||
public const string ScenePersistent = "Scene_Persistent";
|
||||
public const string SceneMainMenu = "Scene_MainMenu";
|
||||
|
||||
// ── Player ──────────────────────────────────────────────────────
|
||||
public const string PrefabPlayer = "PLY_Player";
|
||||
|
||||
// ── Enemies ─────────────────────────────────────────────────────
|
||||
public const string PrefabEnemyGrunt = "ENM_GruntWarrior";
|
||||
public const string PrefabEnemySkullArch = "ENM_SkullArcher";
|
||||
|
||||
// ── Projectiles ─────────────────────────────────────────────────
|
||||
public const string PrefabProjArrow = "PROJ_Arrow";
|
||||
public const string PrefabProjFireball = "PROJ_Fireball";
|
||||
public const string PrefabProjSoulBall = "PROJ_SoulBall";
|
||||
|
||||
// ── VFX ─────────────────────────────────────────────────────────
|
||||
public const string PrefabVFXHitSpark = "VFX_HitSpark";
|
||||
public const string PrefabVFXBloodSplat = "VFX_BloodSplat";
|
||||
public const string PrefabVFXExplosion = "VFX_Explosion";
|
||||
|
||||
// ── UI ───────────────────────────────────────────────────────────
|
||||
public const string PrefabUIFloatingDmgText = "UI_FloatingDamageText";
|
||||
|
||||
// ── Collectibles ─────────────────────────────────────────────────
|
||||
public const string PrefabCollectibleGeo = "COL_Geo";
|
||||
public const string PrefabCollectibleHPOrb = "COL_HPOrb";
|
||||
|
||||
// ── Weapons ──────────────────────────────────────────────────────
|
||||
public const string PrefabWeaponSkyBlade = "WPN_SkyBlade";
|
||||
public const string PrefabWeaponEarthClaw = "WPN_EarthClaw";
|
||||
public const string PrefabWeaponSoulStaff = "WPN_SoulStaff";
|
||||
|
||||
// ── Config ScriptableObjects ─────────────────────────────────────
|
||||
public const string DataFootstepCatalog = "Config/FootstepCatalog";
|
||||
|
||||
// ── Labels(批量加载用)──────────────────────────────────────────
|
||||
public const string LabelEnemy = "Enemy";
|
||||
public const string LabelPoolable = "Poolable";
|
||||
public const string LabelBGM = "BGM";
|
||||
public const string LabelCharms = "Charms";
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Assets/AddressKeys.cs.meta
Normal file
11
Assets/Scripts/Core/Assets/AddressKeys.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faaab8ae1b4f5584688d2c294fcffa1a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
53
Assets/Scripts/Core/Assets/AssetLoader.cs
Normal file
53
Assets/Scripts/Core/Assets/AssetLoader.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
|
||||
namespace BaseGames.Core.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressables 运行时加载工具(薄封装)。
|
||||
/// 场景卸载时配合 <see cref="AssetReleaseTracker"/> 批量 Release。
|
||||
/// </summary>
|
||||
public static class AssetLoader
|
||||
{
|
||||
/// <summary>异步加载资产,返回 handle 供 Release 使用。</summary>
|
||||
public static async Task<(T asset, AsyncOperationHandle<T> handle)> LoadAsync<T>(string addressKey)
|
||||
{
|
||||
var handle = Addressables.LoadAssetAsync<T>(addressKey);
|
||||
var result = await handle.Task;
|
||||
return (result, handle);
|
||||
}
|
||||
|
||||
/// <summary>释放一个已加载的 handle(引用计数 -1)。</summary>
|
||||
public static void Release<T>(AsyncOperationHandle<T> handle)
|
||||
{
|
||||
if (handle.IsValid()) Addressables.Release(handle);
|
||||
}
|
||||
|
||||
/// <summary>释放一个 GameObject 实例(Addressables.ReleaseInstance)。</summary>
|
||||
public static bool ReleaseInstance(GameObject go)
|
||||
=> Addressables.ReleaseInstance(go);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 场景卸载时批量释放由 Addressables 加载的资产句柄。
|
||||
/// 挂在场景的根 GameObject 上;OnDestroy 时自动 Release 注册的所有 handle。
|
||||
/// </summary>
|
||||
public class AssetReleaseTracker : MonoBehaviour
|
||||
{
|
||||
private readonly List<AsyncOperationHandle> _handles = new();
|
||||
|
||||
public void Track<T>(AsyncOperationHandle<T> handle)
|
||||
=> _handles.Add(handle);
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var h in _handles)
|
||||
if (h.IsValid()) Addressables.Release(h);
|
||||
_handles.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Assets/AssetLoader.cs.meta
Normal file
11
Assets/Scripts/Core/Assets/AssetLoader.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b53704ca12a83b479fc3967704c642b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/Scripts/Core/Assets/AssetReleaseTracker.cs
Normal file
46
Assets/Scripts/Core/Assets/AssetReleaseTracker.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Assets;
|
||||
using BaseGames.Core.Pool;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产释放跟踪器。
|
||||
/// 事件驱动:监听 EVT_SceneLoadRequest,在新场景加载前清理旧场景的对象池缓存。
|
||||
/// ⚠️ 不使用显式注册 API;GlobalObjectPool.ClearPool 在场景切换时批量清理。
|
||||
/// </summary>
|
||||
public class AssetReleaseTracker : MonoBehaviour
|
||||
{
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
|
||||
|
||||
private string _lastLoadedScene;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onSceneLoadRequest != null)
|
||||
_onSceneLoadRequest.OnEventRaised += OnSceneLoadRequested;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onSceneLoadRequest != null)
|
||||
_onSceneLoadRequest.OnEventRaised -= OnSceneLoadRequested;
|
||||
}
|
||||
|
||||
private void OnSceneLoadRequested(SceneLoadRequest req)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_lastLoadedScene)) { _lastLoadedScene = req.SceneName; return; }
|
||||
|
||||
// 清除旧场景的敌人对象池缓存(按需扩展)
|
||||
if (GlobalObjectPool.Instance != null)
|
||||
{
|
||||
GlobalObjectPool.Instance.ClearPool(AddressKeys.PrefabEnemyGrunt);
|
||||
GlobalObjectPool.Instance.ClearPool(AddressKeys.PrefabEnemySkullArch);
|
||||
}
|
||||
|
||||
_lastLoadedScene = req.SceneName;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Assets/AssetReleaseTracker.cs.meta
Normal file
11
Assets/Scripts/Core/Assets/AssetReleaseTracker.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba91049904d9daa43a59032bc1481cd1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/Scripts/Core/BaseGames.Core.asmdef
Normal file
19
Assets/Scripts/Core/BaseGames.Core.asmdef
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Core",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Core",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Core.Save",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Core/BaseGames.Core.asmdef.meta
Normal file
7
Assets/Scripts/Core/BaseGames.Core.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e41e18c796a92334c8eb801039fc7440
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
141
Assets/Scripts/Core/BuiltinGameStates.cs
Normal file
141
Assets/Scripts/Core/BuiltinGameStates.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core.States
|
||||
{
|
||||
/// <summary>初始化状态(应用启动时的第一个状态)。</summary>
|
||||
public class InitializingState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.Initializing;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.MainMenu };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>主菜单状态。</summary>
|
||||
public class MainMenuState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.MainMenu;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.LoadingScene };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>场景加载中状态。</summary>
|
||||
public class LoadingSceneState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.LoadingScene;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.MainMenu, GameStates.Gameplay };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>正常游玩状态。</summary>
|
||||
public class GameplayState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.Gameplay;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId>
|
||||
{
|
||||
GameStates.LoadingScene,
|
||||
GameStates.BossFight,
|
||||
GameStates.Paused,
|
||||
GameStates.Dead,
|
||||
GameStates.Cutscene,
|
||||
};
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>Boss 战状态。</summary>
|
||||
public class BossFightState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.BossFight;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId>
|
||||
{
|
||||
GameStates.LoadingScene,
|
||||
GameStates.Gameplay,
|
||||
GameStates.Paused,
|
||||
GameStates.Dead,
|
||||
};
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>暂停状态。</summary>
|
||||
public class PausedState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.Paused;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId>
|
||||
{
|
||||
GameStates.Gameplay,
|
||||
GameStates.BossFight,
|
||||
GameStates.MainMenu,
|
||||
};
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>死亡状态。</summary>
|
||||
public class DeadState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.Dead;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.LoadingScene, GameStates.GameOver };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>过场动画状态。</summary>
|
||||
public class CutsceneState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.Cutscene;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.Gameplay };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
|
||||
/// <summary>Game Over 状态(SteelSoul 清档用)。</summary>
|
||||
public class GameOverState : IGameState
|
||||
{
|
||||
public GameStateId Id => GameStates.GameOver;
|
||||
|
||||
public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
|
||||
new HashSet<GameStateId> { GameStates.MainMenu };
|
||||
|
||||
public void OnEnter(GameStateId prev) { }
|
||||
public void OnExit(GameStateId next) { }
|
||||
public void Tick(float dt) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/BuiltinGameStates.cs.meta
Normal file
11
Assets/Scripts/Core/BuiltinGameStates.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9bcf1a51cdbfa94a8e3d63c25626d34
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
78
Assets/Scripts/Core/DeathRespawnService.cs
Normal file
78
Assets/Scripts/Core/DeathRespawnService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 死亡/复活流程服务接口。
|
||||
/// </summary>
|
||||
public interface IDeathRespawnService
|
||||
{
|
||||
/// <summary>玩家死亡时由 GameManager 调用,启动死亡演出流程。</summary>
|
||||
IEnumerator StartDeathSequenceCoroutine();
|
||||
|
||||
/// <summary>DeathScreen 确认按钮点击后调用,执行复活流程。</summary>
|
||||
IEnumerator StartRespawnCoroutine();
|
||||
|
||||
/// <summary>SteelSoul 模式:HP 归零后直接清档并返回主菜单。</summary>
|
||||
IEnumerator StartGameOverCoroutine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 死亡/复活流程独立服务(Phase 0 骨架,Phase 1 完整实现)。
|
||||
/// </summary>
|
||||
public class DeathRespawnService : MonoBehaviour, IDeathRespawnService
|
||||
{
|
||||
[Header("Config")]
|
||||
[SerializeField] private float _deathAnimDuration = 1.2f;
|
||||
[SerializeField] private float _deathScreenDelay = 0.5f;
|
||||
[SerializeField] private float _respawnFadeDuration = 0.4f;
|
||||
|
||||
[Header("Event Channels - Raise")]
|
||||
[SerializeField] private VoidEventChannelSO _onRespawnStarted;
|
||||
[SerializeField] private VoidEventChannelSO _onRespawnCompleted;
|
||||
|
||||
[Header("Event Channels - Listen")]
|
||||
[SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed;
|
||||
|
||||
private bool _deathConfirmed;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onDeathScreenConfirmed != null)
|
||||
_onDeathScreenConfirmed.OnEventRaised += HandleDeathScreenConfirmed;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onDeathScreenConfirmed != null)
|
||||
_onDeathScreenConfirmed.OnEventRaised -= HandleDeathScreenConfirmed;
|
||||
}
|
||||
|
||||
private void HandleDeathScreenConfirmed() => _deathConfirmed = true;
|
||||
|
||||
public IEnumerator StartDeathSequenceCoroutine()
|
||||
{
|
||||
yield return new WaitForSeconds(_deathAnimDuration);
|
||||
yield return new WaitForSeconds(_deathScreenDelay);
|
||||
_deathConfirmed = false;
|
||||
yield return new WaitUntil(() => _deathConfirmed);
|
||||
}
|
||||
|
||||
public IEnumerator StartRespawnCoroutine()
|
||||
{
|
||||
_onRespawnStarted?.Raise();
|
||||
yield return new WaitForSeconds(_respawnFadeDuration);
|
||||
// Phase 1:加载存档场景(TODO)
|
||||
yield return new WaitForSeconds(_respawnFadeDuration);
|
||||
_onRespawnCompleted?.Raise();
|
||||
}
|
||||
|
||||
public IEnumerator StartGameOverCoroutine()
|
||||
{
|
||||
// Phase 1:SteelSoul 清档并返回主菜单(TODO)
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/DeathRespawnService.cs.meta
Normal file
11
Assets/Scripts/Core/DeathRespawnService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 843f5718ab6dbb7418fa7a036a83efc9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Core/Events.meta
Normal file
8
Assets/Scripts/Core/Events.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3771751cae7bcd04b96f7d9026a962aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Core/Events/.gitkeep
Normal file
0
Assets/Scripts/Core/Events/.gitkeep
Normal file
61
Assets/Scripts/Core/Events/BaseEventChannelSO.cs
Normal file
61
Assets/Scripts/Core/Events/BaseEventChannelSO.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 泛型 SO 事件频道基类。T 为负载类型。
|
||||
/// </summary>
|
||||
public abstract class BaseEventChannelSO<T> : ScriptableObject
|
||||
{
|
||||
[Multiline] public string description;
|
||||
|
||||
public event Action<T> OnEventRaised;
|
||||
|
||||
public void Raise(T value)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EventBusMonitor.Record(name, value?.ToString() ?? "null",
|
||||
OnEventRaised?.GetInvocationList().Length ?? 0);
|
||||
#endif
|
||||
OnEventRaised?.Invoke(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅并返回可 Dispose 的订阅句柄,配合 CompositeDisposable 使用。
|
||||
/// </summary>
|
||||
public EventSubscription Subscribe(Action<T> callback)
|
||||
{
|
||||
OnEventRaised += callback;
|
||||
return new EventSubscription(() => OnEventRaised -= callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无负载事件频道基类。
|
||||
/// </summary>
|
||||
public abstract class VoidBaseEventChannelSO : ScriptableObject
|
||||
{
|
||||
[Multiline] public string description;
|
||||
|
||||
public event Action OnEventRaised;
|
||||
|
||||
public void Raise()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EventBusMonitor.Record(name, "<void>",
|
||||
OnEventRaised?.GetInvocationList().Length ?? 0);
|
||||
#endif
|
||||
OnEventRaised?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅并返回可 Dispose 的订阅句柄。
|
||||
/// </summary>
|
||||
public EventSubscription Subscribe(Action callback)
|
||||
{
|
||||
OnEventRaised += callback;
|
||||
return new EventSubscription(() => OnEventRaised -= callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Events/BaseEventChannelSO.cs.meta
Normal file
11
Assets/Scripts/Core/Events/BaseEventChannelSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: adcb05b71d0e3f94f8c4446dee4c253b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Scripts/Core/Events/BaseGames.Core.Events.asmdef
Normal file
14
Assets/Scripts/Core/Events/BaseGames.Core.Events.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "BaseGames.Core.Events",
|
||||
"rootNamespace": "BaseGames.Core.Events",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf44e3f184fd4214eb09a80e6e04d7df
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
22
Assets/Scripts/Core/Events/BossEvents.cs
Normal file
22
Assets/Scripts/Core/Events/BossEvents.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 技能事件负载。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct BossSkillEvent
|
||||
{
|
||||
public string BossId;
|
||||
public string SkillId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boss 阶段切换事件负载。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct BossPhaseEvent
|
||||
{
|
||||
public string BossId;
|
||||
public int Phase;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Core/Events/BossEvents.cs.meta
Normal file
11
Assets/Scripts/Core/Events/BossEvents.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac3dc174ca0e12544a31550b5f61e70b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
Assets/Scripts/Core/Events/BossPhaseEventChannelSO.cs
Normal file
7
Assets/Scripts/Core/Events/BossPhaseEventChannelSO.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Events/BossPhase")]
|
||||
public class BossPhaseEventChannelSO : BaseEventChannelSO<BossPhaseEvent> { }
|
||||
}
|
||||
11
Assets/Scripts/Core/Events/BossPhaseEventChannelSO.cs.meta
Normal file
11
Assets/Scripts/Core/Events/BossPhaseEventChannelSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 647b6596e515ba64483b7ff337c76699
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
Assets/Scripts/Core/Events/BossSkillEventChannelSO.cs
Normal file
7
Assets/Scripts/Core/Events/BossSkillEventChannelSO.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Events/BossSkill")]
|
||||
public class BossSkillEventChannelSO : BaseEventChannelSO<BossSkillEvent> { }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user