(client) feat:支持定义实体的碰撞体大小和偏移;建筑支持定义实体建筑和瓦片建筑,建筑支持指定按钮回调;添加存档管理器;Dev支持设置是否暂停;实体允许定义事件组;添加基地界面 (#57)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/57
This commit is contained in:
2025-09-28 15:02:57 +08:00
parent 87a8abe86c
commit aff747be17
232 changed files with 39203 additions and 4161 deletions

View File

@@ -132,12 +132,7 @@ namespace Base
{
ApplyBufferedChanges();
}
private void OnDestroy()
{
// 在对象销毁时,取消订阅场景加载事件,避免潜在的内存泄漏。
SceneManager.sceneLoaded -= OnSceneLoaded;
}
/// <summary>
/// 单例的初始化方法在Clock实例的生命周期开始时调用Unity的Awake后
@@ -145,48 +140,14 @@ namespace Base
/// </summary>
protected override void OnStart()
{
SceneManager.sceneLoaded += OnSceneLoaded;
Init(); // 初始化时清空所有列表并重新填充
}
/// <summary>
/// 场景加载完成时回调用于重置所有Tick列表以适应新场景中的对象。
/// </summary>
/// <param name="scene">已加载的场景。</param>
/// <param name="mode">场景加载模式。</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Init(); // 场景加载时重置
}
/// <summary>
/// 初始化或重置计时器系统。清空所有注册列表和缓冲列表并重新扫描场景中的所有MonoBehaviour以注册相应的Tick接口。
/// </summary>
public void Init()
{
// 清空所有主列表
_ticks.Clear();
_tickPhysics.Clear();
_tickUIs.Clear();
// 清空所有缓冲区列表
_ticksToAdd.Clear();
_tickPhysicsToAdd.Clear();
_tickUIsToAdd.Clear();
_ticksToRemove.Clear();
_tickPhysicsToRemove.Clear();
_tickUIsToRemove.Clear();
// 扫描场景中所有MonoBehaviour并注册它们实现的Tick接口。
// 使用HashSet会自动处理重复添加确保列表唯一性。
foreach (var obj in FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None))
{
if (obj is ITick tickObj) _ticks.Add(tickObj);
if (obj is ITickPhysics physicsObj) _tickPhysics.Add(physicsObj);
if (obj is ITickUI uiObj) _tickUIs.Add(uiObj);
}
}
/// <summary>
/// 将一个ITick对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主Tick列表。

View File

