(client) feat:添加基地界面到游玩界面的过程,添加存档管理,技能树变得可用 (#58)

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/58
This commit is contained in:
2025-10-03 00:31:34 +08:00
parent aff747be17
commit dd9d90439d
134 changed files with 10322 additions and 4872 deletions

View File

@@ -122,7 +122,7 @@ namespace Managers
}
}
// 如果是第一次启动,写回 loadOrder顺序为 newOrder
// 如果是第一次启动,写回 loadOrder顺序为 newOrder
if (isFirstLaunch)
{
// 将 newOrder 转换为 string[],并写回设置
@@ -315,6 +315,8 @@ namespace Managers
/// <returns>如果找到,返回转换为目标类型的 <see cref="Define"/> 对象;否则返回 null。</returns>
public T FindDefine<T>(string defineName) where T : Define
{
if (string.IsNullOrEmpty(defineName))
return null;
if (defines.TryGetValue(typeof(T).Name, out var typeDict))
{
if (typeDict.TryGetValue(defineName, out var define))
@@ -460,8 +462,8 @@ namespace Managers
return null;
}
}
/// <summary>
/// 返回所有加载的定义包的字符串表示。
/// </summary>

View File

@@ -293,6 +293,7 @@ namespace Managers
var factionKey = def.attributes.defName ?? "default";
_pendingAdditions.Add(Tuple.Create(dimensionId, factionKey, entityComponent));
return entityComponent;
}
catch (Exception ex)
@@ -363,7 +364,7 @@ namespace Managers
/// <param name="dimensionId">实体所属的维度ID。</param>
/// <param name="entityDef">实体定义对象。</param>
/// <param name="pos">生成位置。</param>
public EntityPrefab GenerateEntity(string dimensionId, EntityDef entityDef, Vector3 pos)
public EntityPrefab GenerateCharacterEntity(string dimensionId, EntityDef entityDef, Vector3 pos)
{
if (!characterPrefab)
{
@@ -607,7 +608,7 @@ namespace Managers
Relation targetRelationship)
{
// 参数校验:确保输入参数有效,避免空引用异常。
if (sourceEntityPrefab == null || sourceEntityPrefab.entity == null)
if (!sourceEntityPrefab || !sourceEntityPrefab.entity)
{
Debug.LogWarning("实体管理器FindNearestEntityByRelation 方法中,源实体预制体或其内部实体为空。无法执行搜索。");
return null;
@@ -620,137 +621,111 @@ namespace Managers
return null;
}
// 初始化追踪变量:设置初始值,用于在遍历过程中追踪最近的实体及其距离。
// 使用平方距离 (SqrMagnitude) 可以避免在每次距离计算时进行昂贵的开方运算,从而提高性能。
EntityPrefab nearestTarget = null;
var minDistanceSqr = float.MaxValue;
var sourcePos = sourceEntityPrefab.transform.position;
// 关系管理器实例检查:确保 AffiliationManager 可用,它是判断实体关系的核心组件。
var affiliationManager = AffiliationManager.Instance;
if (affiliationManager == null)
foreach (var (currentFactionKey, factionEntities) in factionDict)
{
Debug.LogError("实体管理器FindNearestEntityByRelation 方法中AffiliationManager 实例为空。无法确定实体关系。");
return null;
}
var factionRelation = affiliationManager.GetRelation(sourceEntityPrefab.entity.affiliation, currentFactionKey);
// 遍历所有派系和实体_dimensionFactionEntities 按维度和派系组织,需要遍历所有派系才能找到维度内的所有实体。
foreach (var factionEntities in factionDict.Values) // factionDict.Values 是 LinkedList<EntityPrefab> 的集合
{
if (factionRelation != targetRelationship)
{
continue;
}
foreach (var currentEntityPrefab in factionEntities)
{
// 实体有效性及排除源实体自身:
// 1. 排除无效或已死亡的实体,确保只处理活跃的实体。
// 2. 在寻找“最近”实体时,通常指的是 *除了自身以外* 的实体。
// 如果需要包含自身(例如,当 targetRelationship 是 AffiliationManager.Relation.Self 时),
// 可以根据具体需求调整此逻辑,但默认行为是排除自身。
if (!currentEntityPrefab || !currentEntityPrefab.entity ||
currentEntityPrefab.entity.IsDead || currentEntityPrefab == sourceEntityPrefab)
{
continue;
}
// 关系判断:使用 AffiliationManager 提供的接口判断源实体与当前遍历实体之间的关系。
var currentRelation =
affiliationManager.GetRelation(sourceEntityPrefab.entity, currentEntityPrefab.entity);
if (currentRelation == targetRelationship)
if (currentEntityPrefab.entity is not CombatantEntity)
{
// 距离计算与最近实体更新:
// 1. 计算与源实体的距离(使用平方距离优化)。
// 2. 如果当前实体更近,则更新 nearestTarget 和 minDistanceSqr。
var distanceSqr = Vector3.SqrMagnitude(currentEntityPrefab.transform.position - sourcePos);
if (distanceSqr < minDistanceSqr)
{
minDistanceSqr = distanceSqr;
nearestTarget = currentEntityPrefab;
}
continue;
}
var distanceSqr = Vector3.SqrMagnitude(currentEntityPrefab.transform.position - sourcePos);
if (distanceSqr < minDistanceSqr)
{
minDistanceSqr = distanceSqr;
nearestTarget = currentEntityPrefab;
}
}
}
return nearestTarget; // 返回找到的最近实体
return nearestTarget;
}
/// <summary>
/// 在指定维度中,判断是否存在任何与源实体敌对的活跃实体。
/// 此版本修正了将掉落物或子弹识别为敌对派系的问题,现在只考虑可转换为 CombatantEntity 的实体。
/// 并优化了搜索逻辑,通过先判断派系关系来减少不必要的实体遍历。
/// </summary>
/// <param name="dimensionId">要搜索的维度ID。</param>
/// <param name="sourceEntityPrefab">作为参照的源实体预制体。</param>
/// <returns>如果存在敌对的 CombatantEntity 则返回 true否则返回 false。</returns>
public bool ExistsHostile(string dimensionId, EntityPrefab sourceEntityPrefab)
{
// 参数校验:确保输入参数有效,避免空引用异常。
// 使用 Unity 的 null 检查运算符 '!'
if (!sourceEntityPrefab || !sourceEntityPrefab.entity)
{
Debug.LogWarning("实体管理器ExistsHostile 方法中,源实体预制体或其内部实体为空。无法执行搜索。");
return false;
}
// 新增校验:确保源实体自身是一个 CombatantEntity。
// 因为我们现在只关注 CombatantEntity 之间的敌对关系。
var sourceCombatant = sourceEntityPrefab.entity as CombatantEntity;
if (!sourceCombatant) // 使用 Unity 的 null 检查运算符 '!'
if (!sourceCombatant)
{
Debug.LogWarning(
$"实体管理器ExistsHostile 方法中,源实体 '{sourceEntityPrefab.name}' 无法转换为 CombatantEntity 类型。无法有效判断是否存在敌对的活跃实体。");
return false;
}
if (sourceCombatant.IsDead)
{
return false; // 源 CombatantEntity 已经失效,它无法判断敌对实体。
return false;
}
// 维度数据存在性检查:验证目标维度是否在实体管理器的内部数据结构中被初始化和管理。
if (!_dimensionFactionEntities.TryGetValue(dimensionId, out var factionDict))
{
Debug.LogWarning($"实体管理器ExistsHostile 方法中,维度 '{dimensionId}' 未被初始化或未在内部管理实体。");
return false;
}
// 遍历所有实体:遍历维度内的所有派系,再遍历每个派系下的所有实体。
foreach (var factionEntities in factionDict.Values)
var affiliationManager = AffiliationManager.Instance; // 获取 AffiliationManager 实例一次
foreach (var (currentFactionKey, factionEntities) in factionDict)
{
var factionRelation = affiliationManager.GetRelation(sourceCombatant.affiliation, currentFactionKey);
if (factionRelation != Relation.Hostile)
{
continue;
}
foreach (var currentEntityPrefab in factionEntities)
{
// 实体有效性及排除源实体自身:
// 1. 排除无效的实体预制体或内部实体。
// 2. 排除已失效的 CombatantEntity。
// 3. 排除源实体自身,避免自身判断为敌对。
if (!currentEntityPrefab || !currentEntityPrefab.entity ||
currentEntityPrefab == sourceEntityPrefab) // 源实体自身已被转换为 sourceCombatant 并检查,这里只检查 prefab 引用是否相同
if (currentEntityPrefab == null || currentEntityPrefab.entity == null ||
currentEntityPrefab == sourceEntityPrefab)
{
continue;
}
// 【核心改动】新增类型检查:只有能转换为 CombatantEntity 的实体才会被进一步判断。
// 这样可以排除掉落物、子弹等非 CombatantEntity 的实体。
var currentCombatant = currentEntityPrefab.entity as CombatantEntity;
if (!currentCombatant) // 使用 Unity 的 null 检查运算符 '!'
if (currentCombatant == null || currentCombatant.IsDead)
{
continue; // 如果不是 CombatantEntity则跳过此实体。
}
// 检查目标 CombatantEntity 是否已失效(死亡或摧毁)。
// 同理,这里假设 CombatantEntity 包含 IsDead 属性。
// **重要:请确保 CombatantEntity 定义了 IsDead 属性,或者替换为更通用的失效状态属性。**
if (currentCombatant.IsDead)
{
continue; // 目标 CombatantEntity 已经失效,不是活跃的敌对实体。
}
// 关系判断:判断当前 CombatantEntity 与源 CombatantEntity 是否为“敌对”关系。
// AffiliationManager.GetRelation 应该接收 CombatantEntity 类型。
if (AffiliationManager.Instance.GetRelation(sourceCombatant, currentCombatant) == Relation.Hostile)
{
return true; // 找到第一个敌对的 CombatantEntity 即返回 true提高效率。
continue;
}
return true;
}
}
return false; // 遍历完所有实体都未找到敌对的 CombatantEntity。
return false;
}
/// <summary>
@@ -913,6 +888,7 @@ namespace Managers
$"实体管理器在场景加载初始化期间Program 为ID '{dimensionId}' 注册了一个空的 Dimension 对象。这可能表明维度注册存在问题。");
}
}
Clock.AddTick(this);
}
/// <summary>

