(client) feat:状态UI

This commit is contained in:
m0_75251201
2025-09-02 11:08:15 +08:00
parent 49d32a99b6
commit ce04c8cec8
54 changed files with 7224 additions and 835 deletions

View File

@ -19,11 +19,13 @@ namespace Base
public float globalVolume = 1.0f;
public WindowMode currentWindowMode = WindowMode.Fullscreen;
public Vector2Int windowResolution = new(1920, 1080);
public string[] loadOrder;
}
// 当前游戏设置
public GameSettings CurrentSettings = new();
// 窗口模式枚举
public enum WindowMode { Fullscreen, Windowed, Borderless }

View File

@ -1,3 +1,4 @@
using System; // Added for Action
using System.Collections.Generic;
using System.Linq;
using UI;
@ -19,6 +20,13 @@ namespace Base
private bool needUpdate = false;
/// <summary>
/// 当UI窗口的可见性状态发生改变时触发的事件。
/// 参数1: 发生改变的UIBase实例。
/// 参数2: 窗口的新可见状态 (true为显示false为隐藏)。
/// </summary>
public event Action<UIBase, bool> OnWindowVisibilityChanged; // <--- 新增
/// <summary>
/// 获取所有已注册的UI窗口的总数量。
/// </summary>
@ -93,7 +101,7 @@ namespace Base
// 确保窗口不为空且其GameObject未被销毁
if (window != null && window.gameObject != null)
{
window.Hide();
window.Hide(); // 隐藏操作会触发 OnWindowVisibilityChanged 事件
}
}
@ -173,6 +181,9 @@ namespace Base
if (itick != null)
Base.Clock.AddTickUI(itick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToShow, true); // <--- 修改点 2
needUpdate = true;
}
@ -208,6 +219,9 @@ namespace Base
// 这防止了隐藏窗口继续被Tick避免性能开销和潜在的NullReferenceException。
if (windowToHide is ITickUI iTick)
Base.Clock.RemoveTickUI(iTick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToHide, false); // <--- 修改点 3
needUpdate = true;
}
@ -241,7 +255,7 @@ namespace Base
// 再次检查窗口是否仍然可见,因为其他操作可能已经隐藏了它
if (visibleWindow != null && visibleWindow.IsVisible)
{
Hide(visibleWindow);
Hide(visibleWindow); // Hide() 方法会触发 OnWindowVisibilityChanged 事件
}
}
}
@ -281,6 +295,10 @@ namespace Base
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// 在销毁时清空所有订阅者防止因MonoSingleton持久化导致下一场景重新加载时出现旧的订阅者如果单例不销毁事件本身也不会自动清空订阅
// 如果 UIInputControl 是一个会随场景销毁的普通 MonoBehaviour 而不是持久化的 MonoSingleton, 某些情况下清除订阅者可以防止跨场景的引用问题。
// 但对于 MonoSingleton它通常是持久化的所以事件在重新加载场景后仍然保留。明确清空可以避免不必要的资源占用虽然在应用程序关闭时会自动释放。
// OnWindowVisibilityChanged = null; // 谨慎使用如果外部有长期订阅的需求清空可能导致问题。通常更推荐外部在OnDestroy中取消订阅。
}
/// <summary>

View File

@ -19,10 +19,6 @@ namespace Data
// ======================================================================
// 百分比偏移 (Multiplied as a factor, e.g., 0.1 for +10%)
// ======================================================================
// 注意:百分比偏移通常是乘法因子。
// 0f 表示没有百分比变化0.1f 表示增加10%-0.1f 表示减少10%。
// 应用时: baseValue * (1 + percentageOffset)
// 或者对于更直接的乘法,可以直接用 multiplyRatio例如 1.1 表示增加 10%。
public float healthPercentOffset = 0f; // 例如 0.1 表示 +10%
public float moveSpeedPercentOffset = 0f;
public float attackPercentOffset = 0f;
@ -31,94 +27,39 @@ namespace Data
public float attackRangePercentOffset = 0f;
public float attackTargetCountPercentOffset = 0f;
// ======================================================================
// 数组偏移 / 集合偏移 (更复杂的情况,如果原始属性是数组/集合)
// 因为你的Attributes类目前都是单一值所以这里先按单一值处理。
// 如果 future Attributes.attackTargets 变成 List<string> 或者 int[]
// 这里就需要 List<string> addedAttackTargets 或 List<int> addedAttackTargetCounts
// 来表示要添加/移除的元素。
// 示例:如果将来有一个属性是效果列表,这里可以定义要添加的效果
// public List<string> addedEffects = new List<string>();
// public List<string> removedEffects = new List<string>();
// 如果 `attackTargetCount` 实际代表可以攻击的目标ID数组那么
// public List<int> addedAttackTargets = new List<int>();
// public List<int> removedAttackTargets = new List<int>();
// 注意:对于 `attackTargetCount` 来说,它本身是一个 int
// 上面的 `attackTargetCountOffset` 已经足够处理“增加攻击目标数量”的需求。
// "数组偏移"通常指的是当原始属性本身是一个集合时,你想要修改这个集合的元素。
// 如果 `Attributes` 类保持其当前形式 (都是单一数值),那么不需要专门的数组偏移。
// ======================================================================
// 构造函数 (可选,用于方便初始化)
// ======================================================================
public AttributesOffsetDef()
{
}
// 可以添加带参数的构造函数,方便快速设置
public AttributesOffsetDef(float healthAbs = 0f, float moveSpeedAbs = 0f, float attackAbs = 0f,
float defenseAbs = 0f, float attackSpeedAbs = 0f, float attackRangeAbs = 0f,
float attackTargetCountAbs = 0f,
float healthPct = 0f, float moveSpeedPct = 0f, float attackPct = 0f,
float defensePct = 0f, float attackSpeedPct = 0f, float attackRangePct = 0f,
float attackTargetCountPct = 0f)
{
healthOffset = healthAbs;
moveSpeedOffset = moveSpeedAbs;
attackOffset = attackAbs;
defenseOffset = defenseAbs;
attackSpeedOffset = attackSpeedAbs;
attackRangeOffset = attackRangeAbs;
attackTargetCountOffset = attackTargetCountAbs;
healthPercentOffset = healthPct;
moveSpeedPercentOffset = moveSpeedPct;
attackPercentOffset = attackPct;
defensePercentOffset = defensePct;
attackSpeedPercentOffset = attackSpeedPct;
attackRangePercentOffset = attackRangePct;
attackTargetCountPercentOffset = attackTargetCountPct;
}
// ======================================================================
// 应用偏移的方法
// ======================================================================
/// <summary>
/// 将此偏移应用到给定的Attributes实例
/// 重载 + 操作符,用于合并两个 AttributesOffsetDef 实例。
/// </summary>
/// <param name="baseAttributes">要应用偏移的基础Attributes。</param>
/// <returns>应用偏移后的新Attributes实例。</returns>
public Attributes ApplyTo(Attributes baseAttributes)
/// <param name="a">第一个属性修正定义。</param>
/// <param name="b">第二个属性修正定义。</param>
/// <returns>一个新的 AttributesOffsetDef 实例,包含两个输入的累加值。</returns>
public static AttributesOffsetDef operator +(AttributesOffsetDef a, AttributesOffsetDef b)
{
// 创建一个新的Attributes实例以避免修改原始实例
// 或者如果需要直接修改,可以返回 void
Attributes modifiedAttributes = new Attributes
// 处理 null 情况,如果其中一个为 null则返回另一个的副本或一个空实例如果两者都为 null
if (a == null && b == null) return new AttributesOffsetDef();
if (a == null) return b; // 如果 a 是 null返回 b 的值
if (b == null) return a; // 如果 b 是 null返回 a 的值
var combined = new AttributesOffsetDef
{
health = baseAttributes.health,
moveSpeed = baseAttributes.moveSpeed,
attack = baseAttributes.attack,
defense = baseAttributes.defense,
attackSpeed = baseAttributes.attackSpeed,
attackRange = baseAttributes.attackRange,
attackTargetCount = baseAttributes.attackTargetCount
// 绝对值偏移累加
healthOffset = a.healthOffset + b.healthOffset,
moveSpeedOffset = a.moveSpeedOffset + b.moveSpeedOffset,
attackOffset = a.attackOffset + b.attackOffset,
defenseOffset = a.defenseOffset + b.defenseOffset,
attackSpeedOffset = a.attackSpeedOffset + b.attackSpeedOffset,
attackRangeOffset = a.attackRangeOffset + b.attackRangeOffset,
attackTargetCountOffset = a.attackTargetCountOffset + b.attackTargetCountOffset,
// 百分比偏移累加
healthPercentOffset = a.healthPercentOffset + b.healthPercentOffset,
moveSpeedPercentOffset = a.moveSpeedPercentOffset + b.moveSpeedPercentOffset,
attackPercentOffset = a.attackPercentOffset + b.attackPercentOffset,
defensePercentOffset = a.defensePercentOffset + b.defensePercentOffset,
attackSpeedPercentOffset = a.attackSpeedPercentOffset + b.attackSpeedPercentOffset,
attackRangePercentOffset = a.attackRangePercentOffset + b.attackRangePercentOffset,
attackTargetCountPercentOffset = a.attackTargetCountPercentOffset + b.attackTargetCountPercentOffset
};
// 首先应用百分比偏移
modifiedAttributes.health = (int)(modifiedAttributes.health * (1f + healthPercentOffset));
modifiedAttributes.moveSpeed *= (1f + moveSpeedPercentOffset);
modifiedAttributes.attack = (int)(modifiedAttributes.attack * (1f + attackPercentOffset));
modifiedAttributes.defense = (int)(modifiedAttributes.defense * (1f + defensePercentOffset));
modifiedAttributes.attackSpeed = (int)(modifiedAttributes.attackSpeed * (1f + attackSpeedPercentOffset));
modifiedAttributes.attackRange = (int)(modifiedAttributes.attackRange * (1f + attackRangePercentOffset));
modifiedAttributes.attackTargetCount =
(int)(modifiedAttributes.attackTargetCount * (1f + attackTargetCountPercentOffset));
// 然后应用绝对值偏移
modifiedAttributes.health += (int)healthOffset;
modifiedAttributes.moveSpeed += moveSpeedOffset;
modifiedAttributes.attack += (int)attackOffset;
modifiedAttributes.defense += (int)defenseOffset;
modifiedAttributes.attackSpeed += (int)attackSpeedOffset;
modifiedAttributes.attackRange += (int)attackRangeOffset;
modifiedAttributes.attackTargetCount += (int)attackTargetCountOffset;
return modifiedAttributes;
return combined;
}
}
}

