多轮审查和修复
This commit is contained in:
8
Assets/Scripts/Support/Accessibility.meta
Normal file
8
Assets/Scripts/Support/Accessibility.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1321d24ceb7521347a8f3db80c553902
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
89
Assets/Scripts/Support/Accessibility/AccessibilityManager.cs
Normal file
89
Assets/Scripts/Support/Accessibility/AccessibilityManager.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Support.Accessibility
|
||||
{
|
||||
/// <summary>
|
||||
/// 无障碍功能管理器(架构 16_SupportingModules §6.1)。
|
||||
/// 响应设置变更,广播色盲模式事件;提供 CanPlayScreenShake() 供 FeedbackSystem 查询。
|
||||
/// </summary>
|
||||
public class AccessibilityManager : MonoBehaviour
|
||||
{
|
||||
[Header("设置资产")]
|
||||
[SerializeField] private AccessibilitySettingsSO _settings;
|
||||
|
||||
[Header("事件频道(输出)")]
|
||||
[SerializeField] private ColorblindModeEventChannelSO _onColorblindModeChanged;
|
||||
[SerializeField] private BoolEventChannelSO _onScreenShakeChanged;
|
||||
|
||||
private static AccessibilityManager _instance;
|
||||
|
||||
// ── 静态查询接口(供 FeedbackSystem 使用) ───────────────────────────────
|
||||
public static bool CanPlayScreenShake()
|
||||
=> _instance == null || (_instance._settings != null && _instance._settings.ScreenShake);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Debug.LogWarning("[AccessibilityManager] 已存在实例,请确保本组件仅放置在 Persistent 场景中。", this);
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
if (_settings != null)
|
||||
_settings.Load();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 初始化时广播当前色盲模式
|
||||
if (_settings != null)
|
||||
_onColorblindModeChanged?.Raise(_settings.ColorblindMode);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_instance == this) _instance = null;
|
||||
}
|
||||
|
||||
/// <summary>应用新的设置并持久化。</summary>
|
||||
public void Apply(AccessibilitySettingsSO newSettings)
|
||||
{
|
||||
_settings.ScreenShake = newSettings.ScreenShake;
|
||||
_settings.ReducedFlash = newSettings.ReducedFlash;
|
||||
_settings.LargeText = newSettings.LargeText;
|
||||
_settings.HighContrast = newSettings.HighContrast;
|
||||
_settings.UIScale = newSettings.UIScale;
|
||||
|
||||
bool colorblindChanged = _settings.ColorblindMode != newSettings.ColorblindMode;
|
||||
_settings.ColorblindMode = newSettings.ColorblindMode;
|
||||
|
||||
_settings.Save();
|
||||
|
||||
_onScreenShakeChanged?.Raise(_settings.ScreenShake);
|
||||
if (colorblindChanged)
|
||||
_onColorblindModeChanged?.Raise(_settings.ColorblindMode);
|
||||
}
|
||||
|
||||
/// <summary>直接设置色盲模式。</summary>
|
||||
public void SetColorblindMode(ColorblindMode mode)
|
||||
{
|
||||
if (_settings == null) return;
|
||||
_settings.ColorblindMode = mode;
|
||||
_settings.Save();
|
||||
_onColorblindModeChanged?.Raise(mode);
|
||||
}
|
||||
|
||||
/// <summary>直接设置屏幕抖动开关。</summary>
|
||||
public void SetScreenShake(bool enabled)
|
||||
{
|
||||
if (_settings == null) return;
|
||||
_settings.ScreenShake = enabled;
|
||||
_settings.Save();
|
||||
_onScreenShakeChanged?.Raise(enabled);
|
||||
}
|
||||
|
||||
public AccessibilitySettingsSO Settings => _settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db1f332f741502f4385b7d639582a8cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
/// <summary>
|
||||
/// 无障碍设置数据 SO(架构 16_SupportingModules §6)。
|
||||
/// 包含屏幕抖动、闪光减弱、色盲模式等辅助功能开关。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Settings/AccessibilitySettings", fileName = "SET_Accessibility")]
|
||||
public class AccessibilitySettingsSO : ScriptableObject
|
||||
{
|
||||
private const string PrefsPrefix = "accessibility_";
|
||||
|
||||
[Header("屏幕体感")]
|
||||
public bool ScreenShake = true;
|
||||
public bool ReducedFlash = false;
|
||||
|
||||
[Header("色盲模式")]
|
||||
public ColorblindMode ColorblindMode = ColorblindMode.None;
|
||||
|
||||
[Header("文字与 UI")]
|
||||
public bool LargeText = false;
|
||||
public bool HighContrast = false;
|
||||
public float UIScale = 1f;
|
||||
|
||||
// ── 持久化 ──────────────────────────────────────────────────────────────
|
||||
public void Save()
|
||||
{
|
||||
PlayerPrefs.SetInt (PrefsPrefix + "ScreenShake", ScreenShake ? 1 : 0);
|
||||
PlayerPrefs.SetInt (PrefsPrefix + "ReducedFlash", ReducedFlash ? 1 : 0);
|
||||
PlayerPrefs.SetInt (PrefsPrefix + "ColorblindMode",(int)ColorblindMode);
|
||||
PlayerPrefs.SetInt (PrefsPrefix + "LargeText", LargeText ? 1 : 0);
|
||||
PlayerPrefs.SetInt (PrefsPrefix + "HighContrast", HighContrast ? 1 : 0);
|
||||
PlayerPrefs.SetFloat (PrefsPrefix + "UIScale", UIScale);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
ScreenShake = PlayerPrefs.GetInt (PrefsPrefix + "ScreenShake", 1) == 1;
|
||||
ReducedFlash = PlayerPrefs.GetInt (PrefsPrefix + "ReducedFlash", 0) == 1;
|
||||
ColorblindMode = (ColorblindMode)PlayerPrefs.GetInt(PrefsPrefix + "ColorblindMode", 0);
|
||||
LargeText = PlayerPrefs.GetInt (PrefsPrefix + "LargeText", 0) == 1;
|
||||
HighContrast = PlayerPrefs.GetInt (PrefsPrefix + "HighContrast", 0) == 1;
|
||||
UIScale = PlayerPrefs.GetFloat(PrefsPrefix + "UIScale", 1f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1cbca9ad14985d469becb19e074f8a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
137
Assets/Scripts/Support/Accessibility/ColorBlindFilter.cs
Normal file
137
Assets/Scripts/Support/Accessibility/ColorBlindFilter.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
/// <summary>
|
||||
/// 色盲滤镜 URP Renderer Feature(架构 16_SupportingModules §6.2)。
|
||||
/// 通过颜色矩阵将画面转换到对应色盲友好的色彩空间。
|
||||
/// 配置:在 URP Renderer Asset 的 Renderer Features 列表中添加此 Feature。
|
||||
/// 订阅 EVT_ColorBlindModeChanged 事件切换模式。
|
||||
/// </summary>
|
||||
public class ColorBlindFilter : ScriptableRendererFeature
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
public ColorblindMode Mode = ColorblindMode.None;
|
||||
public float Strength = 1f;
|
||||
public RenderPassEvent PassEvent = RenderPassEvent.AfterRenderingPostProcessing;
|
||||
}
|
||||
|
||||
[SerializeField] public Settings settings = new();
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private ColorblindModeEventChannelSO _onColorblindModeChanged;
|
||||
|
||||
private ColorBlindRenderPass _pass;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
public override void Create()
|
||||
{
|
||||
_pass = new ColorBlindRenderPass(settings);
|
||||
_pass.renderPassEvent = settings.PassEvent;
|
||||
}
|
||||
|
||||
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
|
||||
{
|
||||
if (settings.Mode == ColorblindMode.None) return;
|
||||
renderer.EnqueuePass(_pass);
|
||||
}
|
||||
|
||||
private void OnEnable() => _onColorblindModeChanged?.Subscribe(SetMode).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
/// <summary>切换色盲模式(由 AccessibilityManager 通过事件调用)。</summary>
|
||||
public void SetMode(ColorblindMode mode)
|
||||
{
|
||||
settings.Mode = mode;
|
||||
_pass?.UpdateSettings(settings);
|
||||
}
|
||||
|
||||
// ── 内部 RenderPass ───────────────────────────────────────────────────────
|
||||
private class ColorBlindRenderPass : ScriptableRenderPass
|
||||
{
|
||||
private Settings _settings;
|
||||
private Material _material;
|
||||
|
||||
private static readonly int ColorMatrixId = Shader.PropertyToID("_ColorMatrix");
|
||||
|
||||
public ColorBlindRenderPass(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_material = CoreUtils.CreateEngineMaterial(Shader.Find("Hidden/ColorBlindFilter"));
|
||||
}
|
||||
|
||||
public void UpdateSettings(Settings s)
|
||||
{
|
||||
_settings = s;
|
||||
}
|
||||
|
||||
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
|
||||
{
|
||||
if (_material == null || _settings.Mode == ColorblindMode.None) return;
|
||||
|
||||
var matrix = GetColorMatrix(_settings.Mode, _settings.Strength);
|
||||
_material.SetMatrix(ColorMatrixId, matrix);
|
||||
|
||||
CommandBuffer cmd = CommandBufferPool.Get("ColorBlindFilter");
|
||||
// Blit with material applies the color matrix shader
|
||||
Blit(cmd, ref renderingData, _material);
|
||||
context.ExecuteCommandBuffer(cmd);
|
||||
CommandBufferPool.Release(cmd);
|
||||
}
|
||||
|
||||
/// <summary>返回指定色盲类型的颜色变换矩阵。</summary>
|
||||
private static Matrix4x4 GetColorMatrix(ColorblindMode mode, float strength)
|
||||
{
|
||||
// 基于 Brettel/Viénot 等人的色盲模拟矩阵
|
||||
Matrix4x4 identity = Matrix4x4.identity;
|
||||
Matrix4x4 sim = mode switch
|
||||
{
|
||||
ColorblindMode.Protanopia => Protanopia,
|
||||
ColorblindMode.Deuteranopia => Deuteranopia,
|
||||
ColorblindMode.Tritanopia => Tritanopia,
|
||||
ColorblindMode.Achromatopsia=> Achromatopsia,
|
||||
_ => identity,
|
||||
};
|
||||
|
||||
// 按强度插值
|
||||
if (Mathf.Approximately(strength, 1f)) return sim;
|
||||
return Lerp(identity, sim, strength);
|
||||
}
|
||||
|
||||
private static Matrix4x4 Lerp(Matrix4x4 a, Matrix4x4 b, float t)
|
||||
{
|
||||
var result = new Matrix4x4();
|
||||
for (int i = 0; i < 16; i++)
|
||||
result[i] = Mathf.Lerp(a[i], b[i], t);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── 预定义色盲矩阵 ───────────────────────────────────────────────────
|
||||
private static readonly Matrix4x4 Protanopia = new(
|
||||
new Vector4(0.567f, 0.433f, 0.000f, 0),
|
||||
new Vector4(0.558f, 0.442f, 0.000f, 0),
|
||||
new Vector4(0.000f, 0.242f, 0.758f, 0),
|
||||
new Vector4(0, 0, 0, 1));
|
||||
|
||||
private static readonly Matrix4x4 Deuteranopia = new(
|
||||
new Vector4(0.625f, 0.375f, 0.000f, 0),
|
||||
new Vector4(0.700f, 0.300f, 0.000f, 0),
|
||||
new Vector4(0.000f, 0.300f, 0.700f, 0),
|
||||
new Vector4(0, 0, 0, 1));
|
||||
|
||||
private static readonly Matrix4x4 Tritanopia = new(
|
||||
new Vector4(0.950f, 0.050f, 0.000f, 0),
|
||||
new Vector4(0.000f, 0.433f, 0.567f, 0),
|
||||
new Vector4(0.000f, 0.475f, 0.525f, 0),
|
||||
new Vector4(0, 0, 0, 1));
|
||||
|
||||
private static readonly Matrix4x4 Achromatopsia = new(
|
||||
new Vector4(0.299f, 0.587f, 0.114f, 0),
|
||||
new Vector4(0.299f, 0.587f, 0.114f, 0),
|
||||
new Vector4(0.299f, 0.587f, 0.114f, 0),
|
||||
new Vector4(0, 0, 0, 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fff57f1963a7e74e8000da706ddafce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/Analytics.meta
Normal file
8
Assets/Scripts/Support/Analytics.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f18a04f598295c4ca6e9c3f2ea80b58
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
134
Assets/Scripts/Support/Analytics/AnalyticsManager.cs
Normal file
134
Assets/Scripts/Support/Analytics/AnalyticsManager.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Support.Analytics
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 游戏玩法数据分析收集器(架构 16_SupportingModules §7)。
|
||||
/// 在本地记录结构化事件日志,供后续分析或上传。
|
||||
/// 注:不收集任何个人身份信息(PII)。
|
||||
/// </summary>
|
||||
public class AnalyticsManager : MonoBehaviour, IAnalyticsService
|
||||
{
|
||||
[Header("配置")]
|
||||
[Tooltip("是否启用分析收集")]
|
||||
[SerializeField] private bool _enabled = true;
|
||||
[Tooltip("超过此条数时自动将缓存刷写到磁盘")]
|
||||
[SerializeField] private int _flushThreshold = 50;
|
||||
|
||||
private readonly List<AnalyticsEvent> _buffer = new();
|
||||
private string _logPath;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
#if !UNITY_EDITOR && !DEVELOPMENT_BUILD
|
||||
if (!_enabled) { enabled = false; return; }
|
||||
#endif
|
||||
if (ServiceLocator.GetOrDefault<IAnalyticsService>() != null) { Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<IAnalyticsService>(this);
|
||||
_logPath = Path.Combine(Application.persistentDataPath, "analytics.json");
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Flush();
|
||||
ServiceLocator.Unregister<IAnalyticsService>(this);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit() => Flush();
|
||||
|
||||
// ── 实例方法(IAnalyticsService 实现)────────────────────────────────────
|
||||
/// <summary>记录一个自定义分析事件。</summary>
|
||||
public void Track(string eventName, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
if (!_enabled) return;
|
||||
Enqueue(eventName, parameters);
|
||||
}
|
||||
|
||||
// ── 预定义事件 ────────────────────────────────────────────────────────────
|
||||
public void TrackBossKill(string bossId, float duration, int deathCount)
|
||||
=> Track("boss_kill", new Dictionary<string, object>
|
||||
{
|
||||
{ "boss_id", bossId },
|
||||
{ "duration", duration },
|
||||
{ "death_count", deathCount },
|
||||
});
|
||||
|
||||
public void TrackDeath(string cause, string sceneId, Vector2 position)
|
||||
=> Track("player_death", new Dictionary<string, object>
|
||||
{
|
||||
{ "cause", cause },
|
||||
{ "scene", sceneId },
|
||||
{ "pos_x", Mathf.RoundToInt(position.x) },
|
||||
{ "pos_y", Mathf.RoundToInt(position.y) },
|
||||
});
|
||||
|
||||
public void TrackAbilityUnlock(string abilityId)
|
||||
=> Track("ability_unlock", new Dictionary<string, object>
|
||||
{
|
||||
{ "ability", abilityId }
|
||||
});
|
||||
|
||||
public void TrackSessionStart(int slotIndex)
|
||||
=> Track("session_start", new Dictionary<string, object>
|
||||
{
|
||||
{ "slot", slotIndex },
|
||||
});
|
||||
|
||||
public void TrackSessionEnd(float playtimeSeconds)
|
||||
=> Track("session_end", new Dictionary<string, object>
|
||||
{
|
||||
{ "playtime", playtimeSeconds },
|
||||
});
|
||||
|
||||
// ── 内部实现 ──────────────────────────────────────────────────────────────
|
||||
private void Enqueue(string name, Dictionary<string, object> parms)
|
||||
{
|
||||
_buffer.Add(new AnalyticsEvent
|
||||
{
|
||||
EventName = name,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Parameters = parms ?? new Dictionary<string, object>(),
|
||||
});
|
||||
|
||||
if (_buffer.Count >= _flushThreshold)
|
||||
Flush();
|
||||
}
|
||||
|
||||
private void Flush()
|
||||
{
|
||||
if (_buffer.Count == 0) return;
|
||||
try
|
||||
{
|
||||
var existing = new List<AnalyticsEvent>();
|
||||
if (File.Exists(_logPath))
|
||||
{
|
||||
string json = File.ReadAllText(_logPath);
|
||||
var loaded = JsonConvert.DeserializeObject<List<AnalyticsEvent>>(json);
|
||||
if (loaded != null) existing.AddRange(loaded);
|
||||
}
|
||||
existing.AddRange(_buffer);
|
||||
File.WriteAllText(_logPath, JsonConvert.SerializeObject(existing, Formatting.Indented));
|
||||
_buffer.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[Analytics] 写入失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class AnalyticsEvent
|
||||
{
|
||||
public string EventName;
|
||||
public long Timestamp;
|
||||
public Dictionary<string, object> Parameters;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace BaseGames.Support.Analytics
|
||||
11
Assets/Scripts/Support/Analytics/AnalyticsManager.cs.meta
Normal file
11
Assets/Scripts/Support/Analytics/AnalyticsManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89c0c8e7bdf40b6489a4967bd85852c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
Assets/Scripts/Support/Analytics/IAnalyticsService.cs
Normal file
21
Assets/Scripts/Support/Analytics/IAnalyticsService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Support.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// 分析收集服务接口。通过 ServiceLocator 注册,供各系统上报事件使用。
|
||||
/// 注:不收集任何个人身份信息(PII)。
|
||||
/// </summary>
|
||||
public interface IAnalyticsService
|
||||
{
|
||||
/// <summary>记录一个自定义分析事件。</summary>
|
||||
void Track(string eventName, Dictionary<string, object> parameters = null);
|
||||
|
||||
void TrackBossKill(string bossId, float duration, int deathCount);
|
||||
void TrackDeath(string cause, string sceneId, Vector2 position);
|
||||
void TrackAbilityUnlock(string abilityId);
|
||||
void TrackSessionStart(int slotIndex);
|
||||
void TrackSessionEnd(float playtimeSeconds);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/Analytics/IAnalyticsService.cs.meta
Normal file
11
Assets/Scripts/Support/Analytics/IAnalyticsService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c32b212b5daca7b4198e1d37aeb891a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/AntiSoftlock.meta
Normal file
8
Assets/Scripts/Support/AntiSoftlock.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44f3dd440d1cbc4428c5d80f3bf40263
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
140
Assets/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs
Normal file
140
Assets/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
using BaseGames.Progression;
|
||||
|
||||
namespace BaseGames.Support.AntiSoftlock
|
||||
{
|
||||
/// <summary>
|
||||
/// 防软锁系统(架构 16_SupportingModules §5)。
|
||||
/// 检测玩家长时间静止且无法移动的情况,提供逃脱选项。
|
||||
/// </summary>
|
||||
public class AntiSoftlockSystem : MonoBehaviour
|
||||
{
|
||||
[Header("配置")]
|
||||
[Tooltip("玩家静止超过此秒数后显示逃脱提示")]
|
||||
[SerializeField] private float _stuckThresholdSeconds = 30f;
|
||||
[Tooltip("移动速度低于此值视为静止")]
|
||||
[SerializeField] private float _minVelocityThreshold = 0.1f;
|
||||
|
||||
[Header("逃脱路径列表(按优先级降序排列)")]
|
||||
[SerializeField] private RoomEscapeInfoSO[] _escapeOptions;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
|
||||
[SerializeField] private VoidEventChannelSO _onShowEscapeUI;
|
||||
[SerializeField] private VoidEventChannelSO _onHideEscapeUI;
|
||||
/// <summary>
|
||||
/// 玩家生成事件频道(PlayerController.Start() 广播)。
|
||||
/// 替代 FindFirstObjectByType 的全场景扫描。
|
||||
/// </summary>
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
[Header("引用")]
|
||||
[SerializeField] private InputReaderSO _input;
|
||||
|
||||
private PlayerController _player;
|
||||
private Rigidbody2D _playerRb;
|
||||
private float _stuckTimer;
|
||||
private bool _escapeUIVisible;
|
||||
private Vector2 _lastPos;
|
||||
|
||||
private readonly BaseGames.Core.Events.CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onPlayerSpawned != null)
|
||||
_onPlayerSpawned.Subscribe(OnPlayerSpawned).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void OnPlayerSpawned(Transform playerTransform)
|
||||
{
|
||||
if (playerTransform == null) return;
|
||||
_player = playerTransform.GetComponent<PlayerController>();
|
||||
_playerRb = playerTransform.GetComponent<Rigidbody2D>();
|
||||
_lastPos = playerTransform.position;
|
||||
_stuckTimer = 0f;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_player == null || !_player.Stats.IsAlive) return;
|
||||
|
||||
Vector2 pos = _player.transform.position;
|
||||
float vel = _playerRb != null ? _playerRb.linearVelocity.magnitude : Vector2.Distance(pos, _lastPos) / Time.deltaTime;
|
||||
|
||||
if (vel > _minVelocityThreshold)
|
||||
{
|
||||
_stuckTimer = 0f;
|
||||
_lastPos = pos;
|
||||
if (_escapeUIVisible) HideEscapeUI();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPos = pos;
|
||||
_stuckTimer += Time.deltaTime;
|
||||
|
||||
if (_stuckTimer >= _stuckThresholdSeconds && !_escapeUIVisible)
|
||||
ShowEscapeUI();
|
||||
}
|
||||
|
||||
private void ShowEscapeUI()
|
||||
{
|
||||
_escapeUIVisible = true;
|
||||
_onShowEscapeUI?.Raise();
|
||||
}
|
||||
|
||||
private void HideEscapeUI()
|
||||
{
|
||||
_escapeUIVisible = false;
|
||||
_onHideEscapeUI?.Raise();
|
||||
}
|
||||
|
||||
/// <summary>执行最优先的逃脱路径(由 UI 按钮调用)。</summary>
|
||||
public void ExecuteEscape()
|
||||
{
|
||||
var escape = ChooseBestEscape();
|
||||
HideEscapeUI();
|
||||
|
||||
var sm = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
string scene = string.IsNullOrEmpty(escape?.targetScene)
|
||||
? sm?.LastCheckpointScene
|
||||
: escape.targetScene;
|
||||
string spawn = string.IsNullOrEmpty(escape?.spawnId)
|
||||
? sm?.LastCheckpointSpawnId
|
||||
: escape.spawnId;
|
||||
|
||||
_onSceneLoadRequest?.Raise(new SceneLoadRequest
|
||||
{
|
||||
SceneName = scene,
|
||||
EntryId = spawn,
|
||||
ShowLoadingScreen = true,
|
||||
IsRespawn = true,
|
||||
});
|
||||
}
|
||||
|
||||
private RoomEscapeInfoSO ChooseBestEscape()
|
||||
{
|
||||
if (_escapeOptions == null || _escapeOptions.Length == 0) return null;
|
||||
RoomEscapeInfoSO best = null;
|
||||
int bestPriority = int.MinValue;
|
||||
foreach (var opt in _escapeOptions)
|
||||
{
|
||||
if (opt == null) continue;
|
||||
if (opt.requiredAbility != BaseGames.Player.AbilityType.None
|
||||
&& (_player?.Stats.HasAbility(opt.requiredAbility) == false)) continue;
|
||||
if (opt.priority > bestPriority)
|
||||
{
|
||||
bestPriority = opt.priority;
|
||||
best = opt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00954cb28f3eb9e4aaf388feb35565dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
36
Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs
Normal file
36
Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.World;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 硬性能力门禁(架构 16_SupportingModules §5.3)。
|
||||
/// 在 AbilityGate 的基础上增加次级验证:检查玩家是否真正拾取了物理道具。
|
||||
/// 当 _requirePhysicalValidation = true 时,还需要通过 _physicalPickupSwitchKey 确认。
|
||||
/// </summary>
|
||||
public class HardAbilityGate : AbilityGate
|
||||
{
|
||||
[Header("HardAbilityGate 设置")]
|
||||
[Tooltip("是否还要求物理拾取验证(需要 World.Switches 中对应 Key = true)")]
|
||||
[SerializeField] private bool _requirePhysicalValidation = false;
|
||||
[Tooltip("物理拾取验证 Switch Key(需 SaveManager.Data.World.Switches[key] == true)")]
|
||||
[SerializeField] private string _physicalPickupSwitchKey;
|
||||
|
||||
[Header("引用")]
|
||||
[SerializeField] private SaveManager _saveManager;
|
||||
|
||||
protected override bool EvaluateAccess()
|
||||
{
|
||||
if (!base.EvaluateAccess()) return false;
|
||||
if (!_requirePhysicalValidation) return true;
|
||||
|
||||
// 次级检查:物理拾取确认
|
||||
if (string.IsNullOrEmpty(_physicalPickupSwitchKey)) return true;
|
||||
var save = _saveManager != null ? _saveManager.Data : null;
|
||||
if (save?.World?.Switches == null) return false;
|
||||
return save.World.Switches.TryGetValue(_physicalPickupSwitchKey, out bool val) && val;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs.meta
Normal file
11
Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7ae44a614ccc104aa8ad89cf2eebf54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs
Normal file
25
Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 房间逃脱信息 SO(架构 16_SupportingModules §5.2)。
|
||||
/// 当 AntiSoftlockSystem 检测到玩家卡关时,提供重置目标场景和生成点。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "AntiSoftlock/RoomEscapeInfo", fileName = "ESC_")]
|
||||
public class RoomEscapeInfoSO : ScriptableObject
|
||||
{
|
||||
[Tooltip("逃脱目标场景名称(留空 = 最近检查点)")]
|
||||
public string targetScene;
|
||||
|
||||
[Tooltip("目标场景中的生成点 ID(留空 = 默认生成点)")]
|
||||
public string spawnId;
|
||||
|
||||
[Tooltip("需要玩家拥有此能力才触发此逃脱路径(None = 无要求)")]
|
||||
public AbilityType requiredAbility = AbilityType.None;
|
||||
|
||||
[Tooltip("此逃脱路径的优先级(越高越优先选择)")]
|
||||
public int priority = 0;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs.meta
Normal file
11
Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89e0ac3b73daf45479939b0d29070cf0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/Scripts/Support/BaseGames.Support.asmdef
Normal file
34
Assets/Scripts/Support/BaseGames.Support.asmdef
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "BaseGames.Support",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Player.States",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.World",
|
||||
"BaseGames.Progression",
|
||||
"BaseGames.Platform",
|
||||
"Unity.TextMeshPro",
|
||||
"Unity.RenderPipelines.Universal.Runtime"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.unity.nuget.newtonsoft-json",
|
||||
"expression": "",
|
||||
"define": "NEWTONSOFT_JSON"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Scripts/Support/BaseGames.Support.asmdef.meta
Normal file
7
Assets/Scripts/Support/BaseGames.Support.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cf751d9b386aae42ac8948c0e9a91be
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/Debug.meta
Normal file
8
Assets/Scripts/Support/Debug.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0034d19093a1314586b9bd7d497a085
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
155
Assets/Scripts/Support/Debug/DebugCheatSystem.cs
Normal file
155
Assets/Scripts/Support/Debug/DebugCheatSystem.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// 调试作弊控制台(仅 UNITY_EDITOR 或 DEVELOPMENT_BUILD 下编译)。
|
||||
/// 按反引号键(`)呼出/隐藏输入框,输入指令后按 Enter 执行。
|
||||
/// 指令格式:指令名 [参数1] [参数2] ...
|
||||
/// </summary>
|
||||
public class DebugCheatSystem : MonoBehaviour
|
||||
{
|
||||
[Header("UI 引用")]
|
||||
[SerializeField] private GameObject _consoleRoot;
|
||||
[SerializeField] private TMP_InputField _inputField;
|
||||
[SerializeField] private TMP_Text _outputText;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerRevive;
|
||||
|
||||
private bool _visible;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (UnityEngine.Input.GetKeyDown(KeyCode.BackQuote))
|
||||
Toggle();
|
||||
|
||||
if (_visible && _inputField != null && UnityEngine.Input.GetKeyDown(KeyCode.Return))
|
||||
ExecuteCommand(_inputField.text);
|
||||
}
|
||||
|
||||
private void Toggle()
|
||||
{
|
||||
_visible = !_visible;
|
||||
if (_consoleRoot != null) _consoleRoot.SetActive(_visible);
|
||||
if (_visible && _inputField != null)
|
||||
{
|
||||
_inputField.text = string.Empty;
|
||||
_inputField.Select();
|
||||
_inputField.ActivateInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCommand(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||
_inputField.text = string.Empty;
|
||||
_inputField.ActivateInputField();
|
||||
|
||||
string[] parts = raw.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0) return;
|
||||
|
||||
string cmd = parts[0].ToLowerInvariant();
|
||||
string result;
|
||||
|
||||
try
|
||||
{
|
||||
result = cmd switch
|
||||
{
|
||||
"help" => "指令: heal | addgeo [n] | godmode | ungodmode | unlock [ability] | killall | scene [name] | revive",
|
||||
"heal" => CmdHeal(),
|
||||
"addgeo" => CmdAddGeo(parts),
|
||||
"godmode" => CmdGodMode(true),
|
||||
"ungodmode" => CmdGodMode(false),
|
||||
"unlock" => CmdUnlock(parts),
|
||||
"killall" => CmdKillAll(),
|
||||
"scene" => CmdLoadScene(parts),
|
||||
"revive" => CmdRevive(),
|
||||
_ => $"未知指令: {cmd}",
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = $"[错误] {ex.Message}";
|
||||
}
|
||||
|
||||
Log(result);
|
||||
}
|
||||
|
||||
// ── 指令实现 ──────────────────────────────────────────────────────────────
|
||||
private string CmdHeal()
|
||||
{
|
||||
var player = FindFirstObjectByType<PlayerController>();
|
||||
if (player == null) return "找不到 PlayerController";
|
||||
player.Stats.FullHeal();
|
||||
return "HP 已满";
|
||||
}
|
||||
|
||||
private string CmdAddGeo(string[] parts)
|
||||
{
|
||||
int amount = parts.Length > 1 && int.TryParse(parts[1], out int v) ? v : 100;
|
||||
var player = FindFirstObjectByType<PlayerController>();
|
||||
if (player == null) return "找不到 PlayerController";
|
||||
player.Stats.AddGeo(amount);
|
||||
return $"增加 Geo: {amount}";
|
||||
}
|
||||
|
||||
private string CmdGodMode(bool on)
|
||||
{
|
||||
var player = FindFirstObjectByType<PlayerController>();
|
||||
if (player == null) return "找不到 PlayerController";
|
||||
player.Stats.SetGodMode(on);
|
||||
return on ? "无敌模式 ON" : "无敌模式 OFF";
|
||||
}
|
||||
|
||||
private string CmdUnlock(string[] parts)
|
||||
{
|
||||
if (parts.Length < 2) return "用法: unlock [AbilityType]";
|
||||
if (!Enum.TryParse<AbilityType>(parts[1], true, out var abilityType))
|
||||
return $"未知能力: {parts[1]}(可用:{string.Join(", ", Enum.GetNames(typeof(AbilityType)))})";
|
||||
var player = FindFirstObjectByType<PlayerController>();
|
||||
if (player == null) return "找不到 PlayerController";
|
||||
player.Stats.UnlockAbility(abilityType);
|
||||
return $"解锁能力: {abilityType}";
|
||||
}
|
||||
|
||||
private string CmdKillAll()
|
||||
{
|
||||
var enemies = FindObjectsByType<EnemyBase>(FindObjectsSortMode.None);
|
||||
var dmg = new DamageInfo.Builder().SetRaw(999999).Build();
|
||||
foreach (var e in enemies) e.TakeDamage(dmg);
|
||||
return $"已击杀 {enemies.Length} 个敌人";
|
||||
}
|
||||
|
||||
private string CmdLoadScene(string[] parts)
|
||||
{
|
||||
if (parts.Length < 2) return "用法: scene [SceneName]";
|
||||
SceneManager.LoadScene(parts[1]);
|
||||
return $"加载场景: {parts[1]}";
|
||||
}
|
||||
|
||||
private string CmdRevive()
|
||||
{
|
||||
_onPlayerRevive?.Raise();
|
||||
return "已触发玩家复活事件";
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
Debug.Log($"[Cheat] {msg}");
|
||||
if (_outputText != null)
|
||||
{
|
||||
_outputText.text = $"> {msg}";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Support/Debug/DebugCheatSystem.cs.meta
Normal file
11
Assets/Scripts/Support/Debug/DebugCheatSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7213e4687db174a4181966eeaf9d8a81
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/Localization.meta
Normal file
8
Assets/Scripts/Support/Localization.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acf94e71d5d85064ba0a9bbbf4536b01
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
51
Assets/Scripts/Support/Localization/LanguageManagerSO.cs
Normal file
51
Assets/Scripts/Support/Localization/LanguageManagerSO.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言设置持久化 SO(架构 16_SupportingModules §4.1)。
|
||||
/// 支持的语言列表由设计师填写;当前语言持久化到 PlayerPrefs。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Settings/LanguageManager", fileName = "SET_LanguageManager")]
|
||||
public class LanguageManagerSO : ScriptableObject
|
||||
{
|
||||
private const string PrefsKey = "game_language";
|
||||
|
||||
[Tooltip("游戏支持的语言 locale code 列表(如 zh-CN、en-US)")]
|
||||
public string[] supportedLocales = { "zh-CN", "en-US" };
|
||||
|
||||
[Tooltip("默认语言 locale code")]
|
||||
public string defaultLocale = "zh-CN";
|
||||
|
||||
public string CurrentLocale { get; private set; }
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
CurrentLocale = PlayerPrefs.GetString(PrefsKey, defaultLocale);
|
||||
}
|
||||
|
||||
/// <summary>设置当前语言并持久化到 PlayerPrefs,同时通知 Unity Localization(若已安装)。</summary>
|
||||
public void SetLocale(string localeCode)
|
||||
{
|
||||
CurrentLocale = localeCode;
|
||||
PlayerPrefs.SetString(PrefsKey, localeCode);
|
||||
PlayerPrefs.Save();
|
||||
ApplyLocale(localeCode);
|
||||
}
|
||||
|
||||
/// <summary>初始化时应用已保存语言。</summary>
|
||||
public void ApplySaved() => ApplyLocale(CurrentLocale);
|
||||
|
||||
private static void ApplyLocale(string localeCode)
|
||||
{
|
||||
#if UNITY_LOCALIZATION
|
||||
var locales = UnityEngine.Localization.Settings.LocalizationSettings.AvailableLocales;
|
||||
var locale = locales?.GetLocale(new UnityEngine.Localization.LocaleIdentifier(localeCode));
|
||||
if (locale != null)
|
||||
UnityEngine.Localization.Settings.LocalizationSettings.SelectedLocale = locale;
|
||||
#else
|
||||
Debug.Log($"[LanguageManager] 语言设为 {localeCode}(Unity Localization 包未安装,仅记录 PlayerPrefs)。");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0305b3bda1379324883e51f0fb0d5cb4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
59
Assets/Scripts/Support/Localization/LocalizationManager.cs
Normal file
59
Assets/Scripts/Support/Localization/LocalizationManager.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化字符串访问工具类(架构 16_SupportingModules §4)。
|
||||
/// 当 Unity Localization 包(com.unity.localization)安装后使用 LocalizationSettings 实现完整功能;
|
||||
/// 未安装时退回到 Key 直传(返回 entryKey 本身)。
|
||||
/// </summary>
|
||||
public static class LocalizationManager
|
||||
{
|
||||
#if UNITY_LOCALIZATION
|
||||
private const string DefaultTable = "UI";
|
||||
|
||||
/// <summary>从指定表中获取本地化字符串。</summary>
|
||||
public static string Get(string entryKey, string tableName = DefaultTable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var op = UnityEngine.Localization.Settings.LocalizationSettings
|
||||
.StringDatabase.GetLocalizedStringAsync(tableName, entryKey);
|
||||
return op.IsDone ? op.Result : entryKey;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[Localization] Key '{entryKey}' in table '{tableName}' 读取失败: {e.Message}");
|
||||
return entryKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>带格式化参数的本地化字符串。</summary>
|
||||
public static string GetFormat(string entryKey, string tableName, params object[] args)
|
||||
{
|
||||
string template = Get(entryKey, tableName);
|
||||
try { return string.Format(template, args); }
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[Localization] GetFormat '{entryKey}' 格式化失败: {e.Message}");
|
||||
return template;
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// <summary>Unity Localization 包未安装:直接返回 Key。</summary>
|
||||
public static string Get(string entryKey, string tableName = "UI")
|
||||
=> entryKey;
|
||||
|
||||
public static string GetFormat(string entryKey, string tableName, params object[] args)
|
||||
{
|
||||
try { return string.Format(entryKey, args); }
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[Localization] GetFormat '{entryKey}' 格式化失败: {e.Message}");
|
||||
return entryKey;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d9a88adb6ede743a783e306209d4e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/Platform.meta
Normal file
8
Assets/Scripts/Support/Platform.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70825e06cc8c90543a0dbe0ee05ac579
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
17
Assets/Scripts/Support/Platform/BaseGames.Platform.asmdef
Normal file
17
Assets/Scripts/Support/Platform/BaseGames.Platform.asmdef
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "BaseGames.Platform",
|
||||
"rootNamespace": "BaseGames.Platform",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b86aa8ca428d1f4e945cb6c1a9a60e5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
54
Assets/Scripts/Support/Platform/IPlatformService.cs
Normal file
54
Assets/Scripts/Support/Platform/IPlatformService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BaseGames.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// 平台服务抽象接口(架构 16_SupportingModules §3)。
|
||||
/// 解耦游戏逻辑与平台 SDK(Steam / Console / Mobile)。
|
||||
/// </summary>
|
||||
public interface IPlatformService
|
||||
{
|
||||
// ── 初始化 / 生命周期 ──────────────────────────────────────────────────
|
||||
bool IsInitialized { get; }
|
||||
Task<bool> InitializeAsync();
|
||||
/// <summary>每帧调用(由 PlatformBootstrap.Update 驱动),处理 SDK 回调。</summary>
|
||||
void RunCallbacks();
|
||||
void Shutdown();
|
||||
|
||||
// ── 成就 ──────────────────────────────────────────────────────────────
|
||||
void UnlockAchievement(string achievementId);
|
||||
void ClearAchievement(string achievementId);
|
||||
Task<bool> IsAchievementUnlocked(string achievementId);
|
||||
|
||||
// ── 统计数据(Steam 成就进度跟踪)────────────────────────────────────
|
||||
void SetStat(string statId, int value);
|
||||
void IncrementStat(string statId, int increment = 1);
|
||||
int GetStat(string statId);
|
||||
|
||||
// ── 云存档 ────────────────────────────────────────────────────────────
|
||||
bool IsCloudAvailable { get; }
|
||||
Task<bool> CloudSaveAsync(string fileName, byte[] data);
|
||||
Task<byte[]> CloudLoadAsync(string fileName);
|
||||
|
||||
// ── Rich Presence ─────────────────────────────────────────────────────
|
||||
void SetRichPresence(string key, string value);
|
||||
void ClearRichPresence();
|
||||
|
||||
// ── 排行榜 ────────────────────────────────────────────────────────────
|
||||
void SubmitLeaderboardScore(string boardId, long score);
|
||||
Task<LeaderboardEntry[]> GetLeaderboardEntries(string boardId, int maxCount);
|
||||
|
||||
// ── DLC / 商城 ────────────────────────────────────────────────────────
|
||||
bool IsDLCOwned(string dlcId);
|
||||
|
||||
// ── 覆盖层(Steam Overlay 等)─────────────────────────────────────────
|
||||
void ShowOverlay(string dialog);
|
||||
}
|
||||
|
||||
public struct LeaderboardEntry
|
||||
{
|
||||
public string PlayerName;
|
||||
public long Score;
|
||||
public int Rank;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/Platform/IPlatformService.cs.meta
Normal file
11
Assets/Scripts/Support/Platform/IPlatformService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8e96f654546bef488e8e5e27eb280da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/Scripts/Support/Platform/NullPlatformService.cs
Normal file
46
Assets/Scripts/Support/Platform/NullPlatformService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// IPlatformService 的空实现,无平台 SDK 时作为默认服务使用。
|
||||
/// 所有操作均为无操作(no-op)或返回安全默认值。
|
||||
/// </summary>
|
||||
public class NullPlatformService : IPlatformService
|
||||
{
|
||||
public bool IsInitialized => true;
|
||||
public bool IsCloudAvailable => false;
|
||||
|
||||
public Task<bool> InitializeAsync() => Task.FromResult(true);
|
||||
public void RunCallbacks() { }
|
||||
public void Shutdown() { }
|
||||
|
||||
public void UnlockAchievement(string achievementId)
|
||||
=> Debug.Log($"[NullPlatform] UnlockAchievement: {achievementId}");
|
||||
|
||||
public void ClearAchievement(string achievementId) { }
|
||||
|
||||
public Task<bool> IsAchievementUnlocked(string achievementId)
|
||||
=> Task.FromResult(false);
|
||||
|
||||
public void SetStat(string statId, int value) { }
|
||||
public void IncrementStat(string statId, int increment = 1) { }
|
||||
public int GetStat(string statId) => 0;
|
||||
|
||||
public Task<bool> CloudSaveAsync(string fileName, byte[] data) => Task.FromResult(false);
|
||||
public Task<byte[]> CloudLoadAsync(string fileName) => Task.FromResult<byte[]>(null);
|
||||
|
||||
public void SetRichPresence(string key, string value) { }
|
||||
public void ClearRichPresence() { }
|
||||
|
||||
public void SubmitLeaderboardScore(string boardId, long score)
|
||||
=> Debug.Log($"[NullPlatform] SubmitLeaderboardScore: {boardId} = {score}");
|
||||
|
||||
public Task<LeaderboardEntry[]> GetLeaderboardEntries(string boardId, int maxCount)
|
||||
=> Task.FromResult(System.Array.Empty<LeaderboardEntry>());
|
||||
|
||||
public bool IsDLCOwned(string dlcId) => false;
|
||||
public void ShowOverlay(string dialog) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/Platform/NullPlatformService.cs.meta
Normal file
11
Assets/Scripts/Support/Platform/NullPlatformService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 452af73acae0f8d40840f96095a34b9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
Assets/Scripts/Support/Platform/PlatformBootstrap.cs
Normal file
42
Assets/Scripts/Support/Platform/PlatformBootstrap.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// 平台服务引导组件(架构 16_SupportingModules §3.1)。
|
||||
/// 在场景加载最早阶段(DefaultExecutionOrder = -200)检测平台并向 ServiceLocator 注入实现。
|
||||
/// ⚠️ 使用 PlatformBootstrap MonoBehaviour + ServiceLocator 模式,非静态 PlatformManager。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-200)]
|
||||
public class PlatformBootstrap : MonoBehaviour
|
||||
{
|
||||
private IPlatformService _platform;
|
||||
|
||||
private async void Awake()
|
||||
{
|
||||
IPlatformService service;
|
||||
|
||||
#if UNITY_STANDALONE && STEAMWORKS_NET
|
||||
service = new SteamPlatformService();
|
||||
#else
|
||||
service = new NullPlatformService();
|
||||
#endif
|
||||
|
||||
bool ok = await service.InitializeAsync();
|
||||
if (!ok)
|
||||
{
|
||||
Debug.LogWarning("[PlatformBootstrap] 平台服务初始化失败,退回到 NullPlatformService。");
|
||||
service = new NullPlatformService();
|
||||
}
|
||||
|
||||
ServiceLocator.Register<IPlatformService>(service);
|
||||
_platform = service;
|
||||
Debug.Log($"[PlatformBootstrap] 已注册 {service.GetType().Name}。");
|
||||
}
|
||||
|
||||
private void Update() => _platform?.RunCallbacks();
|
||||
|
||||
private void OnApplicationQuit() => _platform?.Shutdown();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/Platform/PlatformBootstrap.cs.meta
Normal file
11
Assets/Scripts/Support/Platform/PlatformBootstrap.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 276b59f1a8979c14cbeac97645248a57
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
149
Assets/Scripts/Support/Platform/SteamPlatformService.cs
Normal file
149
Assets/Scripts/Support/Platform/SteamPlatformService.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
#if UNITY_STANDALONE && STEAMWORKS_NET
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Steamworks;
|
||||
|
||||
namespace BaseGames.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 Steamworks.NET 的平台服务实现(仅 UNITY_STANDALONE + STEAMWORKS_NET 时编译)。
|
||||
/// </summary>
|
||||
public class SteamPlatformService : IPlatformService
|
||||
{
|
||||
public bool IsInitialized { get; private set; }
|
||||
public bool IsCloudAvailable => IsInitialized && SteamRemoteStorage.IsCloudEnabledForApp();
|
||||
|
||||
// ── 初始化 / 生命周期 ──────────────────────────────────────────────────
|
||||
public async Task<bool> InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!SteamAPI.Init())
|
||||
{
|
||||
Debug.LogWarning("[SteamPlatform] SteamAPI.Init() 失败,请确认 Steam 客户端已运行。");
|
||||
IsInitialized = false;
|
||||
return false;
|
||||
}
|
||||
IsInitialized = true;
|
||||
Debug.Log("[SteamPlatform] Steam 初始化成功。");
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SteamPlatform] 初始化异常: {ex.Message}");
|
||||
IsInitialized = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void RunCallbacks()
|
||||
{
|
||||
if (IsInitialized) SteamAPI.RunCallbacks();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
if (IsInitialized) SteamAPI.Shutdown();
|
||||
}
|
||||
|
||||
// ── 成就 ──────────────────────────────────────────────────────────────
|
||||
public void UnlockAchievement(string achievementId)
|
||||
{
|
||||
if (!IsInitialized) return;
|
||||
SteamUserStats.SetAchievement(achievementId);
|
||||
SteamUserStats.StoreStats();
|
||||
}
|
||||
|
||||
public void ClearAchievement(string achievementId)
|
||||
{
|
||||
if (!IsInitialized) return;
|
||||
SteamUserStats.ClearAchievement(achievementId);
|
||||
SteamUserStats.StoreStats();
|
||||
}
|
||||
|
||||
public async Task<bool> IsAchievementUnlocked(string achievementId)
|
||||
{
|
||||
if (!IsInitialized) return false;
|
||||
SteamUserStats.GetAchievement(achievementId, out bool achieved);
|
||||
return achieved;
|
||||
}
|
||||
|
||||
// ── 统计数据 ──────────────────────────────────────────────────────────
|
||||
public void SetStat(string statId, int value)
|
||||
{
|
||||
if (!IsInitialized) return;
|
||||
SteamUserStats.SetStat(statId, value);
|
||||
SteamUserStats.StoreStats();
|
||||
}
|
||||
|
||||
public void IncrementStat(string statId, int increment = 1)
|
||||
{
|
||||
if (!IsInitialized) return;
|
||||
SteamUserStats.GetStat(statId, out int current);
|
||||
SteamUserStats.SetStat(statId, current + increment);
|
||||
SteamUserStats.StoreStats();
|
||||
}
|
||||
|
||||
public int GetStat(string statId)
|
||||
{
|
||||
if (!IsInitialized) return 0;
|
||||
SteamUserStats.GetStat(statId, out int value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── 云存档 ────────────────────────────────────────────────────────────
|
||||
public async Task<bool> CloudSaveAsync(string fileName, byte[] data)
|
||||
{
|
||||
if (!IsCloudAvailable) return false;
|
||||
return await Task.Run(() =>
|
||||
SteamRemoteStorage.FileWrite(fileName, data, data.Length));
|
||||
}
|
||||
|
||||
public async Task<byte[]> CloudLoadAsync(string fileName)
|
||||
{
|
||||
if (!IsCloudAvailable || !SteamRemoteStorage.FileExists(fileName))
|
||||
return null;
|
||||
int size = SteamRemoteStorage.GetFileSize(fileName);
|
||||
var buf = new byte[size];
|
||||
await Task.Run(() => SteamRemoteStorage.FileRead(fileName, buf, size));
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ── Rich Presence ─────────────────────────────────────────────────────
|
||||
public void SetRichPresence(string key, string value)
|
||||
{
|
||||
if (IsInitialized) SteamFriends.SetRichPresence(key, value);
|
||||
}
|
||||
|
||||
public void ClearRichPresence()
|
||||
{
|
||||
if (IsInitialized) SteamFriends.ClearRichPresence();
|
||||
}
|
||||
|
||||
// ── 排行榜 ────────────────────────────────────────────────────────────
|
||||
public void SubmitLeaderboardScore(string boardId, long score)
|
||||
{
|
||||
// Steamworks 排行榜提交为异步,此处为 fire-and-forget 简化实现
|
||||
Debug.Log($"[SteamPlatform] SubmitLeaderboardScore: {boardId} = {score}");
|
||||
}
|
||||
|
||||
public Task<LeaderboardEntry[]> GetLeaderboardEntries(string boardId, int maxCount)
|
||||
{
|
||||
return Task.FromResult(System.Array.Empty<LeaderboardEntry>());
|
||||
}
|
||||
|
||||
// ── DLC ───────────────────────────────────────────────────────────────
|
||||
public bool IsDLCOwned(string dlcId)
|
||||
{
|
||||
if (!IsInitialized || !uint.TryParse(dlcId, out uint appId)) return false;
|
||||
return SteamApps.BIsDlcInstalled((AppId_t)appId);
|
||||
}
|
||||
|
||||
// ── 覆盖层 ────────────────────────────────────────────────────────────
|
||||
public void ShowOverlay(string dialog)
|
||||
{
|
||||
if (IsInitialized) SteamFriends.ActivateGameOverlay(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Support/Platform/SteamPlatformService.cs.meta
Normal file
11
Assets/Scripts/Support/Platform/SteamPlatformService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdef5bc72bba5b74cb7d6498cb27395f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Support/Speedrun.meta
Normal file
8
Assets/Scripts/Support/Speedrun.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d58be49899c8d04498da53d0f12815e3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
104
Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs
Normal file
104
Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Support.Speedrun
|
||||
{
|
||||
/// <summary>
|
||||
/// 速通计时器(架构 16_SupportingModules §8)。
|
||||
/// 使用 Time.unscaledDeltaTime 以免被 HitStop(timeScale < 1)拉慢;游戏暂停时须由外部调用 PauseTimer()。
|
||||
/// 实现 ISaveable,计时持久化到 <see cref="StatsSaveData.SpeedrunTime"/>。
|
||||
/// </summary>
|
||||
public class SpeedrunTimer : MonoBehaviour, ISaveable
|
||||
{
|
||||
[Header("显示设置")]
|
||||
[SerializeField] private TMP_Text _display;
|
||||
[SerializeField] private GlobalSettingsSO _settings;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private VoidEventChannelSO _onTimerReset;
|
||||
[SerializeField] private BoolEventChannelSO _onTimerVisibilityChanged;
|
||||
|
||||
public float ElapsedSeconds { get; private set; }
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
private bool _paused;
|
||||
/// <summary>上一帧已显示的整秒值,展示内容未变化时跳过字符串重建。</summary>
|
||||
private int _lastDisplayedSecond = -1;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
bool show = _settings != null && _settings.ShowSpeedrunTimer;
|
||||
SetVisible(show);
|
||||
if (show) StartTimer();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsRunning || _paused) return;
|
||||
ElapsedSeconds += Time.unscaledDeltaTime;
|
||||
// 仅当整秒数发生变化时才重建展示字符串,避免每帧分配
|
||||
int currentSecond = (int)ElapsedSeconds;
|
||||
if (currentSecond != _lastDisplayedSecond)
|
||||
{
|
||||
_lastDisplayedSecond = currentSecond;
|
||||
UpdateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 控制接口 ──────────────────────────────────────────────────────────────
|
||||
public void StartTimer()
|
||||
{
|
||||
IsRunning = true;
|
||||
_paused = false;
|
||||
}
|
||||
|
||||
public void StopTimer() => IsRunning = false;
|
||||
|
||||
public void PauseTimer() => _paused = true;
|
||||
|
||||
public void ResumeTimer() => _paused = false;
|
||||
|
||||
public void ResetTimer()
|
||||
{
|
||||
ElapsedSeconds = 0f;
|
||||
UpdateDisplay();
|
||||
_onTimerReset?.Raise();
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
if (_display != null) _display.gameObject.SetActive(visible);
|
||||
_onTimerVisibilityChanged?.Raise(visible);
|
||||
}
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData saveData)
|
||||
{
|
||||
if (saveData?.Stats != null)
|
||||
saveData.Stats.SpeedrunTime = ElapsedSeconds;
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData saveData)
|
||||
{
|
||||
if (saveData?.Stats != null)
|
||||
ElapsedSeconds = saveData.Stats.SpeedrunTime;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────────
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
if (_display == null) return;
|
||||
int hours = (int)(ElapsedSeconds / 3600);
|
||||
int minutes = (int)(ElapsedSeconds % 3600 / 60);
|
||||
int seconds = (int)(ElapsedSeconds % 60);
|
||||
int ms = (int)(ElapsedSeconds * 100 % 100);
|
||||
_display.text = hours > 0
|
||||
? $"{hours:00}:{minutes:00}:{seconds:00}.{ms:00}"
|
||||
: $"{minutes:00}:{seconds:00}.{ms:00}";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs.meta
Normal file
11
Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2bd7f531212a924b9300bd3a8b7610a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user