@@ -172,7 +172,7 @@ namespace Base
private void Start()
{
// 如果 Program.Instance.needLoad 为 false表示游戏已加载或不需要重新加载直接隐藏加载UI
if (!Program.Instance.needLoad)
if (!Program.Instance.NeedLoad)
{
loadingUI.SetActive(false);
return;
@@ -190,7 +190,7 @@ namespace Base
fadeDuration = Base.Setting.Instance.CurrentSettings.exitAnimationDuration;
#endif
Load(); // 启动加载流程内部会调用LoadAllManagers
Program.Instance.needLoad = false; // 加载完成后重置加载标志
Program.Instance.NeedLoad = false; // 加载完成后重置加载标志
}
/// <summary>

View File

@@ -0,0 +1,16 @@
using UnityEngine;
namespace Base
{
[ExecuteInEditMode]
public class Pixelate : MonoBehaviour
{
public Material effectMaterial;
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination, effectMaterial);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a4c44ec7a45483cb1863f25eeac6228
timeCreated: 1758356791

View File

@@ -1,10 +1,9 @@
// Setting.cs
using Newtonsoft.Json;
using UnityEngine;
namespace Base
{
public class Setting : Utils.Singleton<Setting>
{
// 游戏设置数据类(用于序列化)
@@ -13,7 +12,7 @@ namespace Base
{
public float progressStepDuration = 0.5f;
public float exitAnimationDuration = 1f;
public bool developerMode = false;
public bool developerMode = false; // 默认值仍为 false在编辑器中会被覆盖
public bool friendlyFire = false;
public bool showMiniMap = true;
public float globalVolume = 1.0f;
@@ -58,6 +57,7 @@ namespace Base
PlayerPrefs.SetString("GameSettings", json);
PlayerPrefs.Save();
}
public void LoadSettings()
{
if (PlayerPrefs.HasKey("GameSettings"))
@@ -66,6 +66,10 @@ namespace Base
CurrentSettings = JsonConvert.DeserializeObject<GameSettings>(json);
}
#if UNITY_EDITOR
CurrentSettings.developerMode = true;
#endif
// 应用加载的设置
ApplyAudioSettings();
ApplyWindowSettings();
@@ -76,6 +80,7 @@ namespace Base
ApplyAudioSettings();
ApplyWindowSettings();
}
// 应用音频设置
private void ApplyAudioSettings()
{
@@ -99,4 +104,4 @@ namespace Base
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System; // Added for Action
using System;
using System.Collections.Generic;
using System.Linq;
using UI;
@@ -18,14 +18,17 @@ namespace Base
// 缓存当前可见的窗口
private readonly List<UIBase> _visibleWindows = new List<UIBase>();
// 标记是否需要更新可见窗口缓存和暂停状态
private bool needUpdate = false;
/// <summary>
/// 当UI窗口的可见性状态发生改变时触发的事件。
/// 参数1: 发生改变的UIBase实例。
/// 参数2: 窗口的新可见状态 (true为显示false为隐藏)。
/// <list type="bullet">
/// <item>参数1: 发生改变的UIBase实例。</item>
/// <item>参数2: 窗口的新可见状态 (true为显示false为隐藏)。</item>
/// </list>
/// </summary>
public event Action<UIBase, bool> OnWindowVisibilityChanged; // <--- 新增
public event Action<UIBase, bool> OnWindowVisibilityChanged;
/// <summary>
/// 获取所有已注册的UI窗口的总数量。
@@ -44,7 +47,6 @@ namespace Base
/// <returns>如果窗口可见则返回true否则返回false。</returns>
public bool IsWindowVisible(string uiName)
{
// 使用 Any() 方法检查 _visibleWindows 列表中是否存在名称匹配且可见的窗口
return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible);
}
@@ -55,7 +57,6 @@ namespace Base
/// <returns>如果窗口可见且占用了输入则返回true否则返回false。</returns>
public bool IsWindowInputOccupied(string uiName)
{
// 检查 _visibleWindows 列表中是否存在名称匹配且同时占用输入的窗口
return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible && window.isInputOccupied);
}
@@ -66,7 +67,6 @@ namespace Base
/// <returns>匹配的UIBase实例如果未找到则返回null。</returns>
public UIBase GetWindow(string uiName)
{
// 从 _allWindows 列表中查找第一个名称匹配的窗口
return _allWindows.FirstOrDefault(window => window != null && window.name == uiName);
}
@@ -77,30 +77,25 @@ namespace Base
{
_allWindows.Clear();
// 获取当前活动场景中的所有 GameObject
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
return;
}
// 遍历场景中的所有根对象,并查找其子对象中的 UIBase
foreach (var rootGameObject in activeScene.GetRootGameObjects())
{
var windows = rootGameObject.GetComponentsInChildren<UIBase>(true);
_allWindows.AddRange(windows);
}
// 去重(如果有重复的窗口)
_allWindows = _allWindows.Distinct().ToList();
// 初始化所有窗口为隐藏状态
foreach (var window in _allWindows)
{
// 确保窗口不为空且其GameObject未被销毁
if (window != null && window.gameObject != null)
{
window.Hide(); // 隐藏操作会触发 OnWindowVisibilityChanged 事件
window.Hide();
}
}
@@ -115,28 +110,23 @@ namespace Base
// 使用这个是为了让输入独占窗口关闭自己后不会立即激活其他窗口的按键,延迟一帧
if (needUpdate)
{
// 更新可见窗口缓存和暂停状态
UpdateVisibleWindowsCache();
UpdatePauseState();
needUpdate = false;
return;
}
// 如果有任何可见窗口占用了输入,则阻止其他窗口通过按键进行操作
if(_visibleWindows.Any(window => window && window.isInputOccupied))
if (_visibleWindows.Any(window => window && window.isInputOccupied))
return;
foreach (var window in _allWindows)
{
// 确保窗口不为空且其GameObject未被销毁
if (!window || !window.gameObject) continue;
// 检查窗口是否设置了有效的激活按键,并且该按键在本帧被按下
if (window.actionButton == KeyCode.None || !Input.GetKeyDown(window.actionButton)) continue;
if (window.IsVisible)
{
// 如果窗口当前是可见的,且未占用输入,则通过按键隐藏它
if (!window.isInputOccupied)
{
Hide(window);
@@ -144,7 +134,6 @@ namespace Base
}
else
{
// 如果窗口当前是隐藏的,则显示它
Show(window);
}
}
@@ -156,17 +145,13 @@ namespace Base
/// <param name="windowToShow">要显示的窗口。</param>
public void Show(UIBase windowToShow)
{
// 确保窗口不为空且未被销毁,并且当前不可见
if (!windowToShow || !windowToShow.gameObject || windowToShow.IsVisible) return;
// 如果窗口是独占的,隐藏所有其他窗口
if (windowToShow.exclusive)
{
// 创建一个副本进行迭代,防止在 Hide() 调用中修改 _visibleWindows 导致迭代器失效
var windowsToHide = _visibleWindows.ToList();
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
// 确保窗口不是要显示的窗口本身,且仍然可见
if (visibleWindow && visibleWindow != windowToShow && visibleWindow.IsVisible)
{
Hide(visibleWindow);
@@ -174,14 +159,9 @@ namespace Base
}
}
// 显示目标窗口并更新缓存与暂停状态
windowToShow.Show();
var itick = windowToShow as ITickUI;
if (itick != null)
Base.Clock.AddTickUI(itick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToShow, true); // <--- 修改点 2
OnWindowVisibilityChanged?.Invoke(windowToShow, true);
needUpdate = true;
}
@@ -192,9 +172,8 @@ namespace Base
/// <param name="uiName">要显示的UI窗口名称。</param>
public void Show(string uiName)
{
// 使用 GetWindow 方法获取窗口实例
var window = GetWindow(uiName);
if (window != null)
if (window)
{
Show(window);
return;
@@ -202,26 +181,19 @@ namespace Base
Debug.LogWarning($"[UIInputControl] 未找到名称为 '{uiName}' 的窗口来显示。");
}
/// <summary>
/// 公开的隐藏窗口方法。
/// </summary>
/// <param name="windowToHide">要隐藏的窗口。</param>
public void Hide(UIBase windowToHide)
{
// 确保窗口不为空且未被销毁,并且当前可见
if (!windowToHide || !windowToHide.gameObject || !windowToHide.IsVisible) return;
// 隐藏目标窗口
windowToHide.Hide();
// 当UI窗口被隐藏时如果它实现了ITickUI接口则必须将其从Clock中移除。
// 这防止了隐藏窗口继续被Tick避免性能开销和潜在的NullReferenceException。
if (windowToHide is ITickUI iTick)
Base.Clock.RemoveTickUI(iTick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToHide, false); // <--- 修改点 3
OnWindowVisibilityChanged?.Invoke(windowToHide, false);
needUpdate = true;
}
@@ -231,8 +203,6 @@ namespace Base
/// <param name="uiName">要隐藏的UI窗口名称。</param>
public void Hide(string uiName)
{
// 仅在可见窗口中查找并隐藏更符合HideByName的即时操作期望
// 如果需要隐藏所有名称匹配的窗口(包括隐藏的),则需要遍历 _allWindows
var visibleWindowToHide = _visibleWindows.FirstOrDefault(window => window != null && window.name == uiName && window.IsVisible);
if (visibleWindowToHide != null)
{
@@ -247,14 +217,12 @@ namespace Base
/// </summary>
public void HideAll()
{
// 创建 _visibleWindows 的一个副本进行迭代,以避免在循环中修改原列表导致迭代器失效
var windowsToHide = _visibleWindows.ToList();
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
// 再次检查窗口是否仍然可见,因为其他操作可能已经隐藏了它
if (visibleWindow != null && visibleWindow.IsVisible)
{
Hide(visibleWindow); // Hide() 方法会触发 OnWindowVisibilityChanged 事件
Hide(visibleWindow);
}
}
}
@@ -264,7 +232,6 @@ namespace Base
/// </summary>
private void UpdatePauseState()
{
// 确保 _visibleWindows 中的窗口都有效
var shouldPause = _visibleWindows.Any(w => w && w.needPause);
if (Base.Clock.Instance.Pause != shouldPause)
{
@@ -280,7 +247,7 @@ namespace Base
_visibleWindows.Clear();
foreach (var window in _allWindows)
{
if (window && window.IsVisible) // 确保窗口有效且可见
if (window && window.IsVisible)
{
_visibleWindows.Add(window);
}
@@ -294,10 +261,7 @@ namespace Base
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// 在销毁时清空所有订阅者防止因MonoSingleton持久化导致下一场景重新加载时出现旧的订阅者如果单例不销毁事件本身也不会自动清空订阅
// 如果 UIInputControl 是一个会随场景销毁的普通 MonoBehaviour 而不是持久化的 MonoSingleton, 某些情况下清除订阅者可以防止跨场景的引用问题。
// 但对于 MonoSingleton它通常是持久化的所以事件在重新加载场景后仍然保留。明确清空可以避免不必要的资源占用虽然在应用程序关闭时会自动释放。
// OnWindowVisibilityChanged = null; // 谨慎使用如果外部有长期订阅的需求清空可能导致问题。通常更推荐外部在OnDestroy中取消订阅。
Clock.RemoveTickUI(this);
}
/// <summary>
@@ -306,8 +270,8 @@ namespace Base
/// </summary>
protected override void OnStart()
{
// 订阅场景加载事件以便在新场景加载后重新注册UI窗口
SceneManager.sceneLoaded += OnSceneLoaded;
Clock.AddTickUI(this);
}
/// <summary>
@@ -318,7 +282,6 @@ namespace Base
/// <param name="mode">场景加载模式。</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 当场景加载时重新查找并注册所有UI窗口
RegisterAllWindows();
}
}