View File

@ -150,6 +150,8 @@ namespace Data
public string packID;
public string packRootPath;
public int priority = -1;
public string Name
{
get

View File

@ -12,8 +12,9 @@ namespace Data
public BehaviorTreeDef behaviorTree;
public AffiliationDef affiliation;
public DrawNodeDef deathAnimation;
public EventDef[] deathEffects;
}

View File

@ -4,5 +4,6 @@ namespace Data
{
public HediffEventDef hediffEvent;
public EntityEventDef entityEvent;
public string value;
}
}

View File

@ -3,6 +3,6 @@ namespace Data
public class HediffStageDef:Define
{
public float start;
public AffiliationDef attributesOffset = new();
public AttributesOffsetDef attributesOffset = new();
}
}

View File

@ -0,0 +1,11 @@
namespace Data
{
public class SkillTreeDef:Define
{
public string tag="Default";
public AffiliationDef faction;
public SkillTreeDef[] prerequisites;
public WeaponDef[] unlockedWeapons;
public HediffDef[] unlockedHediffs;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 08ca1b39c5b342ceb23bd565d433ac66
timeCreated: 1756705012

View File

@ -1,3 +1,4 @@
using System;
using Data;
namespace Entity
@ -11,6 +12,7 @@ namespace Entity
public int attackSpeed = 2;
public int attackRange = 3;
public int attackTargetCount = 1;
public Attributes(AttributesDef def)
{
health = def.health;
@ -21,7 +23,99 @@ namespace Entity
attackRange = def.attackRange;
attackTargetCount = def.attackTargetCount;
}
public Attributes(Attributes other)
{
health = other.health;
moveSpeed = other.moveSpeed;
attack = other.attack;
defense = other.defense;
attackSpeed = other.attackSpeed;
attackRange = other.attackRange;
attackTargetCount = other.attackTargetCount;
}
public Attributes()
{}
{
}
/// <summary>
/// 根据给定的属性偏移,生成一个新的 Attributes 实例。
/// 原有的 Attributes 实例保持不变。
/// </summary>
/// <param name="offset">要应用的属性偏移定义。</param>
/// <returns>一个新的 Attributes 实例,包含了应用偏移后的值。</returns>
public Attributes GetModifiedAttributes(AttributesOffsetDef offset)
{
// 1. 创建当前 Attributes 实例的一个副本
var newAttributes = new Attributes(this);
if (offset == null)
{
return newAttributes; // 如果没有偏移,直接返回副本
}
// 2. 在副本上应用绝对值偏移
newAttributes.health += (int)offset.healthOffset;
newAttributes.moveSpeed += offset.moveSpeedOffset;
newAttributes.attack += (int)offset.attackOffset;
newAttributes.defense += (int)offset.defenseOffset;
newAttributes.attackSpeed += (int)offset.attackSpeedOffset;
newAttributes.attackRange += (int)offset.attackRangeOffset;
newAttributes.attackTargetCount += (int)offset.attackTargetCountOffset;
// 3. 在副本上应用百分比偏移 (基于应用绝对值偏移后的结果)
newAttributes.health = (int)(newAttributes.health * (1 + offset.healthPercentOffset));
newAttributes.moveSpeed *= (1 + offset.moveSpeedPercentOffset);
newAttributes.attack = (int)(newAttributes.attack * (1 + offset.attackPercentOffset));
newAttributes.defense = (int)(newAttributes.defense * (1 + offset.defensePercentOffset));
newAttributes.attackSpeed = (int)(newAttributes.attackSpeed * (1 + offset.attackSpeedPercentOffset));
newAttributes.attackRange = (int)(newAttributes.attackRange * (1 + offset.attackRangePercentOffset));
newAttributes.attackTargetCount =
(int)(newAttributes.attackTargetCount * (1 + offset.attackTargetCountPercentOffset));
// 4. 确保属性不低于最小值
newAttributes.health = Math.Max(0, newAttributes.health);
newAttributes.moveSpeed = Math.Max(0f, newAttributes.moveSpeed);
newAttributes.attack = Math.Max(0, newAttributes.attack);
newAttributes.defense = Math.Max(0, newAttributes.defense);
newAttributes.attackSpeed = Math.Max(0, newAttributes.attackSpeed);
newAttributes.attackRange = Math.Max(0, newAttributes.attackRange);
newAttributes.attackTargetCount = Math.Max(1, newAttributes.attackTargetCount);
// 5. 返回修改后的新 Attributes 实例
return newAttributes;
}
/// <summary>
/// 合并两个 Attributes 实例,生成一个新的 Attributes 实例,
/// 其中每个属性值都取自传入两个实例中对应属性的最小值。
/// 这对于应用属性上限或限制非常有用。
/// </summary>
/// <param name="a">第一个 Attributes 实例。</param>
/// <param name="b">第二个 Attributes 实例。</param>
/// <returns>一个新的 Attributes 实例,其属性是输入实例中对应属性的最小值。</returns>
public static Attributes Min(Attributes a, Attributes b)
{
// 处理 null 情况
if (a == null && b == null) return new Attributes(); // 两者都为null返回默认空属性
if (a == null) return new Attributes(b); // a为null返回b的副本
if (b == null) return new Attributes(a); // b为null返回a的副本
// 创建一个新的 Attributes 实例来存储结果
var result = new Attributes
{
health = Math.Min(a.health, b.health),
moveSpeed = Math.Min(a.moveSpeed, b.moveSpeed),
attack = Math.Min(a.attack, b.attack),
defense = Math.Min(a.defense, b.defense),
attackSpeed = Math.Min(a.attackSpeed, b.attackSpeed),
attackRange = Math.Min(a.attackRange, b.attackRange),
attackTargetCount = Math.Min(a.attackTargetCount, b.attackTargetCount)
};
return result;
}
}
}