View File

@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Data;
using EventWorkClass;
using Utils;
using UnityEngine;
using Base;
using UnityEngine.SceneManagement;
namespace Managers
{
@@ -34,6 +36,8 @@ namespace Managers
/// 获取当前加载步骤的描述,用于启动流程的进度显示。
/// </summary>
public string StepDescription => "正在载入事件和故事";
public bool HasStoryDisplay=>_activeStoryPlayers.Any();
/// <summary>
/// 初始化事件管理器,从定义管理器中加载所有事件定义和故事定义。
@@ -85,8 +89,14 @@ namespace Managers
// 将自身注册到时钟系统,以便每帧更新故事播放。
Clock.AddTick(this);
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Clock.AddTick(this);
}
/// <summary>
/// 清理事件管理器,释放所有已加载的事件和故事定义。
/// </summary>

View File

@@ -65,18 +65,16 @@ namespace Managers
public ItemResource GetItem(string defName)
{
return _items.GetValueOrDefault(defName, defaultItem);
return string.IsNullOrEmpty(defName) ? defaultItem : _items.GetValueOrDefault(defName, defaultItem);
}
public ItemResource FindItemByName(string itemName)
{
if (string.IsNullOrEmpty(itemName)) return defaultItem;
return _itemsByName.GetValueOrDefault(itemName)?.FirstOrDefault();
return string.IsNullOrEmpty(itemName) ? defaultItem : _itemsByName.GetValueOrDefault(itemName)?.FirstOrDefault();
}
public List<ItemResource> FindAllItemsByName(string itemName)
{
if (string.IsNullOrEmpty(itemName)) return new List<ItemResource>();
return _itemsByName.GetValueOrDefault(itemName, new List<ItemResource>());
return string.IsNullOrEmpty(itemName) ? new List<ItemResource>() : _itemsByName.GetValueOrDefault(itemName, new List<ItemResource>());
}
public void Clear()

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using UnityEngine; // 引入 UnityEngine 命名空间以使用 Debug.Log
namespace Managers
{
/// <summary>
/// 提供一个通用的键值对存档管理器
/// 允许存储和检索各种类型的数据,并与 SaveManager 集成以进行持久化。
/// </summary>
public class KeyValueArchiveManager : Utils.Singleton<KeyValueArchiveManager>, ISavableSingleton, ILaunchManager
{
/// <summary>
/// 内部用于存储键值对的字典,标记为 [Savable] 以便 SaveManager 进行序列化和反序列化。
/// </summary>
[Savable]
private Dictionary<string, object> _data = new Dictionary<string, object>();
/// <summary>
/// 私有构造函数,用于单例模式。
/// 在首次创建实例时,将其注册到 SaveManager。
/// </summary>
public KeyValueArchiveManager()
{
SaveManager.Instance.RegisterSavable(this);
}
/// <summary>
/// 将指定键的值设置为指定值。
/// 如果键已存在,则更新其值;如果不存在,则添加键值对。
/// </summary>
/// <typeparam name="T">值的类型。</typeparam>
/// <param name="key">要设置的键,不能为空或空白。</param>
/// <param name="value">要存储的值。</param>
/// <exception cref="ArgumentException">当键为 null 或空白时抛出。</exception>
public void Set<T>(string key, T value)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("键不能为空或空白。", nameof(key));
}
_data[key] = value;
}
/// <summary>
/// 根据指定的键获取存储的值。
/// </summary>
/// <typeparam name="T">期望值的类型。</typeparam>
/// <param name="key">要获取值的键,不能为空或空白。</param>
/// <param name="defaultValue">如果键不存在或类型不匹配时返回的默认值。默认为类型 T 的默认值。</param>
/// <returns>与键关联的值,如果键不存在或类型不匹配则返回 defaultValue。</returns>
/// <exception cref="ArgumentException">当键为 null 或空白时抛出。</exception>
public T Get<T>(string key, T defaultValue = default(T))
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("键不能为空或空白。", nameof(key));
}
if (_data.TryGetValue(key, out object value))
{
if (value is T typedValue)
{
return typedValue;
}
Debug.LogWarning(
$"警告: 键 '{key}' 存储的值类型为 '{value.GetType().Name}',但请求的类型为 '{typeof(T).Name}'。返回默认值。");
}
return defaultValue;
}
/// <summary>
/// 检查管理器中是否存在指定的键。
/// </summary>
/// <param name="key">要检查的键,不能为空或空白。</param>
/// <returns>如果键存在则为 true否则为 false。</returns>
/// <exception cref="ArgumentException">当键为 null 或空白时抛出。</exception>
public bool HasKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("键不能为空或空白。", nameof(key));
}
return _data.ContainsKey(key);
}
/// <summary>
/// 从管理器中移除指定的键及其对应的值。
/// </summary>
/// <param name="key">要移除的键,不能为空或空白。</param>
/// <exception cref="ArgumentException">当键为 null 或空白时抛出。</exception>
public void Remove(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("键不能为空或空白。", nameof(key));
}
_data.Remove(key);
}
/// <summary>
/// ISavableSingleton 接口实现:当游戏状态需要被彻底重置时调用(例如,开始一个新游戏)。
/// 将清除所有存储的键值对。
/// </summary>
public void ResetState()
{
_data.Clear();
}
// 加载存档数据中的步骤描述。
public string StepDescription { get; } = "加载存档数据中";
public void Init()
{
// 启动初始化逻辑(当前为空)
}
public void Clear()
{
// 清理逻辑(当前为空)
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b8bc5d84a1ec4dcea0b47115691d1284
timeCreated: 1759402368

View File

@@ -25,7 +25,7 @@ namespace Managers
SceneManager.sceneLoaded -= OnSceneLoaded;
}
public void DisplayMessage(string message, PromptDisplayCategory type,Color? color=null)
public void DisplayMessage(string message, PromptDisplayCategory type,Color? color=null,float showTime=3)
{
if (!_canvas)
{
@@ -40,7 +40,7 @@ namespace Managers
return;
// GenerateTemporaryAnimation的第三个参数是显示时间
TemporaryAnimationManager.Instance.GenerateTemporaryAnimation(message,
Program.Instance.FocusedEntity.Position, 5); // 5秒显示时间
Program.Instance.FocusedEntity.Position, showTime); // 5秒显示时间
break;
case PromptDisplayCategory.Default:
@@ -75,6 +75,7 @@ namespace Managers
}
hintTextInstance.Init(message); // Init 方法会处理动画和生命周期
hintTextInstance.lifeTime = showTime;
// TemporaryAnimatorText 应该在 Init 内部设置好 lifeTime 并自动销毁。
break;
@@ -102,8 +103,7 @@ namespace Managers
}
textInstance.Init(message); // Init 方法会处理动画和生命周期
// textInstance.lifeTime 可以在 Init 方法内部设置,如果 Init 没有提供参数,这里就无法直接设置。
// 假设 Init 已经处理好生命周期。
textInstance.lifeTime = showTime;
break;
case PromptDisplayCategory.FocusedEntityChatBubble:

