角色能力,存档
This commit is contained in:
111
Assets/_Game/Scripts/Core/AutoSaveService.cs
Normal file
111
Assets/_Game/Scripts/Core/AutoSaveService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动存档服务:在关键游戏事件发生后自动写盘,减少玩家因意外中断损失进度。
|
||||
///
|
||||
/// 触发时机:
|
||||
/// · 进入新房间 — EVT_SceneLoaded
|
||||
/// · 击败 Boss — EVT_BossFightEnded(bool=true 表示玩家获胜)
|
||||
/// · 获得新能力 — EVT_AbilityUnlockedStr
|
||||
/// · 购买物品 — EVT_ShopPurchase
|
||||
/// · 拾取关键物品 — EVT_CollectiblePickup
|
||||
/// · 拾取 HP 容器 — EVT_MaxHPContainerPickedUp
|
||||
/// · 开门/交互机关 — EVT_DoorOpened(使用钥匙、触发机关等)
|
||||
/// · 完成任务 — EVT_QuestStateChanged(State == Completed)
|
||||
///
|
||||
/// 自动存档使用与手动存档(存档点)相同的存档槽。
|
||||
/// 自动存档不会更改复活位置——SavePoint.OnSave 的 _isActivated 守卫确保
|
||||
/// 只有玩家已坐过的存档点才会写入 Player.Scene 和 Meta.SavePointId。
|
||||
///
|
||||
/// 防抖:同一冷却窗口内的多次触发合并为一次写盘操作,避免频繁 I/O。
|
||||
///
|
||||
/// 挂载位置:Persistent 场景根对象(与 GameServiceRegistrar 同级)。
|
||||
/// </summary>
|
||||
public class AutoSaveService : MonoBehaviour
|
||||
{
|
||||
[Header("触发事件频道")]
|
||||
[Tooltip("EVT_SceneLoaded — 进入新房间时自动存档")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
[Tooltip("EVT_BossFightEnded — bool=true 表示玩家获胜,此时触发存档")]
|
||||
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
|
||||
|
||||
[Tooltip("EVT_AbilityUnlockedStr — 获得新能力时触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onAbilityUnlocked;
|
||||
|
||||
[Tooltip("EVT_ShopPurchase — 购买物品后触发存档")]
|
||||
[SerializeField] private ShopPurchaseEventChannelSO _onShopPurchase;
|
||||
|
||||
[Tooltip("EVT_CollectiblePickup — 拾取关键物品(护符、道具等)后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onCollectiblePickup;
|
||||
|
||||
[Tooltip("EVT_MaxHPContainerPickedUp — 拾取 HP 容器后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onMaxHPContainerPickedUp;
|
||||
|
||||
[Tooltip("EVT_DoorOpened — 使用钥匙或触发机关开门后触发存档")]
|
||||
[SerializeField] private StringEventChannelSO _onDoorOpened;
|
||||
|
||||
[Tooltip("EVT_QuestStateChanged — 任务完成(State == Completed)时触发存档")]
|
||||
[SerializeField] private QuestStateChangedEventChannel _onQuestStateChanged;
|
||||
|
||||
[Header("防抖")]
|
||||
[Tooltip("两次自动存档之间的最短间隔(秒)。防止短时间内多次触发导致频繁写盘。")]
|
||||
[SerializeField] [Range(0.5f, 10f)] private float _cooldownSeconds = 2f;
|
||||
|
||||
private bool _onCooldown;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSceneLoaded? .Subscribe(OnSceneLoaded) .AddTo(_subs);
|
||||
_onBossFightEnded? .Subscribe(OnBossFightEnded) .AddTo(_subs);
|
||||
_onAbilityUnlocked? .Subscribe(_ => RequestAutoSave("ability")) .AddTo(_subs);
|
||||
_onShopPurchase? .Subscribe(_ => RequestAutoSave("shop")) .AddTo(_subs);
|
||||
_onCollectiblePickup? .Subscribe(_ => RequestAutoSave("collectible")) .AddTo(_subs);
|
||||
_onMaxHPContainerPickedUp? .Subscribe(_ => RequestAutoSave("hp_container")) .AddTo(_subs);
|
||||
_onDoorOpened? .Subscribe(_ => RequestAutoSave("door")) .AddTo(_subs);
|
||||
_onQuestStateChanged? .Subscribe(OnQuestStateChanged) .AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── 触发处理 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSceneLoaded(string _) => RequestAutoSave("scene");
|
||||
private void OnBossFightEnded(bool won) { if (won) RequestAutoSave("boss"); }
|
||||
private void OnQuestStateChanged(QuestStateChangedEvent e)
|
||||
{
|
||||
if (e.State == QuestState.Completed) RequestAutoSave("quest_complete");
|
||||
}
|
||||
|
||||
// ── 防抖存档 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void RequestAutoSave(string reason)
|
||||
{
|
||||
if (_onCooldown) return;
|
||||
StartCoroutine(DoAutoSave(reason));
|
||||
}
|
||||
|
||||
private IEnumerator DoAutoSave(string reason)
|
||||
{
|
||||
_onCooldown = true;
|
||||
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null)
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.Log($"[AutoSave] 触发:{reason}");
|
||||
#endif
|
||||
// 与手动存档使用相同槽位,fire-and-forget
|
||||
_ = svc.SaveAsync(svc.ActiveSlot);
|
||||
}
|
||||
|
||||
yield return new WaitForSecondsRealtime(_cooldownSeconds);
|
||||
_onCooldown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/AutoSaveService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/AutoSaveService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bdcd96d81e589642a250987222c25e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/_Game/Scripts/Core/CheckpointService.cs
Normal file
48
Assets/_Game/Scripts/Core/CheckpointService.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查点服务实现。挂载在 Persistent 场景根对象上,由 GameServiceRegistrar 注册。
|
||||
/// 订阅 EVT_SceneLoaded,换房间时自动清空当前检查点。
|
||||
/// </summary>
|
||||
public class CheckpointService : MonoBehaviour, ICheckpointService
|
||||
{
|
||||
[Header("事件 - 监听")]
|
||||
[Tooltip("EVT_SceneLoaded — 收到后自动清空检查点")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
private bool _hasCheckpoint;
|
||||
private Vector2 _checkpointPosition;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSceneLoaded?.Subscribe(OnSceneLoaded).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── ICheckpointService ────────────────────────────────────────────────
|
||||
|
||||
public bool HasCheckpoint => _hasCheckpoint;
|
||||
public Vector2 CheckpointPosition => _checkpointPosition;
|
||||
|
||||
public void RegisterCheckpoint(Vector2 position)
|
||||
{
|
||||
_hasCheckpoint = true;
|
||||
_checkpointPosition = position;
|
||||
}
|
||||
|
||||
public void ClearCheckpoint()
|
||||
{
|
||||
_hasCheckpoint = false;
|
||||
}
|
||||
|
||||
// ── Private ───────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSceneLoaded(string _) => ClearCheckpoint();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/CheckpointService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/CheckpointService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 486e785d32d1c4c468a4eb0fd4cf1822
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -25,6 +25,7 @@ namespace BaseGames.Core
|
||||
[SerializeField] private SceneService _sceneService;
|
||||
[SerializeField] private EventChannelRegistry _eventChannelRegistry;
|
||||
[SerializeField] private GameSaveManager _saveManager;
|
||||
[SerializeField] private CheckpointService _checkpointService;
|
||||
/// <summary>
|
||||
/// Persistent 场景中唯一保留的主 AudioListener(通常挂在主相机上)。
|
||||
/// 在 Inspector 中绑定后可完全跳过 Awake 时的 FindObjectsOfType 全场景扫描。
|
||||
@@ -69,12 +70,18 @@ namespace BaseGames.Core
|
||||
else
|
||||
Debug.LogWarning("[GameServiceRegistrar] ⚠ _saveManager 未绑定,ISaveService 未注册。", this);
|
||||
|
||||
if (_checkpointService)
|
||||
ServiceLocator.Register<ICheckpointService>(_checkpointService);
|
||||
else
|
||||
Debug.LogWarning("[GameServiceRegistrar] ⚠ _checkpointService 未绑定,ICheckpointService 未注册。", this);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var sb = new System.Text.StringBuilder("[GameServiceRegistrar] ✅ 服务注册完成:");
|
||||
if (_deathRespawnService) sb.Append(" IDeathRespawnService");
|
||||
if (_sceneService) sb.Append(" | ISceneService");
|
||||
if (_eventChannelRegistry) sb.Append(" | IEventChannelRegistry");
|
||||
if (_saveManager) sb.Append(" | ISaveService");
|
||||
if (_checkpointService) sb.Append(" | ICheckpointService");
|
||||
sb.Append(" | IAudioService(Null→等待覆盖)");
|
||||
Debug.Log(sb.ToString(), this);
|
||||
#endif
|
||||
|
||||
30
Assets/_Game/Scripts/Core/ICheckpointService.cs
Normal file
30
Assets/_Game/Scripts/Core/ICheckpointService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查点服务接口。
|
||||
/// 运行时追踪玩家在当前房间内最近经过的检查点坐标,不持久化至存档。
|
||||
/// 换房间时由 CheckpointService 自动清空。
|
||||
/// </summary>
|
||||
public interface ICheckpointService
|
||||
{
|
||||
/// <summary>当前场景内是否存在已激活的检查点。</summary>
|
||||
bool HasCheckpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近激活的检查点世界坐标。
|
||||
/// <see cref="HasCheckpoint"/> 为 false 时无意义。
|
||||
/// </summary>
|
||||
Vector2 CheckpointPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 将指定位置登记为当前检查点(由 CheckpointMarker 调用)。
|
||||
/// 同一场景内多次调用时以最新值为准。
|
||||
/// </summary>
|
||||
void RegisterCheckpoint(Vector2 position);
|
||||
|
||||
/// <summary>清空当前检查点(换场景时自动调用)。</summary>
|
||||
void ClearCheckpoint();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/ICheckpointService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/ICheckpointService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 985eec4caa88c87428f183c8b6a6e6ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user