View File

@ -9,7 +9,7 @@ using Utils;
namespace Entity
{
public class Character : Entity
public class Character : LivingEntity
{
private int _currentSelected; // 私有字段用于存储实际值

View File

@ -52,7 +52,13 @@ namespace Entity
/// <summary>
/// 实体的属性定义,包括生命值、攻击力、防御力等。
/// </summary>
public Attributes attributes = new();
public virtual Attributes attributes { get; protected set; }
private Attributes _baseAttributes;
public virtual Attributes baseAttributes
{
get { return _baseAttributes ??= new Attributes(entityDef.attributes); }
}
/// <summary>
/// 实体当前的移动方向。
@ -169,8 +175,6 @@ namespace Entity
// 协程引用
private Coroutine _attackCoroutine;
protected List<Hediff> hediffs;
[SerializeField] private float _hitBarUIShowTime = 5;
private float _hitBarUIShowTimer = 0;
@ -462,7 +466,7 @@ namespace Entity
/// <summary>
/// 更新实体的逻辑,包括玩家控制和自动行为。
/// </summary>
public void Tick()
public virtual void Tick()
{
if (_walkingTimer > 0)
{
@ -523,7 +527,7 @@ namespace Entity
if (IsAttacking || IsDead) return; // 死亡时无法攻击
// 尝试获取当前武器
WeaponResource currentWeapon = GetCurrentWeapon();
var currentWeapon = GetCurrentWeapon();
// 如果没有武器,可以选择进行徒手攻击或者直接返回
// 暂时设定为:如果没有武器,则不进行攻击
@ -777,7 +781,7 @@ namespace Entity
}
// STEP 4: 等待到攻击判定时间
float elapsedTime = 0f;
var elapsedTime = 0f;
while (elapsedTime < weapon.AttackDetectionTime)
{
if (IsDead)
@ -799,7 +803,7 @@ namespace Entity
ExecuteWeaponAction(weapon);
float remainingAnimationTime = weapon.AttackAnimationTime - elapsedTime;
var remainingAnimationTime = weapon.AttackAnimationTime - elapsedTime;
if (remainingAnimationTime > 0)
{
yield return new WaitForSeconds(remainingAnimationTime);
@ -884,10 +888,10 @@ namespace Entity
// 获取子弹方向。这里使用实体当前的移动方向作为子弹发射方向
// 更复杂的逻辑可能根据鼠标位置、目标位置等确定
Vector3 bulletDirection = direction; // 实体当前的朝向
var bulletDirection = direction; // 实体当前的朝向
if (PlayerControlled && Input.GetMouseButton(0)) // 玩家控制时,如果鼠标按下,尝试朝鼠标方向发射
{
Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
var mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mouseWorldPos.z = transform.position.z; // 保持Z轴一致
bulletDirection = (mouseWorldPos - transform.position).normalized;
}

View File

@ -1,15 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data;
using UnityEngine;
namespace Entity
{
/// <summary>
/// 定义一个接口,用于提供属性偏移量。
/// 任何实现此接口的组件都可以贡献实体的属性修正。
/// </summary>
public interface IAttributesOffsetProvider
{
/// <summary>
/// 获取此提供者当前的属性偏移量定义。
/// </summary>
/// <returns>此提供者所带来的属性偏移量。</returns>
AttributesOffsetDef GetAttributesOffset();
}
/// <summary>
/// 表示一个实体上运行时存在的健康状态Hediff
/// Hediff 可以是疾病、增益、减益、伤口等,它们会影响实体的属性、行为或状态。
/// </summary>
public class Hediff
{
public float Duration{get;private set;}
/// <summary>
/// 此 Hediff 的定义数据。
/// </summary>
public HediffDef def { get; private set; }
/// <summary>
/// 此 Hediff 附加到的活体实体。
/// </summary>
public LivingEntity parent { get; internal set; }
public Hediff(HediffDef def)
/// <summary>
/// 此 Hediff 存在的当前年龄(以秒为单位)。
/// </summary>
public float Age { get; private set; } = 0f;
/// <summary>
/// 当前激活的 Hediff 阶段的索引。
/// </summary>
private int _currentStageIndex = -1;
/// <summary>
/// 附加到此 Hediff 上的所有组件列表。
/// </summary>
public List<HediffComp> Comps { get; private set; } = new List<HediffComp>();
/// <summary>
/// 标志,指示此 Hediff 是否应该被父实体移除。
/// </summary>
public bool ShouldRemove { get; private set; } = false;
/// <summary>
/// 内部缓存,用于存储计算出的总属性偏移量。
/// </summary>
private AttributesOffsetDef _cachedTotalAttributesOffset;
/// <summary>
/// 标志,指示 Hediff 的属性偏移量是否需要重新计算。
/// </summary>
private bool _attribsDirty = true;
/// <summary>
/// 构造函数,创建一个新的运行时 Hediff 实例。
/// </summary>
/// <param name="definition">此 Hediff 的定义。</param>
/// <exception cref="ArgumentNullException">如果传入的定义为空。</exception>
public Hediff(HediffDef definition)
{
if (definition == null)
{
throw new ArgumentNullException(nameof(definition), "Hediff 定义不能为空。");
}
this.def = definition;
this._attribsDirty = true; // 构造时标记需要计算属性
// 确保阶段列表按开始时间排序,以便正确判断当前阶段。
if (this.def.stages == null) this.def.stages = new List<HediffStageDef>();
this.def.stages = this.def.stages.OrderBy(s => s.start).ToList();
// 实例化所有定义的组件。
if (def.comps != null)
{
foreach (var compDef in def.comps)
{
if (compDef.compClass != null && typeof(HediffComp).IsAssignableFrom(compDef.compClass))
{
try
{
// 使用 Activator.CreateInstance 动态创建组件实例,并传入构造函数参数。
// HediffComp 的构造函数需要接受 Hediff parentHediff 和 HediffCompDef def。
var comp = (HediffComp)Activator.CreateInstance(compDef.compClass, this, compDef);
Comps.Add(comp);
comp.Initialize(); // 初始化组件
}
catch (Exception ex)
{
Debug.LogError(
$"实例化健康状态组件 '{compDef.compClass?.Name ?? ""}' 失败,所属健康状态 '{def.defName ?? def.GetType().Name}'{ex.Message}");
}
}
else
{
Debug.LogWarning(
$"警告:健康状态组件定义 '{compDef.compClass?.Name ?? ""}' 无效或未继承自 HediffComp所属健康状态 '{def.defName ?? def.GetType().Name}'。");
}
}
}
// 初始化时确定第一个阶段,这会触发 SetDirty()。
UpdateStageIndex();
}
/// <summary>
/// 获取此 Hediff 当前激活的阶段定义。
/// 如果没有激活的阶段或阶段列表为空,则返回 null。
/// </summary>
public HediffStageDef CurrentStage
{
get
{
if (_currentStageIndex >= 0 && _currentStageIndex < def.stages.Count)
{
return def.stages[_currentStageIndex];
}
return null;
}
}
/// <summary>
/// 获取此 Hediff 当前累计的总属性偏移量。
/// 只有当属性被标记为“脏”时才重新计算,否则返回缓存值。
/// </summary>
public AttributesOffsetDef CurrentTotalAttributesOffset
{
get
{
// 只有当属性需要更新时才重新计算。
if (_attribsDirty || _cachedTotalAttributesOffset == null)
{
_cachedTotalAttributesOffset = CalculateTotalAttributesOffset();
_attribsDirty = false; // 计算完成后重置标志。
}
return _cachedTotalAttributesOffset;
}
}
/// <summary>
/// 内部方法,用于实际计算 Hediff 的所有总属性偏移量。
/// 这包括当前阶段的偏移量和所有组件提供的偏移量。
/// </summary>
/// <returns>此 Hediff 当前的总属性偏移量。</returns>
private AttributesOffsetDef CalculateTotalAttributesOffset()
{
var totalOffset = new AttributesOffsetDef();
if (CurrentStage != null)
{
totalOffset += CurrentStage.attributesOffset;
}
// 累加来自组件的属性偏移。
foreach (var comp in Comps)
{
if (comp is IAttributesOffsetProvider attributesOffsetProvider)
{
totalOffset += attributesOffsetProvider.GetAttributesOffset();
}
}
return totalOffset;
}
/// <summary>
/// 标记 Hediff 的属性偏移量需要重新计算。
/// 当 Hediff 的内部状态改变并可能影响属性时调用。
/// 同时会通知其父 LivingEntity使其也更新属性缓存。
/// </summary>
internal void SetDirty()
{
_attribsDirty = true;
// 如果此 Hediff 附加到了一个 LivingEntity 上,也通知 LivingEntity 属性可能改变。
if (parent)
{
parent.SetAttribsDirty();
}
}
/// <summary>
/// 更新 Hediff 的年龄,检查阶段变化并更新所有组件。
/// </summary>
/// <param name="deltaTime">自上次更新以来的时间(秒)。</param>
public void Tick(float deltaTime)
{
if (ShouldRemove)
return; // 已经标记为移除,无需继续更新。
Age += deltaTime; // 增加 Hediff 的年龄。
UpdateStageIndex(); // 检查是否有阶段变化(此方法内部会调用 SetDirty
// 更新所有组件。
foreach (var comp in Comps)
{
comp.Tick(deltaTime);
}
// 检查 Hediff 是否到期(如果 def.time > 0 表示有时限,否则为永久)。
if (def.time > 0 && Age >= def.time)
{
ShouldRemove = true;
SetDirty(); // Hediff 将被移除,其贡献的属性将不再有效,标记为脏。
}
}
/// <summary>
/// 根据当前年龄更新阶段索引。
/// 如果阶段发生变化,会调用 SetDirty()。
/// </summary>
private void UpdateStageIndex()
{
var originalStageIndex = _currentStageIndex; // 获取当前阶段索引。
var newStageIndex = _currentStageIndex;
// 从后往前遍历阶段,找到当前年龄所属的最高阶段。
for (var i = def.stages.Count - 1; i >= 0; i--)
{
if (def.stages[i].start <= Age)
{
newStageIndex = i;
break;
}
}
// 如果阶段发生变化,则标记为脏。
if (newStageIndex != originalStageIndex)
{
_currentStageIndex = newStageIndex;
// 阶段发生变化,属性偏移可能改变,标记为脏。
SetDirty();
}
}
/// <summary>
/// 当此 Hediff 被添加到实体时调用。
/// 进行初始化设置,如存储父实体引用,并标记属性为脏以强制重新计算。
/// </summary>
/// <param name="entity">此 Hediff 附加到的 LivingEntity 实例。</param>
internal void OnAdded(LivingEntity entity)
{
this.parent = entity; // 存储父实体引用。
SetDirty(); // 刚添加也需要标记为脏以确保属性在下次计算时被纳入。
foreach (var comp in Comps)
{
comp.OnAdded();
}
}
/// <summary>
/// 当此 Hediff 从实体移除时调用。
/// 进行清理操作,如清除父实体引用,并标记属性为脏以强制重新计算。
/// </summary>
/// <param name="entity">此 Hediff 从中移除的 LivingEntity 实例。</param>
internal void OnRemoved(LivingEntity entity)
{
foreach (var comp in Comps)
{
comp.OnRemoved();
}
this.parent = null; // 清除对父实体的引用。
SetDirty(); // 移除后也需要标记为脏确保属性计算考虑去除此Hediff。
}
}
}
}

View File

@ -0,0 +1,47 @@
using Data;
namespace Entity
{
// 运行时健康状态组件的抽象基类
public abstract class HediffComp
{
protected Hediff parentHediff; // 对父 Hediff 的引用
protected HediffCompDef def; // 对组件定义的引用
public HediffComp(Hediff parentHediff, HediffCompDef def)
{
this.parentHediff = parentHediff;
this.def = def;
}
/// <summary>
/// 组件初始化时调用,在构造函数之后。
/// </summary>
public virtual void Initialize()
{
}
/// <summary>
/// 每帧更新时调用。
/// </summary>
/// <param name="deltaTime">自上次更新以来的时间(秒)。</param>
public virtual void Tick(float deltaTime)
{
}
/// <summary>
/// 当其父 Hediff 被添加到实体时调用。
/// </summary>
public virtual void OnAdded()
{
}
/// <summary>
/// 当其父 Hediff 从实体移除时调用。
/// </summary>
public virtual void OnRemoved()
{
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f3f6a16d500840bea821c21ea612a5c6
timeCreated: 1756612166

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using Data;
using Managers;
using UnityEngine;
namespace Entity
{
// 假设 Entity 基类如下结构
// public abstract class Entity
// {
// public virtual Attributes baseAttributes { get; protected set; } = new Attributes();
// public virtual Attributes attributes { get; protected set; } = new Attributes();
// public virtual void Tick(float deltaTime) { /* Base entity update logic */ }
// }
/// <summary>
/// 表示一个具有生命周期、属性和可受健康状态Hediff影响的实体。
/// </summary>
public class LivingEntity : Entity
{
// 存储应用于此实体的所有健康状态Hediff列表。
protected List<Hediff> hediffs = new List<Hediff>();
// 标记实体属性是否需要重新计算。当Hediff发生变化时此标记会被设置为true。
private bool _needUpdateAttributes = true;
// 缓存实体的基础属性这些属性不受动态Hediff影响但可能受基类或Def影响。
private Attributes _cachedBaseAttributes;
/// <summary>
/// 获取实体的基础属性。这些属性通常来源于实体的定义Def并可能受到常驻的基础健康状态Base Hediffs影响。
/// </summary>
public override Attributes baseAttributes
{
get
{
// 仅在 _cachedBaseAttributes 为 null 时计算一次
if(_cachedBaseAttributes == null)
{
var defAttributes = base.baseAttributes;
var hediffOffset = new AttributesOffsetDef();
// 这里假设 SaveManager.Instance.baseHediffs 指的是“所有实体共通的基础Hediff”
// 并且这些基础 Hediff 也会影响 baseAttributes
foreach (var hediff in SaveManager.Instance.baseHediffs)
{
hediffOffset += hediff.CurrentTotalAttributesOffset;
}
_cachedBaseAttributes = defAttributes.GetModifiedAttributes(hediffOffset);
}
return _cachedBaseAttributes;
}
}
// 缓存实体当前的最终属性该属性是基础属性加上所有健康状态Hediff修正后的结果。
private Attributes _cachedAttributes;
/// <summary>
/// 获取实体当前的最终属性包括所有健康状态Hediff的修正。
/// </summary>
public override Attributes attributes
{
get
{
if (_needUpdateAttributes || _cachedAttributes == null)
{
// 1. 获取旧的属性值(在重新计算之前,也就是当前的缓存值)
// 仅用于需要与“当前值”进行比较或钳制的情况例如最大生命值Buff移除时
var oldCachedAttributes = _cachedAttributes;
if (oldCachedAttributes == null) // 如果是第一次计算初始化一个默认值或者使用baseAttributes
{
oldCachedAttributes = baseAttributes;
}
// 2. 计算完全修正后的“理论”最大属性值(基于 baseAttributes 和所有 Hediff 偏移)
var totalModifiedAttributes = baseAttributes;
var hediffOffset = new AttributesOffsetDef();
foreach (var hediff in hediffs)
{
hediffOffset += hediff.CurrentTotalAttributesOffset;
}
// 应用所有 hediff 的偏移到 totalModifiedAttributes
totalModifiedAttributes = totalModifiedAttributes.GetModifiedAttributes(hediffOffset);
_cachedAttributes = Attributes.Min(oldCachedAttributes, totalModifiedAttributes);
// 标记为已更新
_needUpdateAttributes = false;
}
return _cachedAttributes;
}
protected set => _cachedAttributes = value;
}
/// <summary>
/// 供内部使用的属性标记方法。当 Hediff 自身状态改变并影响属性时,通过此方法通知 LivingEntity。
/// </summary>
internal void SetAttribsDirty()
{
_needUpdateAttributes = true;
}
/// <summary>
/// 每帧调用的更新函数,传入时间增量。
/// </summary>
public override void Tick()
{
base.Tick(); // 调用基类的Tick方法
// 遍历并更新所有健康状态从后向前循环以安全地移除已完成的Hediff
for (var i = hediffs.Count - 1; i >= 0; i--)
{
var hediff = hediffs[i];
hediff.Tick(Time.deltaTime); // 调用单个Hediff的Tick方法
// 检查Hediff是否已达到移除条件
if (hediff.ShouldRemove)
{
RemoveHediff(hediff); // 使用RemoveHediff方法确保OnRemoved被调用并设置_needUpdateAttributes
}
}
}
/// <summary>
/// 添加一个新的健康状态到实体上。
/// </summary>
/// <param name="hediff">要添加的 Hediff 实例。</param>
public void AddHediff(Hediff hediff)
{
if (hediff == null)
{
Debug.LogWarning("尝试向活体实体添加一个空的健康状态Hediff。");
return;
}
hediffs.Add(hediff);
// 通知Hediff它被添加到一个实体上进行初始化等操作并传入自身引用
hediff.OnAdded(this);
_needUpdateAttributes = true; // 添加新Hediff需要更新属性缓存
}
/// <summary>
/// 移除一个特定的健康状态。
/// </summary>
/// <param name="hediff">要移除的 Hediff 实例。</param>
public void RemoveHediff(Hediff hediff)
{
if (hediff == null)
{
Debug.LogWarning("尝试从活体实体移除一个空的健康状态Hediff。");
return;
}
// 尝试从列表中移除Hediff
if (hediffs.Remove(hediff))
{
// 通知Hediff它被从实体上移除进行清理等操作并传入自身引用
hediff.OnRemoved(this);
_needUpdateAttributes = true; // 移除Hediff需要更新属性缓存
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 440140899cba41b3a023f86e27e69909
timeCreated: 1756632414

View File

@ -4,7 +4,7 @@ using Managers;
namespace Entity
{
public class Monster:Entity
public class Monster:LivingEntity
{
private WeaponResource weapon;
public override void Init(EntityDef entityDef)

View File

@ -1,12 +0,0 @@
using UnityEngine;
namespace Item
{
public class ItemBase
{
public ItemResource resource;
public int count=0;
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 01a2843d5856483fa5b6967e2e01db62
timeCreated: 1755061705

View File

@ -67,24 +67,80 @@ namespace Managers
if (packs.Count > 0)
return;
// 获取所有定义包的文件夹路径
// // 获取所有定义包的文件夹路径
// var packFolder = Configs.ConfigProcessor.GetSubFolders(new(dataSetFilePath));
// foreach (var folder in packFolder)
// {
// var pack = new DefinePack();
// if (pack.LoadPack(folder))
// {
// packs.Add(pack.packID, pack);
// }
// }
var packFolder = Configs.ConfigProcessor.GetSubFolders(new(dataSetFilePath));
// 获取当前的加载顺序
var currentOrder = Base.Setting.Instance.CurrentSettings.loadOrder; // 假设为 string[]
var isFirstLaunch = currentOrder == null || currentOrder.Length == 0;
var newOrder = new List<string>(); // 用于最终写回
foreach (var folder in packFolder)
{
var pack = new DefinePack();
if (pack.LoadPack(folder))
{
// 根据加载顺序设置 priority
int priority;
if (isFirstLaunch)
{
// 第一次启动,按遍历顺序设置 priority
// 暂时使用新增时的顺序作为 priority顺序从 newOrder 的索引确定
priority = newOrder.Count;
// 记录该 pack 的 ID后续写回 loadOrder
newOrder.Add(pack.packID);
}
else
{
// 非首次启动,使用现有 loadOrder 中的位置来设置 priority
var idx = Array.IndexOf(currentOrder, pack.packID);
if (idx >= 0)
{
priority = idx;
}
else
{
// 未出现在现有顺序中,放在末尾
priority = currentOrder.Length;
// 可选:也将其加入 newOrder 以更新 loadOrder
}
}
pack.priority = priority;
packs.Add(pack.packID, pack);
}
}
// 如果是第一次启动,写回 loadOrder顺序为 newOrder
if (isFirstLaunch)
{
// 将 newOrder 转换为 string[],并写回设置
Base.Setting.Instance.CurrentSettings.loadOrder = new string[newOrder.Count];
newOrder.CopyTo(Base.Setting.Instance.CurrentSettings.loadOrder);
// 可能需要保存设置到磁盘/持久化
// Example: Settings.Save(Base.Setting.Instance.CurrentSettings);
}
// 字段信息缓存,用于优化反射性能。
Dictionary<Type, FieldInfo[]> fieldCache = new();
// 存储需要进行链接的定义引用信息。
// Tuple的元素依次代表被引用的定义Define引用该定义的字段FieldInfo以及引用占位符Define
List<Tuple<Define, FieldInfo, Define>> defineCache = new();
string currentPackID;
// 递归处理定义对象及其内部的嵌套定义和引用。
@ -100,7 +156,7 @@ namespace Managers
{
// 获取所有公共实例字段。
defineFields = def.GetType()
.GetFields(BindingFlags.Public | BindingFlags.Instance);
.GetFields(BindingFlags.Public | BindingFlags.Instance);
// 缓存当前类型的字段信息。
fieldCache[def.GetType()] = defineFields;
@ -130,6 +186,7 @@ namespace Managers
anonymousDefines.Add(typeName, new List<Define>());
anonymousDefines[typeName].Add(defRef);
}
ProcessDefine(defRef);
}
}
@ -181,7 +238,7 @@ namespace Managers
{
if (!defines.ContainsKey(typeName))
defines[typeName] = new Dictionary<string, Define>();
foreach (var def in defList)
{
defines[typeName][def.defName] = def;
@ -208,7 +265,8 @@ namespace Managers
var value = FindDefine(defRef.Item3.description, defRef.Item3.defName);
if (value == null)
{
Debug.LogError($"未找到引用,出错的定义:定义类型:{defRef.Item1.GetType().Name}, 定义名:{defRef.Item1.defName} ; 类型:{defRef.Item3.description}, 定义名:{defRef.Item3.defName}");
Debug.LogError(
$"未找到引用,出错的定义:定义类型:{defRef.Item1.GetType().Name}, 定义名:{defRef.Item1.defName} ; 类型:{defRef.Item3.description}, 定义名:{defRef.Item3.defName}");
continue;
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Entity;
namespace Managers
{
public class SaveManager:Utils.Singleton<SaveManager>
{
public List<Hediff> baseHediffs = new();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: daa6e65f38a5495492e994e89eba53f4
timeCreated: 1756655387

View File

@ -0,0 +1,138 @@
// Base/BaseAnimator.cs
using System;
using UnityEngine;
using Base; // 假设ITick接口在这个命名空间
namespace Base
{
// 抽象基类,封装通用动画逻辑
public abstract class BaseAnimator : MonoBehaviour, ITick
{
// 通用公开字段(可在编辑器中设置)
[SerializeField] protected Sprite[] _sprites; // 动画精灵序列
[SerializeField] protected float _fps = 2; // 每秒帧数
[SerializeField] protected Sprite _staticSprite; // 暂停时的静态精灵
// 通用内部状态
protected bool _isPaused; // 暂停状态
protected float _frameTimer; // 帧计时器
protected int _currentFrameIndex; // 当前帧索引
// 抽象方法:子类必须实现以获取并验证其特有的显示组件
protected abstract void ValidateComponent();
// 抽象方法子类必须实现以设置实际显示组件的Sprite
protected abstract void SetDisplaySprite(Sprite sprite);
protected virtual void Awake()
{
ValidateComponent(); // 子类获取组件
ValidateStartFrame(); // 初始化第一帧
}
// ITick接口实现
public void Tick()
{
var deltaTime = Time.deltaTime;
if (_isPaused)
{
HandlePausedState();
return;
}
PlayAnimation(deltaTime);
}
protected void ValidateStartFrame()
{
// 确保有精灵时可显示有效帧
if (_sprites != null && _sprites.Length > 0)
{
_currentFrameIndex = Mathf.Clamp(_currentFrameIndex, 0, _sprites.Length - 1);
SetDisplaySprite(_sprites[_currentFrameIndex]); // 调用抽象方法设置Sprite
}
else
{
SetDisplaySprite(null); // 调用抽象方法清空Sprite
}
}
protected void HandlePausedState()
{
// 优先使用静态精灵,否则保持当前帧
if (_staticSprite)
{
SetDisplaySprite(_staticSprite); // 调用抽象方法设置Sprite
}
// 否则保持当前显示的Sprite不需要额外操作SetDisplaySprite已在NextFrame中设置
}
protected void PlayAnimation(float deltaTime)
{
if (_sprites == null || _sprites.Length == 0)
{
// 如果没有精灵确保显示组件的Sprite被清除
SetDisplaySprite(null);
return;
}
// 更新帧计时器
_frameTimer += deltaTime;
var frameDuration = 1f / _fps;
// 检查帧切换条件
while (_frameTimer >= frameDuration)
{
_frameTimer -= frameDuration;
NextFrame();
}
}
protected void NextFrame()
{
if (_sprites == null || _sprites.Length == 0) return;
// 循环播放动画
_currentFrameIndex = (_currentFrameIndex + 1) % _sprites.Length;
SetDisplaySprite(_sprites[_currentFrameIndex]); // 调用抽象方法更新Sprite
}
// 外部控制方法
public void SetPaused(bool paused) => _isPaused = paused;
public void SetSprites(Sprite[] newSprites)
{
_sprites = newSprites;
// 如果有新的精灵数组,则立即显示第一帧
if (_sprites != null && _sprites.Length > 0)
{
_currentFrameIndex = 0; // 重置当前帧索引为第一帧
SetDisplaySprite(_sprites[_currentFrameIndex]); // 立即显示第一帧
}
else
{
SetDisplaySprite(null); // 如果没有精灵,则清空渲染器
}
// 重置帧计时器,以确保从头开始播放
_frameTimer = 0f;
}
public void Restore()
{
_currentFrameIndex = 0;
if (_sprites != null && _sprites.Length > 0)
{
SetDisplaySprite(_sprites[_currentFrameIndex]); // 恢复到第一帧
}
else
{
SetDisplaySprite(null);
}
}
public void SetFPS(float newFPS) => _fps = Mathf.Max(0.1f, newFPS);
public void SetStaticSprite(Sprite sprite) => _staticSprite = sprite;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84dda25302c44253949e7818cd62b7e7
timeCreated: 1756778358

View File

@ -0,0 +1,10 @@
using UI;
using UnityEngine;
namespace Prefab
{
public class BuffIconUI: MonoBehaviour
{
public UIImageAnimator icon;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17587b13f9d4467dbff77cf9762dc8fe
timeCreated: 1756780876

View File

@ -1,115 +1,36 @@
using System;
using Base;
using UnityEngine;
using Base; // 引入Base命名空间以使用BaseAnimator
namespace Prefab
{
[RequireComponent(typeof(SpriteRenderer))]
public class SpriteAnimator : MonoBehaviour, ITick
public class SpriteAnimator : BaseAnimator
{
// 公开字段(可在编辑器中设置)
[SerializeField] private Sprite[] _sprites; // 动画精灵序列
[SerializeField] private float _fps = 2; // 每秒帧数
[SerializeField] private Sprite _staticSprite; // 暂停时的静态精灵
private SpriteRenderer _renderer; // 渲染器组件
private bool _isPaused; // 暂停状态
private float _frameTimer; // 帧计时器
private int _currentFrameIndex; // 当前帧索引
private void Awake()
protected override void ValidateComponent()
{
_renderer = GetComponent<SpriteRenderer>();
ValidateStartFrame();
}
// ITick接口实现
public void Tick()
{
var deltaTime=Time.deltaTime;
if (_isPaused)
if (_renderer == null)
{
HandlePausedState();
return;
}
PlayAnimation(deltaTime);
}
private void ValidateStartFrame()
{
// 确保有精灵时可显示有效帧
if (_sprites != null && _sprites.Length > 0)
{
_currentFrameIndex = Mathf.Clamp(_currentFrameIndex, 0, _sprites.Length - 1);
_renderer.sprite = _sprites[_currentFrameIndex];
}
else
{
_renderer.sprite = null;
Debug.LogError("SpriteAnimator requires a SpriteRenderer component.", this);
enabled = false; // 禁用脚本如果组件缺失
}
}
private void HandlePausedState()
protected override void SetDisplaySprite(Sprite sprite)
{
// 优先使用静态精灵,否则保持当前帧
if (_staticSprite)
if (_renderer != null)
{
_renderer.sprite = _staticSprite;
_renderer.sprite = sprite;
}
}
private void PlayAnimation(float deltaTime)
{
if (_sprites == null || _sprites.Length == 0) return;
// 更新帧计时器
_frameTimer += deltaTime;
var frameDuration = 1f / _fps;
// 检查帧切换条件
while (_frameTimer >= frameDuration)
{
_frameTimer -= frameDuration;
NextFrame();
}
}
private void NextFrame()
{
// 循环播放动画
_currentFrameIndex = (_currentFrameIndex + 1) % _sprites.Length;
_renderer.sprite = _sprites[_currentFrameIndex];
}
// 外部控制方法
public void SetPaused(bool paused) => _isPaused = paused;
public void SetSprites(Sprite[] newSprites)
{
_sprites = newSprites;
// 如果有新的精灵数组,则立即显示第一帧
if (_sprites != null && _sprites.Length > 0)
{
_currentFrameIndex = 0; // 重置当前帧索引为第一帧
_renderer.sprite = _sprites[_currentFrameIndex]; // 立即显示第一帧
}
else
{
_renderer.sprite = null; // 如果没有精灵,则清空渲染器
}
// 重置帧计时器,以确保从头开始播放
_frameTimer = 0f;
}
public void Restore()
{
_currentFrameIndex = 0;
_renderer.sprite = _sprites[_currentFrameIndex];
}
public void SetFPS(float newFPS) => _fps = Mathf.Max(0.1f, newFPS);
public void SetStaticSprite(Sprite sprite) => _staticSprite = sprite;
// [可选] 如果SpriteAnimator需要任何Awake后特有的初始化逻辑可以在这里重写
// protected override void Awake()
// {
// base.Awake(); // 确保调用基类的Awake
// // 子类特有的初始化
// }
}
}

View File

@ -0,0 +1,38 @@
// UI/UIImageAnimator.cs
using UnityEngine;
using UnityEngine.UI;
using Base; // 引入Base命名空间以使用BaseAnimator
namespace UI
{
[RequireComponent(typeof(Image))]
public class UIImageAnimator : BaseAnimator // 继承BaseAnimator
{
private Image _image; // UI Image组件
protected override void ValidateComponent()
{
_image = GetComponent<Image>();
if (_image == null)
{
Debug.LogError("UIImageAnimator requires an Image component.", this);
enabled = false; // 禁用脚本如果组件缺失
}
}
protected override void SetDisplaySprite(Sprite sprite)
{
if (_image)
{
_image.sprite = sprite;
}
}
// [可选] 如果UIImageAnimator需要任何Awake后特有的初始化逻辑可以在这里重写
// protected override void Awake()
// {
// base.Awake(); // 确保调用基类的Awake
// // 子类特有的初始化
// }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c6f899d1c5ef450bb6f3e670fa55cffd
timeCreated: 1756778168

View File

@ -5,6 +5,9 @@ using Map;
using UnityEngine;
using Utils;
/// <summary>
/// Program 类作为单例模式的核心管理器,负责维护和管理游戏或应用中的维度(Dimension)实例和焦点状态。
/// 它提供了维度注册、注销、获取以及焦点维度设置的功能。

View File

@ -0,0 +1,16 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
public class AttackModeUI:MonoBehaviour
{
public UIImageAnimator icon;
private void Start()
{
icon.gameObject.SetActive(false);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2d81179f82c542edb275daabff5bfe12
timeCreated: 1756781456

View File

@ -0,0 +1,20 @@
using System;
using Prefab;
using UnityEngine;
namespace UI
{
public class BuffIconListUI: MonoBehaviour
{
public BuffIconUI prefab;
public Transform container;
private void Start()
{
foreach (Transform child in container)
{
Destroy(child.gameObject);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8f6f033d6fe84e769e101fb355ef3ffa
timeCreated: 1756780958

View File

@ -0,0 +1,16 @@
using System;
using TMPro;
using UnityEngine;
namespace UI
{
public class CoinCountUI:MonoBehaviour
{
public TMP_Text text;
private void Start()
{
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 85cfb4a924a04c6e9f6f7801207dbee3
timeCreated: 1756776391

View File

@ -1,275 +1,15 @@
using System.Collections.Generic;
using Base;
using Entity;
using UnityEngine;
// 确保 Character 类在此命名空间下
namespace UI
{
/// <summary>
/// 负责管理和显示角色的装备用户界面。
/// 该组件会监听当前关注实体的变化,并根据所关注角色的库存数据动态更新装备槽位的显示,
/// 同时采用对象池技术高效管理 ItemUI 实例的创建和复用。
/// 除了显示,现在还支持通过滚轮选择物品,并同步更新焦点角色的 CurrentSelected 字段。
/// </summary>
public class EquipmentUI : MonoBehaviour, ITick
public class EquipmentUI : MonoBehaviour
{
[SerializeField]
[Tooltip("所有 ItemUI 实例的父级 GameObject用于布局。")]
private GameObject uiParent;
[SerializeField]
[Tooltip("用于实例化装备槽位的 ItemUI 预制件。")]
private ItemUI itemUIPrefab;
/// <summary>
/// 当前界面所关联和关注的角色实体。
/// </summary>
private Character focusedEntity;
/// <summary>
/// ItemUI 实例的对象池,用于高效管理和复用 ItemUI。
/// </summary>
private List<ItemUI> itemUIPool = new();
/// <summary>
/// MonoBehaviour 的 Start 生命周期方法。
/// 在此方法中,注册当游戏主要程序中关注的实体发生变化时,调用 <see cref="UpdateFocusedEntity"/> 方法进行更新。
/// </summary>
private void Start()
{
Program.Instance.OnFocusedEntityChanged += UpdateFocusedEntity;
uiParent.SetActive(false);
}
/// <summary>
/// MonoBehaviour 的 OnDestroy 生命周期方法。
/// 在此方法中,取消注册所有已订阅的事件监听器,并清理对象池中创建的所有 ItemUI 实例,
/// 以防止内存泄漏和不必要的引用。
/// </summary>
private void OnDestroy()
{
Program.Instance.OnFocusedEntityChanged -= UpdateFocusedEntity;
// 如果当前有关注的角色,取消注册其库存改变事件。
// 确保 focusedEntity 不为 null 且 Inventory 不为 null。
if (focusedEntity != null && focusedEntity.Inventory != null)
{
focusedEntity.Inventory.OnInventoryChanged -= UpdateUI;
}
// 销毁对象池中所有 ItemUI 的 GameObject。
foreach (var itemUI in itemUIPool)
{
if (itemUI != null && itemUI.gameObject != null)
{
Destroy(itemUI.gameObject);
}
}
itemUIPool.Clear();
}
/// <summary>
/// 当游戏程序中关注的实体发生变化时调用此方法。
/// 该方法会更新当前 EquipmentUI 所关联的角色,并相应地注册或取消注册库存改变事件。
/// </summary>
/// <param name="entity">新的关注实体,可能为 null 或非 Character 类型。</param>
private void UpdateFocusedEntity(Entity.Entity entity)
{
// 如果之前有关注的角色,先取消注册其库存改变事件。
// 确保 focusedEntity 不为 null 且 Inventory 不为 null。
if (focusedEntity != null && focusedEntity.Inventory != null)
{
focusedEntity.Inventory.OnInventoryChanged -= UpdateUI;
}
// 尝试将新的实体转换为角色类型。
var newCharacter = entity as Character;
if (newCharacter != null)
{
focusedEntity = newCharacter;
}
else
{
// 如果传入的 entity 不是 Character 类型,或者为 null则清除当前的 focusedEntity。
focusedEntity = null;
}
// 如果现在有关注的角色,注册其库存改变事件。
if (focusedEntity != null)
{
focusedEntity.Inventory.OnInventoryChanged += UpdateUI;
}
// 立即更新UI以反映新的关注实体或没有关注实体的状态。
UpdateUI();
// 在更新UI后确保UI的选中状态与角色当前选中字段同步
// 只有当有焦点角色且库存不为空时才更新选中状态
if (focusedEntity != null && focusedEntity.Inventory != null && focusedEntity.Inventory.Capacity > 0)
{
// 确保 CurrentSelected 在有效范围内否则重置为0
if (focusedEntity.CurrentSelected < 0 || focusedEntity.CurrentSelected >= focusedEntity.Inventory.Capacity)
{
focusedEntity.CurrentSelected = 0;
}
UpdateSelectionUI(focusedEntity.CurrentSelected);
}
else
{
// 如果没有焦点实体或库存为空,则清空所有选中状态
UpdateSelectionUI(-1); // 传入一个无效索引以取消所有选中
}
}
/// <summary>
/// 根据当前关注角色的库存数据更新装备UI的显示。
/// 该方法通过对象池机制高效地管理 ItemUI 实例的创建、复用和禁用。
/// </summary>
private void UpdateUI()
{
// 如果没有关注的角色或其库存,则禁用所有 ItemUI。
if (focusedEntity == null || focusedEntity.Inventory == null)
{
foreach (var itemUI in itemUIPool)
{
if (itemUI != null && itemUI.gameObject != null)
{
itemUI.gameObject.SetActive(false);
}
}
uiParent.SetActive(false);
return;
}
// 检查用于创建物品UI的预制件是否已在 Inspector 中赋值。
if (itemUIPrefab == null)
{
Debug.LogError("ItemUIPrefab 未在 EquipmentUI 中指定。无法创建物品用户界面。", this);
foreach (var itemUI in itemUIPool) itemUI.gameObject.SetActive(false);
uiParent.SetActive(false); // 确保父级也被禁用
return;
}
var requiredUIs = focusedEntity.Inventory.Capacity;
var currentUIPoolSize = itemUIPool.Count; // 当前对象池中 ItemUI 实例的总数。
// 遍历所有必要的物品槽位,复用对象池中的 ItemUI或在不足时创建新的 ItemUI。
for (var i = 0; i < requiredUIs; i++)
{
ItemUI itemUI;
if (i < currentUIPoolSize)
{
itemUI = itemUIPool[i];
}
else
{
// 使用 Instantiate(GameObject, Transform) 以确保父级设置正确且避免转换问题。
var itemObj = Instantiate(itemUIPrefab.gameObject, uiParent.transform);
itemUI = itemObj.GetComponent<ItemUI>();
itemUIPool.Add(itemUI);
currentUIPoolSize++; // 更新池的大小计数。
}
// 确保 ItemUI GameObject 处于激活状态,并使用当前物品槽位的数据进行初始化。
itemUI.gameObject.SetActive(true);
itemUI.Init(focusedEntity.Inventory.GetSlot(i), i);
// 移除此处 itemUI.Select = false; 选中状态将由 UpdateSelectionUI 统一管理
}
// 如果库存槽位数量减少,禁用对象池中多余的 ItemUI 实例。
for (var i = requiredUIs; i < currentUIPoolSize; i++)
{
if (itemUIPool[i] != null && itemUIPool[i].gameObject != null)
{
itemUIPool[i].gameObject.SetActive(false);
itemUIPool[i].Select = false; // 禁用时也确保清除选中状态
}
}
uiParent.SetActive(true);
// 首次更新UI时或者当Inventory改变时需要确保 CurrentSelected 的UI状态是正确的
// 但如果 UpdateFocusedEntity 已经处理了,这里可以省略,或者确保只在必要时调用
// 考虑到 UpdateUI 也会被 Inventory.OnInventoryChanged 调用,这里再次确保同步是合理的。
if (focusedEntity != null && focusedEntity.Inventory != null && focusedEntity.Inventory.Capacity > 0)
{
UpdateSelectionUI(focusedEntity.CurrentSelected);
}
else
{
UpdateSelectionUI(-1);
}
}
/// <summary>
/// 当当前选中物品改变时,更新所有 ItemUI 的选中状态。
/// </summary>
/// <param name="selectedItemIndex">当前选中的物品索引。传入 -1 将取消所有 ItemUI 的选中状态。</param>
private void UpdateSelectionUI(int selectedItemIndex)
{
// 如果对象池为空,则无需更新
if (itemUIPool == null || itemUIPool.Count == 0)
{
return;
}
for (var i = 0; i < itemUIPool.Count; i++)
{
var itemUI = itemUIPool[i];
if (itemUI != null && itemUI.gameObject != null)
{
// 只有在 ItemUI 激活状态下才设置其选中状态避免对禁用UI的操作
// 或者如果传入-1即使激活也全部设置为false
if (itemUI.gameObject.activeSelf || selectedItemIndex == -1) // 确保当取消所有选中时循环到所有激活的甚至当前禁用的ItemUI
{
itemUI.Select = (i == selectedItemIndex && itemUI.gameObject.activeSelf);
}
}
}
}
/// <summary>
/// 每帧调用的更新方法,用于处理滚轮输入以选择物品。
/// </summary>
public void Tick()
{
// 如果没有焦点实体、没有库存或库存为空,不进行选择操作
if (focusedEntity == null || focusedEntity.Inventory == null || focusedEntity.Inventory.Capacity == 0)
{
return;
}
var scrollInput = Input.GetAxis("Mouse ScrollWheel");
if (scrollInput != 0) // 检测到滚轮输入
{
var currentSelection = focusedEntity.CurrentSelected;
var inventoryCapacity = focusedEntity.Inventory.Capacity;
if (scrollInput > 0) // 滚轮向上,选择前一个
{
currentSelection--;
if (currentSelection < 0)
{
currentSelection = inventoryCapacity - 1; // 循环到最后一个
}
}
else // 滚轮向下,选择后一个
{
currentSelection++;
if (currentSelection >= inventoryCapacity)
{
currentSelection = 0; // 循环到第一个
}
}
// 如果选择发生变化则更新焦点实体和UI
if (focusedEntity.CurrentSelected != currentSelection)
{
focusedEntity.CurrentSelected = currentSelection;
UpdateSelectionUI(currentSelection); // 更新UI选中状态
}
}
}
public ItemUI currentUse;
public ItemUI two;
public ItemUI three;
}
}

View File

@ -1,91 +1,34 @@
using System;
using Base;
using TMPro;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UI
{
public class ItemUI:MonoBehaviour,IPointerEnterHandler,IPointerExitHandler,IPointerClickHandler,ITick
public class ItemUI:MonoBehaviour
{
[SerializeField] private Image textureUI;
[SerializeField] private TMP_Text countUI;
[SerializeField] private TMP_Text nameUI;
[SerializeField] private GameObject selectedOutline;
private Entity.InventorySlot _item;
private float timer = 0;
private float switchTime = 0;
private int texturePtr = 0;
public event Action OnPlayerSelect;
public bool Select
public Entity.InventorySlot inventorySlot;
public UIImageAnimator icon;
private void Start()
{
get => selectedOutline.activeSelf;
set => selectedOutline.SetActive(value);
icon.gameObject.SetActive(false);
}
public int SlotIndex { get; private set; } = -1;
public void Init(Entity.InventorySlot item,int index)
public void SetDisplayItem(Entity.InventorySlot slot)
{
if (item == null)
inventorySlot = slot;
if (inventorySlot == null)
{
switchTime = -1;
textureUI.gameObject.SetActive(false);
countUI.text = "";
nameUI.text = "";
return;
}
textureUI.gameObject.SetActive(true);
_item = item;
textureUI.sprite = item.Item.Icon[0];
countUI.text = item.Quantity.ToString();
nameUI.text = item.Item.Name;
nameUI.gameObject.SetActive(false);
Select = false;
SlotIndex = index;
if (item.Item.FPS > 0)
{
switchTime = 1f / item.Item.FPS;
icon.gameObject.SetActive(false);
}
else
{
switchTime = 0;
icon.gameObject.SetActive(true);
icon.SetSprites(inventorySlot.Item.Icon.ToArray());
}
}
public void OnPointerEnter(PointerEventData eventData)
{
nameUI.gameObject.SetActive(true);
}
public void OnPointerExit(PointerEventData eventData)
{
nameUI.gameObject.SetActive(false);
}
public void OnPointerClick(PointerEventData eventData)
{
OnPlayerSelect?.Invoke();
}
public void Tick()
{
if (switchTime > 0)
{
timer+=Time.deltaTime;
if (timer >= switchTime)
{
timer-=switchTime;
texturePtr++;
texturePtr%=_item.Item.Icon.Count;
textureUI.sprite=_item.Item.Icon[texturePtr];
}
}
}
}
}

View File

@ -6,7 +6,7 @@ using UnityEngine;
namespace UI
{
public class LogUI : UIBase
public class LogUI : FullScreenUI
{
public Transform contentPanel; // 日志内容容器
public TextPrefab textPrefab; // 文本预制体引用

View File

@ -1,4 +1,6 @@
using System;
using Base;
using Map;
using TMPro;
using UnityEngine;
@ -8,7 +10,13 @@ namespace UI
{
[SerializeField] private BarUI focusedEntityHP;
[SerializeField] private BarUI lastEntityHP;
[SerializeField] private BarUI BaseBuildingHP;
[SerializeField] private MiniMap miniMap;
[SerializeField] private EquipmentUI equipmentUI;
[SerializeField] private CoinCountUI coinCountUI;
[SerializeField] private BuffIconListUI focuseEntityBuffIconList;
[SerializeField] private BuffIconListUI lastEntityBuffIconList;
[SerializeField] private AttackModeUI attackMode;
public void Tick()
{
@ -19,6 +27,53 @@ namespace UI
(float)focusedEntity.attributes.health / focusedEntity.entityDef.attributes.health;
}
}
public void Show()
{
focusedEntityHP.gameObject.SetActive(true);
lastEntityHP.gameObject.SetActive(true);
BaseBuildingHP.gameObject.SetActive(true);
miniMap.gameObject.SetActive(true);
equipmentUI.gameObject.SetActive(true);
coinCountUI.gameObject.SetActive(true);
focuseEntityBuffIconList.gameObject.SetActive(true);
lastEntityBuffIconList.gameObject.SetActive(true);
attackMode.gameObject.SetActive(true);
}
public void Hide()
{
focusedEntityHP.gameObject.SetActive(false);
lastEntityHP.gameObject.SetActive(false);
BaseBuildingHP.gameObject.SetActive(false);
miniMap.gameObject.SetActive(false);
equipmentUI.gameObject.SetActive(false);
coinCountUI.gameObject.SetActive(false);
focuseEntityBuffIconList.gameObject.SetActive(false);
lastEntityBuffIconList.gameObject.SetActive(false);
attackMode.gameObject.SetActive(false);
}
private void Start()
{
UIInputControl.Instance.OnWindowVisibilityChanged += UIChange;
}
private void OnDestroy()
{
UIInputControl.Instance.OnWindowVisibilityChanged -= UIChange;
}
private void UIChange(UIBase ui, bool open)
{
if (ui.exclusive && open)
{
Hide();
}
else
{
Show();
}
}
}
}