View File

@@ -20,7 +20,7 @@ namespace Managers
/// <summary>
/// 用于标记在单例类中需要被SaveManager存储的属性。
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class SavableAttribute : Attribute
{
/// <summary>
@@ -40,13 +40,17 @@ namespace Managers
private const string SaveFileName = "game_save.json";
// 存档路径
private static string SaveFilePath => Path.Combine(Application.persistentDataPath, SaveFileName);
private static string SaveFilePath => Path.Combine("Save", SaveFileName);
public string StepDescription => "加载存档中";
// 存储所有需要保存状态的单例实例
private readonly Dictionary<Type, ISavableSingleton> _savableSingletons = new();
~SaveManager()
{
SaveGame(); // 在销毁时保存游戏
}
/// <summary>
/// 注册一个需要保存状态的单例实例。
/// </summary>
@@ -86,33 +90,10 @@ namespace Managers
}
/// <summary>
/// 实现ILaunchManager接口的Clear方法清空存档数据
/// 实现ILaunchManager接口的Clear方法Clear是重载不做任何事
/// </summary>
public void Clear()
{
try
{
if (File.Exists(SaveFilePath))
{
File.Delete(SaveFilePath);
Debug.Log($"SaveManager: 存档文件 '{SaveFileName}' 已删除。");
}
}
catch (Exception ex)
{
Debug.LogError($"SaveManager: 清空存档数据失败: {ex.Message}");
}
// 清空所有注册单例的Savable属性设置为默认值
foreach (var singletonEntry in _savableSingletons)
{
var singletonType = singletonEntry.Key;
var singletonInstance = singletonEntry.Value;
foreach (var prop in GetSavableProperties(singletonType))
{
prop.SetValue(singletonInstance, GetDefaultValue(prop.PropertyType));
}
}
}
/// <summary>
@@ -144,6 +125,12 @@ namespace Managers
try
{
var directoryPath = Path.GetDirectoryName(SaveFilePath);
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
// DEBUG: Debug.Log($"SaveManager: 文件夹 '{directoryPath}' 已创建。");
}
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,

View File

@@ -21,11 +21,7 @@ namespace Managers
{
SaveManager.Instance.RegisterSavable(this);
}
~SkillTreeManager()
{
SaveManager.Instance.UnregisterSavable(this);
}
public void Init()
{