(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

@@ -9,10 +9,10 @@ namespace Entity
private BuildingOutline buildingOutline;
private BuildingDef buildingDef;
public override void Init(EntityDef entityDef)
public override void Init(EntityDef entityDefine)
{
base.Init(entityDef);
buildingDef = entityDef as BuildingDef;
base.Init(entityDefine);
buildingDef = entityDefine as BuildingDef;
buildingOutline = entityPrefab.outline as BuildingOutline;
}

View File

@@ -40,7 +40,7 @@ namespace Entity
trigger.offset = position;
}
tip.transform.localPosition += new Vector3(0f, textureSize.y * 2 / 3, 0f);
// tip.transform.localPosition += new Vector3(0f, textureSize.y * 2 / 3, 0f);
tip.text = $"按{buildingDef.activateKey}打开{buildingDef.label}\n{buildingDef.description}";
tip.gameObject.SetActive(false);

View File

@@ -14,23 +14,25 @@ namespace Entity
public void SetBulletSource(Entity source)
{
bulletSource = source;
attributes.attack = source.attributes.attack;
AttributesNow.attack = source.AttributesNow.attack;
var weapon = source.GetCurrentWeapon();
if (weapon != null)
{
lifeTime = weapon.Attributes.attackRange / attributes.moveSpeed;
lifeTime = weapon.Attributes.attackRange / AttributesNow.moveSpeed;
}
affiliation = source.affiliation;
}
public override void SetTarget(Vector3 pos)
{
base.SetTarget(pos);
Utils.RotateTool.RotateTransformToDirection(transform, direction);
Utils.RotateTool.RotateTransformToDirection(transform, Direction);
}
protected override void AutoBehave()
{
TryMove();
Move();
lifeTime -= Time.deltaTime;
if (lifeTime <= 0)
{
@@ -41,7 +43,12 @@ namespace Entity
private void OnTriggerEnter2D(Collider2D other)
{
var entity = other.GetComponent<Entity>();
if (IsDead||!entity || entity == bulletSource || entity is Pickup) return;
if (!entity)
{
Kill();
return;
}
if (IsDead || entity == bulletSource || entity is Pickup) return;
if (Managers.AffiliationManager.Instance.GetRelation(bulletSource.affiliation, entity.affiliation) != Relation.Friendly || Setting.Instance.CurrentSettings.friendlyFire)
{
entity.OnHit(this);
@@ -51,7 +58,7 @@ namespace Entity
return; // 如果是友好关系且不允许友军伤害,则不处理
}
attributes.health -= 1;
AttributesNow.health -= 1;
}

View File

@@ -1,5 +1,8 @@
using System;
using Configs;
using Data;
using Item;
using Managers;
using UnityEngine;
// 添加 System 命名空间以使用 Action
@@ -36,23 +39,34 @@ namespace Entity
get => _currentSelected;
set
{
var maxIndex = Inventory != null && Inventory.Capacity > 0 ? Inventory.Capacity - 1 : 0;
var maxIndex = PlayerInventory != null && PlayerInventory.Capacity > 0 ? PlayerInventory.Capacity - 1 : 0;
var clampedValue = Mathf.Clamp(value, 0, maxIndex);
_currentSelected = clampedValue;
InitWeaponAnimator();
}
}
public Inventory Inventory { get; private set; }
public Inventory PlayerInventory { get; private set; }
public InventorySlot Coin{get;private set;}
public override void Init(EntityDef entityDef)
public override void Init(EntityDef entityDefine)
{
Inventory = new Inventory(this, 3);
Inventory.OnInventoryChanged += InventoryChange;
PlayerInventory = new Inventory(this, 3);
var coinItem = ItemResourceManager.Instance.GetItem(ConfigManager.Instance.GetValue<string>("CoinItem"));
if(!KeyValueArchiveManager.Instance.HasKey("coinCount"))
{
KeyValueArchiveManager.Instance.Set("coinCount", 0);
}
Coin = Program.Instance.OutsidePlayDimension == null
? new InventorySlot(coinItem, Program.Instance.CoinCount)
: new InventorySlot(coinItem, 0);
PlayerInventory.OnInventoryChanged += InventoryChange;
CurrentSelected = 0;
base.Init(entityDef);
base.Init(entityDefine);
}
/// <summary>
/// 尝试将指定物品添加到角色的背包中。
@@ -67,21 +81,21 @@ namespace Entity
/// </returns>
public int TryPickupItem(ItemResource itemResource, int quantity)
{
if (Inventory == null)
if (PlayerInventory == null)
{
Debug.LogError($"Character '{name}' inventory is not initialized. Cannot pickup item.");
return quantity; // 如果背包未初始化,则视为未能添加任何物品
}
var remainingQuantity = Inventory.AddItem(itemResource, quantity);
var remainingQuantity = PlayerInventory.AddItem(itemResource, quantity);
return remainingQuantity;
}
public override WeaponResource GetCurrentWeapon()
{
var currentSelectItem = Inventory.GetSlot(CurrentSelected);
var currentSelectItem = PlayerInventory.GetSlot(CurrentSelected);
return currentSelectItem?.Item as WeaponResource;
}

View File

@@ -33,19 +33,19 @@ namespace Entity
/// <summary>
/// 人工智能行为树,定义实体的行为逻辑。
/// </summary>
public AIBase aiTree;
public BehaviorTreeBase BehaviorTreeTree { get; private set; }
/// <summary>
/// 当前实体正在执行的任务。
/// </summary>
public JobBase currentJob;
// /// <summary>
// /// 当前实体正在执行的任务。
// /// </summary>
// public JobBase currentJob;
private Attributes _attribute;
/// <summary>
/// 实体的属性定义,包括生命值、攻击力、防御力等。
/// </summary>
public virtual Attributes attributes
public virtual Attributes AttributesNow
{
get { return _attribute ??= new Attributes(baseAttributes); }
protected set => _attribute = value;
@@ -62,10 +62,28 @@ namespace Entity
}
}
private Vector3 _direction;
/// <summary>
/// 实体当前的移动方向。
/// </summary>
public Vector3 direction;
public Vector3 Direction
{
get => _direction;
set
{
_direction = Mathf.Approximately(value.sqrMagnitude, 1) ? value : value.normalized;
Orientation ori;
if (Mathf.Abs(Direction.y) > Mathf.Abs(Direction.x))
{
ori = Direction.y > 0 ? Orientation.Up : Orientation.Down;
}
else
{
ori = Direction.x > 0 ? Orientation.Right : Orientation.Left;
}
SetBodyTexture(CurrentState, ori);
}
}
public Vector3 attackDirection;
@@ -87,7 +105,7 @@ namespace Entity
public string currentDimensionId;
public Vector2 Size => entityPrefab.outline.GetColliderSize();
/// <summary>
/// 表示实体是否由玩家控制。
@@ -98,7 +116,7 @@ namespace Entity
{
if (value)
{
currentJob = null;
BehaviorTreeTree?.Reset();
if (Program.Instance.FocusedEntity && Program.Instance.FocusedEntity != this)
{
Program.Instance.FocusedEntity.PlayerControlled = false;
@@ -127,7 +145,7 @@ namespace Entity
/// <summary>
/// 表示实体是否已经死亡(生命值小于等于零)。
/// </summary>
public bool IsDead => attributes.health <= 0;
public bool IsDead => AttributesNow.health <= 0;
public bool IsShowingHealthBarUI => _hitBarUIShowTimer > 0;
public bool IsAttacking => _attackTimer > 0;
@@ -135,12 +153,7 @@ namespace Entity
private float _attackDetectionTime;
private WeaponResource currentAttackWeapon;
/// <summary>
/// 当实体受到伤害时触发的事件。
/// 可以订阅此事件来响应实体的生命值变化例如更新UI或播放受击特效。
/// </summary>
public event Action<EntityHitEventArgs> OnEntityHit;
/// <summary>
/// 当实体死亡时触发的事件。
/// 只在实体首次进入死亡状态时触发一次。
@@ -157,7 +170,7 @@ namespace Entity
/// <summary>
/// 当前实体的朝向。
/// </summary>
public Orientation CurrentOrientation { get; private set; } = Orientation.Down;
public Orientation CurrentOrientation { get; private set; } = Orientation.Up;
/// <summary>
/// 当前实体的状态
@@ -165,7 +178,18 @@ namespace Entity
public EntityState CurrentState { get; private set; } = EntityState.Idle;
public AudioSource Audio;
public Vector3 moveTarget;
private const float TargetReachThresholdSquared = 0.001f;
public virtual bool OnTargetPoint
{
get
{
var offset = Position - moveTarget;
var distanceSquared = offset.sqrMagnitude;
return distanceSquared <= TargetReachThresholdSquared;
}
}
@@ -186,25 +210,27 @@ namespace Entity
[SerializeField] private float _hitBarUIShowTime = 5;
private float _hitBarUIShowTimer;
private int _walkingTimer;
protected int _walkingTimer;
private List<(Func<Entity, bool>,string)> _conditionalEvents=new();
/// <summary>
/// 初始化实体的基本属性和行为树。
/// </summary>
/// <param name="entityDef">实体的定义数据。</param>
public virtual void Init(EntityDef entityDef)
/// <param name="entityDefine">实体的定义数据。</param>
public virtual void Init(EntityDef entityDefine)
{
attributes = new Attributes(entityDef.attributes);
aiTree = BehaviorTree.ConvertToAIBase(entityDef.behaviorTree);
affiliation = entityDef.affiliation?.defName;
InitBody(entityDef.drawingOrder);
this.entityDef = entityDef;
AttributesNow = new Attributes(entityDefine.attributes);
affiliation = entityDefine.affiliation?.defName;
InitBody(entityDefine.drawingOrder);
entityDef = entityDefine;
BehaviorTreeTree = BehaviorTreeUtils.ConvertToAIBase(entityDefine.behaviorTree);
BehaviorTreeTree?.Init(entityDefine.behaviorTree, this);
HideHealthBar();
InitWeaponAnimator();
InitConditionalEvents(entityDef.eventDef?.GetAllConditionalEvents());
InitConditionalEvents(entityDefine.eventDef?.GetAllConditionalEvents());
}
protected virtual void InitWeaponAnimator()
@@ -316,7 +342,6 @@ namespace Entity
obj.SetActive(false);
}
}
SetBodyTexture(EntityState.Idle, Orientation.Down); // 激活默认朝向
}
@@ -406,21 +431,20 @@ namespace Entity
/// <summary>
/// 尝试攻击目标实体。
/// </summary>
public virtual void TryAttack() // 使用override允许子类重写
public virtual bool TryAttack() // 使用override允许子类重写
{
if (IsAttacking || IsDead) return; // 死亡时无法攻击
if (IsAttacking || IsDead) return false; // 死亡时无法攻击
// 尝试获取当前武器
var currentWeapon = GetCurrentWeapon();
// 如果没有武器,可以选择进行徒手攻击或者直接返回
// 暂时设定为:如果没有武器,则不进行攻击
if (currentWeapon == null)
{
return;
return false;
}
StartAttack(currentWeapon);
return true;
}
private void StartAttack(WeaponResource weaponResource)
@@ -451,6 +475,8 @@ namespace Entity
public void SetBodyTexture(EntityState state, Orientation orientation)
{
if (state == CurrentState && orientation == CurrentOrientation)
return;
HideCurrentBodyTexture();
if (IsAttacking && currentAttackWeapon is { UseEntityAttackAnimation: false })
return;
@@ -476,17 +502,58 @@ namespace Entity
}
/// <summary>
/// 根据方向尝试移动实体。
/// 根据方向尝试移动实体,并避免超调目标点
/// </summary>
public virtual void TryMove()
{
// if (IsAttacking)
// return;
transform.position += direction * (attributes.moveSpeed * Time.deltaTime);
if (OnTargetPoint)
{
return; // 已到达目标点,无需进一步移动。
}
var currentPosition = Position; // 假设 Position 是获取当前位置的属性
var targetDestination = moveTarget; // 假设 moveTarget 是目标位置字段
var directionToTarget = targetDestination - currentPosition;
// 逻辑修改1计算平方距离避免开方。
var sqrDistanceToTarget = directionToTarget.sqrMagnitude;
// 逻辑修改2将到达目标点的判断改为使用平方距离并包含等于阈值的情况避免开方。
if (sqrDistanceToTarget <= TargetReachThresholdSquared)
{
transform.position = targetDestination; // 假设 transform 是 Transform 组件
return;
}
var maxMoveDistance = AttributesNow.moveSpeed * Time.deltaTime; // 假设 AttributesNow 是包含 moveSpeed 的属性
// 逻辑修改4计算最大移动距离的平方用于后续与平方距离的比较避免开方。
var maxMoveDistanceSquared = maxMoveDistance * maxMoveDistance;
Vector3 newPosition;
bool willReachTargetThisFrame;
// 逻辑修改5直接比较平方距离来确定是否能到达目标优化了之前的Mathf.Min和复杂判断。
if (maxMoveDistanceSquared >= sqrDistanceToTarget)
{
newPosition = targetDestination; // 精确捕捉到目标点
willReachTargetThisFrame = true;
}
else
{
newPosition = currentPosition + Direction * maxMoveDistance;
willReachTargetThisFrame = false;
}
transform.position = newPosition;
// 如果已到达目标,则不设置行走动画和重置计时器
if (willReachTargetThisFrame) return;
SetBodyTexture(EntityState.Walking, CurrentOrientation); // 假设 SetBodyTexture 是设置动画的方法
_walkingTimer = 2; // 假设 _walkingTimer 是一个计时器
}
public virtual void Move()
{
transform.position += Direction * (AttributesNow.moveSpeed * Time.deltaTime);
SetBodyTexture(EntityState.Walking, CurrentOrientation);
_walkingTimer = 2;
}
/// <summary>
/// 处理实体受到攻击的逻辑。
/// </summary>
@@ -498,22 +565,17 @@ namespace Entity
{
return;
}
var hit = from.attributes.attack - attributes.defense;
var hit = from.AttributesNow.attack - AttributesNow.defense;
if (hit < 0)
hit = from.attributes.attack / 100;
hit = from.AttributesNow.attack / 100;
// 确保伤害不为负最小为0
hit = Mathf.Max(0, hit);
attributes.health -= hit;
AttributesNow.health -= hit;
var wasFatal = IsDead; // 检查这次攻击是否导致实体死亡
// 触发 OnEntityHit 事件
OnEntityHit?.Invoke(new EntityHitEventArgs(
this, from, hit, attributes.health, entityDef.attributes.health, wasFatal));
currentJob?.StopJob();
BehaviorTreeTree?.Reset();
if (wasFatal)
{
// 如果是首次死亡,则触发 OnEntityDied 事件
// MODIFIED: 停止所有活动,包括当前工作
currentJob = null; // 清除当前工作
OnEntityDied?.Invoke(this);
}
@@ -533,7 +595,7 @@ namespace Entity
if (!healthBarPrefab)
return;
healthBarPrefab.gameObject.SetActive(true);
healthBarPrefab.Progress = (float)attributes.health / entityDef.attributes.health;
healthBarPrefab.Progress = (float)AttributesNow.health / entityDef.attributes.health;
_hitBarUIShowTimer = _hitBarUIShowTime;
}
@@ -554,10 +616,8 @@ namespace Entity
return;
}
attributes.health = 0; // 直接设置生命值为0
// MODIFIED: 停止所有活动,包括当前工作
currentJob?.StopJob();
currentJob = null; // 清除当前工作
AttributesNow.health = 0; // 直接设置生命值为0
BehaviorTreeTree?.Reset();
// 触发 OnEntityDied 事件
OnEntityDied?.Invoke(this);
ShowHealthBar();
@@ -569,24 +629,11 @@ namespace Entity
/// <param name="pos">目标位置。</param>
public virtual void SetTarget(Vector3 pos)
{
direction = (pos - transform.position).normalized;
Orientation ori;
// 判断方向向量最接近哪个朝向
if (Mathf.Abs(direction.y) > Mathf.Abs(direction.x))
{
// 垂直方向优先
ori = direction.y > 0 ? Orientation.Up : Orientation.Down;
}
else
{
// 水平方向优先
ori = direction.x > 0 ? Orientation.Right : Orientation.Left;
}
SetBodyTexture(CurrentState, ori);
Direction = (pos - transform.position).normalized;
moveTarget = pos;
if (!PlayerControlled)
{
attackDirection=direction;
attackDirection=Direction;
}
}
@@ -595,26 +642,7 @@ namespace Entity
/// </summary>
protected virtual void AutoBehave()
{
if (aiTree == null)
return;
if (currentJob == null || !currentJob.Running)
{
currentJob = aiTree.GetJob(this);
if (currentJob == null)
{
if (!_warning)
{
Debug.LogWarning($"{GetType().Name}类型的{name}没有分配到任何工作,给行为树末尾添加等待行为,避免由于没有工作导致无意义的反复查找工作导致性能问题");
_warning = true;
}
return;
}
currentJob.StartJob(this);
}
currentJob.Update();
var result=BehaviorTreeTree?.Tick();
}
@@ -626,11 +654,14 @@ namespace Entity
if (Input.GetMouseButton(0))
{
var mousePos = MousePosition.GetWorldPosition();
attackDirection = new Vector3(mousePos.x,mousePos.y) - Position;
attackDirection = new Vector3(mousePos.x, mousePos.y) - Position;
if (weaponItem)
RotateTool.RotateTransformToDirection(weaponItem.transform, attackDirection);
{
var angle= RotateTool.RotateTransformToDirection(weaponItem.transform, attackDirection);
weaponItem.Flip(false,angle is > 90 or <= -90);
}
}
if (Input.GetKeyDown(KeyCode.V))
{
weaponItem.gameObject.SetActive(!weaponItem.gameObject.activeSelf);
@@ -668,14 +699,8 @@ namespace Entity
// 归一化方向向量,确保对角线移动速度一致
inputDirection = inputDirection.normalized;
// 设置目标位置2D 移动Z 轴保持不变)
var targetPosition = transform.position + new Vector3(inputDirection.x, inputDirection.y, 0);
// 调用 SetTarget 方法设置目标位置
SetTarget(targetPosition);
// 调用 TryMove 方法处理实际移动逻辑
TryMove();
Direction = new Vector3(inputDirection.x, inputDirection.y, 0);
Move();
}

View File

@@ -0,0 +1,441 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Entity
{
public enum PathNodeStatus // 新增枚举
{
Unvisited,
InOpenSet,
InClosedSet
}
/// <summary>
/// 表示寻路算法中的一个节点。
/// </summary>
public class PathNode : IComparable<PathNode> // 实现 IComparable 接口
{
public Vector3Int CellCoordinates; // 格子坐标
public Vector3 WorldCoordinates; // 世界坐标 (中心点)
public float GCost; // 从起点到当前节点的实际代价
public float HCost; // 从当前节点到终点的估算代价 (启发式)
public float FCost => GCost + HCost; // 总代价
public PathNode Parent; // 父节点,用于回溯路径
public PathNodeStatus Status; // 新增状态属性
/// <summary>
/// 构造一个新的PathNode实例。
/// </summary>
/// <param name="cellCoords">节点的格子坐标。</param>
/// <param name="worldCoords">节点的世界坐标。</param>
public PathNode(Vector3Int cellCoords, Vector3 worldCoords)
{
CellCoordinates = cellCoords;
WorldCoordinates = worldCoords;
GCost = float.MaxValue; // 初始化 GCost 为最大值
HCost = 0;
Parent = null; // 确保 Parent 默认是 null
Status = PathNodeStatus.Unvisited; // 初始化状态
}
/// <summary>
/// 判断两个PathNode实例是否相等基于它们的格子坐标。
/// </summary>
/// <param name="obj">要比较的对象。</param>
/// <returns>如果相等则返回true否则返回false。</returns>
public override bool Equals(object obj)
{
if (obj is PathNode other)
{
return CellCoordinates.Equals(other.CellCoordinates);
}
return false;
}
/// <summary>
/// 获取PathNode实例的哈希码基于其格子坐标。
/// </summary>
/// <returns>PathNode实例的哈希码。</returns>
public override int GetHashCode()
{
return CellCoordinates.GetHashCode();
}
/// <summary>
/// 返回表示当前节点的字符串。
/// </summary>
/// <returns>节点的字符串表示。</returns>
public override string ToString()
{
return $"Node({CellCoordinates}, F:{FCost:F1}, G:{GCost:F1}, H:{HCost:F1}, Status:{Status})";
}
/// <summary>
/// 比较两个PathNode实例用于排序优先队列
/// 优先比较 FCostFCost 相等时优先比较 HCost避免路径过长
/// </summary>
public int CompareTo(PathNode other)
{
if (other == null) return 1;
var fCostComparison = FCost.CompareTo(other.FCost);
if (fCostComparison != 0)
{
return fCostComparison;
}
// FCost 相等时HCost 小的优先级更高(更接近目标)
return HCost.CompareTo(other.HCost);
}
}
/// <summary>
/// 管理实体的路径生成和移动。
/// </summary>
public class EntityPathManager
{
public bool IsPathComplete => _currentPath == null || _currentPath.Count == 0 || _pathIndex >= _currentPath.Count;
/// <summary>
/// 最小优先队列实现用于A*算法的OpenSet。
/// </summary>
/// <typeparam name="T">队列中存储的元素类型,必须实现 IComparableT。</typeparam>
private class PriorityQueue<T> where T : IComparable<T>
{
private readonly List<T> data; // 存储堆元素的列表
public int Count => data.Count; // 队列中的元素数量
public PriorityQueue()
{
data = new List<T>();
}
/// <summary>
/// 将元素添加到队列中。
/// </summary>
/// <param name="item">要添加的元素。</param>
public void Enqueue(T item)
{
data.Add(item);
var ci = data.Count - 1; // child index
while (ci > 0)
{
var pi = (ci - 1) / 2; // parent index
if (data[ci].CompareTo(data[pi]) >= 0) // 如果子节点不比父节点小,则满足堆属性
break;
(data[ci], data[pi]) = (data[pi], data[ci]);
ci = pi;
}
}
/// <summary>
/// 移除并返回队列中最小的元素。
/// </summary>
/// <returns>最小的元素。</returns>
public T Dequeue()
{
// 假设队列不为空,调用前应检查 Count > 0
var li = data.Count - 1; // last index
var frontItem = data[0]; // 最小元素
data[0] = data[li]; // 将最后一个元素移到根部
data.RemoveAt(li); // 移除最后一个元素
--li; // last index (after removal)
var pi = 0; // parent index
while (true)
{
var ci = pi * 2 + 1; // child index (left child)
if (ci > li) break; // 如果左子节点超出范围,说明没有子节点了
var rc = ci + 1; // right child index
if (rc <= li && data[rc].CompareTo(data[ci]) < 0) // 如果右子节点存在且比左子节点小
ci = rc; // 选择右子节点作为要比较的子节点
if (data[pi].CompareTo(data[ci]) <= 0) break; // 如果父节点不比选中的子节点大,则满足堆属性
(data[pi], data[ci]) = (data[ci], data[pi]);
pi = ci;
}
return frontItem;
}
}
private readonly Entity _entity; // 标记为 readonly
private List<Vector2> _currentPath;
private int _pathIndex;
private const float HEURISTIC_MULTIPLIER = 1.0f; // 启发式乘数
private const float IMPASSABLE_TRAVEL_COST_THRESHOLD = 1.0f; // 不可通行成本阈值
/// <summary>
/// 构造一个新的EntityPathManager实例。
/// </summary>
/// <param name="entity">要管理路径的实体。</param>
public EntityPathManager(Entity entity)
{
_entity = entity;
_currentPath = new List<Vector2>();
_pathIndex = 0;
}
/// <summary>
/// 生成从实体当前位置到目标坐标的路径。
/// </summary>
/// <param name="targetCoordinate">目标世界坐标。</param>
public void GeneratePath(Vector2 targetCoordinate)
{
_currentPath.Clear();
_pathIndex = 0;
if (_entity == null)
{
Debug.LogError("实体在实体路径管理器中为空,无法生成路径。");
return;
}
// 假设 Program.Instance.GetDimension 和 currentDimension.landform 存在且有效
// 否则在实际项目中需要更严格的空值检查和错误处理
var currentDimension = Program.Instance.GetDimension(_entity.currentDimensionId);
if (currentDimension == null)
{
Debug.LogError($"未找到维度: {_entity.currentDimensionId}");
return;
}
var landform = currentDimension.landform;
// 起点和终点的格子坐标
var startCell = landform.GetCellCoordinates(_entity.Position);
var targetWorldPosition3D = new Vector3(targetCoordinate.x, targetCoordinate.y, _entity.Position.z);
var targetCell = landform.GetCellCoordinates(targetWorldPosition3D);
var entitySize =
new Vector3Int(Mathf.CeilToInt(_entity.Size.x), Mathf.CeilToInt(_entity.Size.y), 1);
// A* 算法
var openSet = new PriorityQueue<PathNode>(); // 使用优先队列
var allNodes = new Dictionary<Vector3Int, PathNode>(); // 用于快速查找已创建的节点
var startNode = new PathNode(startCell, _entity.Position)
{
GCost = 0,
HCost = CalculateHeuristic(_entity.Position, targetCoordinate),
Status = PathNodeStatus.InOpenSet // 设置起始节点状态
};
openSet.Enqueue(startNode);
allNodes.Add(startCell, startNode);
// 检查目标点是否可通行
// 如果目标点在地图边界外landform.GetTravelCostForArea 应该返回一个不可通行值
var targetCellTravelCost = landform.GetTravelCostForArea(targetCell, entitySize);
if (targetCellTravelCost >= IMPASSABLE_TRAVEL_COST_THRESHOLD)
{
Debug.LogWarning($"目标位置 {targetCoordinate} 不可通行或超出地图边界,无法生成路径。");
_currentPath.Clear();
return;
}
while (openSet.Count > 0)
{
var currentNode = openSet.Dequeue(); // 从优先队列中取出FCost最小的节点
// 如果这个节点已经以最优路径处理过 (通过另一个更优的路径被Dequeued并Closed)
if (currentNode.Status == PathNodeStatus.InClosedSet)
{
continue; // 跳过旧的或重复的队列项
}
// 将当前节点标记为已处理
currentNode.Status = PathNodeStatus.InClosedSet;
// 如果找到目标节点
if (currentNode.CellCoordinates == targetCell)
{
_currentPath = ReconstructPath(currentNode);
return;
}
// 遍历邻居节点 (8个方向包括对角线)
foreach (var neighborCellOffset in new[]
{
new Vector3Int(0, 1, 0), new Vector3Int(0, -1, 0), new Vector3Int(1, 0, 0), new Vector3Int(-1, 0, 0),
new Vector3Int(1, 1, 0), new Vector3Int(1, -1, 0), new Vector3Int(-1, 1, 0), new Vector3Int(-1, -1, 0)
})
{
var neighborCell = currentNode.CellCoordinates + neighborCellOffset;
// 获取邻居节点的世界坐标中心点
var neighborWorldPosition = landform.GetWorldCoordinates(neighborCell);
// 获取邻居区域的通行成本,考虑到实体大小
var travelCost = landform.GetTravelCostForArea(neighborCell, entitySize);
// 如果邻居不可通行,则跳过
if (travelCost >= IMPASSABLE_TRAVEL_COST_THRESHOLD)
{
continue;
}
PathNode neighborNode;
// 尝试从 allNodes 字典中获取邻居节点
if (!allNodes.TryGetValue(neighborCell, out neighborNode))
{
// 如果节点未被发现,则创建它并添加到 allNodes 字典
neighborNode = new PathNode(neighborCell, neighborWorldPosition);
allNodes.Add(neighborCell, neighborNode);
}
// 【逻辑修改1】A* 状态管理一致性改进
// 如果邻居节点已经在closedSet中则意味着我们已经以最优路径处理过它跳过。
// 此处不进行“重新开启已关闭节点”的操作。
if (neighborNode.Status == PathNodeStatus.InClosedSet)
{
continue;
}
// 从当前节点到邻居节点的实际代价
// Distance 乘以 (1 + travelCost) 作为难度系数
var newGCost = currentNode.GCost +
Vector3.Distance(currentNode.WorldCoordinates, neighborNode.WorldCoordinates) *
(1f + travelCost);
// 如果找到更短的路径,则更新邻居节点信息
if (newGCost < neighborNode.GCost) // 路径更优
{
neighborNode.GCost = newGCost;
neighborNode.HCost = CalculateHeuristic(neighborNode.WorldCoordinates, targetCoordinate);
neighborNode.Parent = currentNode;
// 【逻辑修改1】A* 状态管理一致性改进
// 只有当邻居节点是Unvisited时才将其加入OpenSet。
// 如果节点已经是InOpenSet它的GCost已经被更新优先队列会处理其优先级变化。
// 如果节点是InClosedSet则由于前面的continue语句不会到达此处。
if (neighborNode.Status == PathNodeStatus.Unvisited)
{
neighborNode.Status = PathNodeStatus.InOpenSet;
openSet.Enqueue(neighborNode);
}
}
}
}
// 如果OpenSet为空且没有找到路径
Debug.LogWarning($"未找到实体 {_entity.name} 到目标 {targetCoordinate} 的路径。可能目标不可达,或被完全包围。");
_currentPath.Clear();
}
/// <summary>
/// 获取实体下一帧应该移动到的位置。
/// </summary>
/// <param name="currentPosition">实体当前的世界坐标。</param>
/// <param name="moveSpeed">实体的移动速度(单位:世界单位/秒)。</param>
/// <returns>下一帧实体的位置。</returns>
public Vector3 GetNextPosition(Vector3 currentPosition, float moveSpeed)
{
if (IsPathComplete)
{
return currentPosition; // 没有路径或路径已完成,停留在原地
}
var targetWaypoint2D = _currentPath[_pathIndex];
var currentPosition2D = new Vector2(currentPosition.x, currentPosition.y);
// 计算到当前目标路点的距离
var distanceToWaypoint = Vector2.Distance(currentPosition2D, targetWaypoint2D);
// 计算实体在当前帧内可以移动的最大距离
var maxMoveDistance = moveSpeed * Time.deltaTime;
if (distanceToWaypoint <= maxMoveDistance)
{
// 实体可以在当前帧内到达或超过当前路点。
// 直接移动到路点,并将路径索引前进到下一个。
Vector3 nextPosition = new Vector3(targetWaypoint2D.x, targetWaypoint2D.y, currentPosition.z);
_pathIndex++; // 前进到下一个路点
return nextPosition;
}
else
{
// 实体在当前帧内无法到达当前路点。
// 以最大移动距离向当前路点移动。
var direction = (targetWaypoint2D - currentPosition2D).normalized;
var newPosition2D = currentPosition2D + direction * maxMoveDistance;
return new Vector3(newPosition2D.x, newPosition2D.y, currentPosition.z);
}
}
// 【新增函数】获取实体下一帧的方向
/// <summary>
/// 获取实体应该移动的方向2D
/// </summary>
/// <param name="currentPosition">实体的当前世界坐标。</param>
/// <returns>标准化后的移动方向Vector2如果没有路径或路径完成则返回Vector2.zero。</returns>
public Vector2 GetNextDirection(Vector3 currentPosition)
{
if (IsPathComplete)
{
return Vector2.zero; // 没有路径或路径已完成,没有移动方向
}
var targetWaypoint2D = _currentPath[_pathIndex];
var currentPosition2D = new Vector2(currentPosition.x, currentPosition.y);
var direction = (targetWaypoint2D - currentPosition2D);
// 如果实体当前已非常接近目标路点,预判性地查看下一个路点的方向
// 这样做有助于动画平滑过渡,避免在到达路点瞬间方向变为零
if (direction.sqrMagnitude < 0.001f) // 使用一个小的阈值判断是否“到达”路点
{
int lookAheadIndex = _pathIndex + 1;
if (lookAheadIndex < _currentPath.Count)
{
var nextWaypoint2D = _currentPath[lookAheadIndex];
return (nextWaypoint2D - currentPosition2D).normalized;
}
else
{
// 这是最后一个路点,或者没有下一个路点
return Vector2.zero;
}
}
return direction.normalized;
}
/// <summary>
/// 计算从当前节点到目标节点的启发式代价。
/// </summary>
/// <param name="currentWorldPosition">当前世界坐标。</param>
/// <param name="targetWorldPosition">目标世界坐标。</param>
/// <returns>启发式代价。</returns>
private float CalculateHeuristic(Vector3 currentWorldPosition, Vector2 targetWorldPosition)
{
return Vector2.Distance(new Vector2(currentWorldPosition.x, currentWorldPosition.y), targetWorldPosition) *
HEURISTIC_MULTIPLIER;
}
/// <summary>
/// 从终点回溯并重建路径。
/// </summary>
/// <param name="endNode">路径的终点节点。</param>
/// <returns>世界坐标点的路径列表。</returns>
private List<Vector2> ReconstructPath(PathNode endNode)
{
var path = new List<Vector2>();
var currentNode = endNode;
while (currentNode != null)
{
path.Add(new Vector2(currentNode.WorldCoordinates.x, currentNode.WorldCoordinates.y));
currentNode = currentNode.Parent;
}
path.Reverse(); // 将路径反转,使其从起点到终点
return path;
}
// 移除了 GetSmoothTravelCost 方法
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 55b59820532642b287ed7713a3c788c5
timeCreated: 1759141469

View File

@@ -54,7 +54,7 @@ namespace Entity
/// <summary>
/// 获取实体当前的最终属性包括所有健康状态Hediff的修正。
/// </summary>
public override Attributes attributes
public override Attributes AttributesNow
{
get
{
@@ -87,8 +87,17 @@ namespace Entity
}
protected set => _cachedAttributes = value;
}
public EntityPathManager pathManager;
public override bool OnTargetPoint => pathManager.IsPathComplete;
public LivingEntity()
{
pathManager = new EntityPathManager(this);
}
/// <summary>
/// 供内部使用的属性标记方法。当 Hediff 自身状态改变并影响属性时,通过此方法通知 LivingEntity。
/// </summary>
@@ -96,7 +105,7 @@ namespace Entity
{
_needUpdateAttributes = true;
}
/// <summary>
/// 每帧调用的更新函数,传入时间增量。
/// </summary>
@@ -156,5 +165,23 @@ namespace Entity
_needUpdateAttributes = true; // 移除Hediff需要更新属性缓存
}
}
public override void SetTarget(Vector3 pos)
{
base.SetTarget(pos);
pathManager.GeneratePath(pos);
}
public override void TryMove()
{
if (pathManager.IsPathComplete)
return;
_walkingTimer = 2;
var target= pathManager.GetNextPosition(Position, AttributesNow.moveSpeed);
Direction = target - Position;
SetBodyTexture(EntityState.Walking, CurrentOrientation);
transform.position = target;
}
}
}

View File

@@ -1,16 +1,18 @@
using System;
using Data;
using Item;
using Managers;
using UnityEngine;
namespace Entity
{
public class Monster:LivingEntity
{
private WeaponResource weapon;
public override void Init(EntityDef entityDef)
public override void Init(EntityDef entityDefine)
{
base.Init(entityDef);
var monsterDef = entityDef as MonsterDef;
base.Init(entityDefine);
var monsterDef = entityDefine as MonsterDef;
if (monsterDef != null)
{
weapon = (WeaponResource)ItemResourceManager.Instance.GetItem(monsterDef.weapon.defName);
@@ -21,6 +23,13 @@ namespace Entity
{
return weapon;
}
private void OnCollisionStay2D(Collision2D other)
{
if (!other.gameObject.CompareTag("Player")) return;
var playerEntity = other.gameObject.GetComponent<Entity>();
playerEntity?.OnHit(this);
}
}
}

View File

@@ -9,95 +9,183 @@ namespace Entity
{
public class Outline : MonoBehaviour
{
public RightMenuPrefab rightMenuPrefab;
// 实体身体的游戏对象
public GameObject body;
// 描边渲染器
public SpriteRenderer outlineRenderer;
// 描边碰撞体
public CapsuleCollider2D outlineCollider;
// 进度条预制件
public ProgressBarPrefab progressBarPrefab;
// 关联的实体
public Entity entity;
public static Vector3 minimum = new(0.5f, 0.5f, 0.5f);
// 边界的最小尺寸
public static readonly Vector2 MinimumBoundsSize = new(0.5f, 0.5f);
public bool CanShow => Setting.Instance.CurrentSettings.developerMode && entity.canSelect;
// 缓存身体纹理大小
private Vector2 _cachedBodyTextureSize;
public virtual void Init()
// 缓存碰撞体大小
private Vector2 _cachedColliderSize;
// 缓存碰撞体偏移
private Vector2 _cachedOffset;
// 缓存是否已初始化标志 (明确它管理的是数据缓存状态)
private bool _isDataCacheInitialized; // 修改部分1变量更名
// 是否可以显示描边
public bool CanShow => Setting.Instance.CurrentSettings.developerMode && entity.canSelect &&
!UIInputControl.Instance.HasWindowOpen;
/// <summary>
/// 确保描边尺寸相关数据缓存已初始化。如果未初始化,则会计算并缓存。
/// 此方法保证内部的尺寸计算只执行一次。
/// </summary>
private void EnsureDataCacheInitialized() // 修改部分2新增私有方法
{
outlineRenderer.size = GetBodyTextureSize();
var size = GetColliderSize();
outlineCollider.direction = size.x > size.y ? CapsuleDirection2D.Horizontal : CapsuleDirection2D.Vertical;
outlineCollider.size = size;
outlineCollider.offset = GetOffset();
if (_isDataCacheInitialized) return; // 如果数据缓存已初始化,则直接返回
// 计算并缓存所有必要尺寸
_cachedBodyTextureSize = CalculateBodyTextureSize();
_cachedColliderSize = CalculateColliderSize();
_cachedOffset = CalculateOffset();
_isDataCacheInitialized = true; // 标记数据缓存已初始化
}
/// <summary>
/// 初始化描边,将计算出的尺寸应用于描边渲染器、碰撞体和进度条。
/// 此方法确保数据缓存已初始化,并可以被重复调用以重新应用设置。
/// </summary>
public virtual void Init() // 修改部分4修改 Init 方法
{
// 首先确保尺寸数据是可用的。这个调用会立即返回如果数据缓存已初始化。
EnsureDataCacheInitialized();
// 使用缓存值设置OutlineRenderer
outlineRenderer.size = _cachedBodyTextureSize;
// 使用缓存值设置OutlineCollider
var colliderSize = _cachedColliderSize; // 使用缓存值
outlineCollider.direction = colliderSize.x > colliderSize.y
? CapsuleDirection2D.Horizontal
: CapsuleDirection2D.Vertical;
outlineCollider.size = colliderSize;
outlineCollider.offset = _cachedOffset; // 使用缓存值
// 使用缓存值调整ProgressBarPrefab
if (progressBarPrefab)
{
progressBarPrefab.transform.localPosition += new Vector3(0f, size.y * 2 / 3, 0f);
progressBarPrefab.transform.localScale = new Vector3(size.x, 1f / 10f, 1);
// 确保每次 Init 调用都能正确设置位置而不重复累加。
progressBarPrefab.transform.localPosition =
new Vector3(0f, colliderSize.y * 2 / 3, 0f); // 逻辑修改:将 += 改为 =
progressBarPrefab.transform.localScale = new Vector3(colliderSize.x, 1f / 10f, 1);
}
}
/// <summary>
/// 显示描边。
/// </summary>
public void Show()
{
outlineRenderer.enabled = true;
if (CanShow)
outlineRenderer.enabled = true;
}
/// <summary>
/// 隐藏描边。
/// </summary>
public void Hide()
{
outlineRenderer.enabled = false;
}
/// <summary>
/// 获取指定对象及其所有子对象组成的图像的大小
/// 获取当前实体的碰撞体大小。在首次调用时自动计算并缓存
/// </summary>
/// <returns>
/// 返回一个 Vector3 对象,表示对象在世界空间中的总大小(宽度、高度、深度)。
/// 如果没有找到任何渲染器,则返回 (-1, -1, -1) 表示无效大小。
/// </returns>
public Vector2 GetColliderSize()
/// <returns>碰撞体大小的Vector2。</returns>
public Vector2 GetColliderSize() // 修改部分3修改 GetColliderSize 方法
{
return !string.IsNullOrEmpty(entity.entityDef.colliderSize)
? Utils.StringUtils.StringToVector2(entity.entityDef.colliderSize)
: GetBodyTextureSize();
EnsureDataCacheInitialized(); // 确保数据缓存已初始化
return _cachedColliderSize;
}
public Vector2 GetBodyTextureSize()
/// <summary>
/// 获取身体的纹理总大小。在首次调用时自动计算并缓存。
/// </summary>
/// <returns>身体纹理大小的Vector2。</returns>
public Vector2 GetBodyTextureSize() // 修改部分3修改 GetBodyTextureSize 方法
{
EnsureDataCacheInitialized(); // 确保数据缓存已初始化
return _cachedBodyTextureSize;
}
/// <summary>
/// 获取当前实体的碰撞体偏移。在首次调用时自动计算并缓存。
/// </summary>
/// <returns>碰撞体偏移的Vector2。</returns>
public Vector2 GetOffset() // 修改部分3修改 GetOffset 方法
{
EnsureDataCacheInitialized(); // 确保数据缓存已初始化
return _cachedOffset;
}
/// <summary>
/// 计算并返回碰撞体大小。此方法是内部实现细节。
/// </summary>
/// <returns>碰撞体大小的Vector2。</returns>
private Vector2 CalculateColliderSize()
{
return !string.IsNullOrEmpty(entity.entityDef?.colliderSize)
? Utils.StringUtils.StringToVector2(entity.entityDef.colliderSize)
: CalculateBodyTextureSize();
}
/// <summary>
/// 计算并返回身体纹理的总大小。此方法是内部实现细节。
/// </summary>
/// <returns>身体纹理总大小的Vector2。</returns>
private Vector2 CalculateBodyTextureSize()
{
// 获取所有子对象的 Renderer 组件
var renderers = body.GetComponentsInChildren<Renderer>();
// 如果没有找到任何 Renderer返回一个默认值 (-1, -1, -1)
var renderers = body.GetComponentsInChildren<Renderer>(true);
// 如果没有找到任何 Renderer返回一个默认值
if (renderers.Length == 0)
{
return minimum;
return MinimumBoundsSize;
}
// 初始化 totalBounds 为第一个 Renderer 的 bounds
var totalBounds = renderers[0].bounds;
// 遍历剩余的 Renderer将它们的 bounds 合并到 totalBounds 中
for (var i = 1; i < renderers.Length; i++)
{
totalBounds.Encapsulate(renderers[i].bounds);
}
// 获取合并后的包围盒的大小
var size = totalBounds.size;
// 确保每个维度的大小都不小于 0.5
size.x = Mathf.Max(size.x, 0.5f);
size.y = Mathf.Max(size.y, 0.5f);
size.z = Mathf.Max(size.z, 0.5f);
// 获取合并后的包围盒的XY大小
var size = new Vector2(totalBounds.size.x, totalBounds.size.y);
// 确保每个维度的大小都不小于最小限制
size.x = Mathf.Max(size.x, MinimumBoundsSize.x);
size.y = Mathf.Max(size.y, MinimumBoundsSize.y);
return size;
}
public Vector2 GetOffset()
/// <summary>
/// 计算并返回碰撞体偏移。此方法是内部实现细节。
/// </summary>
/// <returns>碰撞体偏移的Vector2。</returns>
private Vector2 CalculateOffset()
{
return string.IsNullOrEmpty(entity.entityDef.colliderPosition)
return string.IsNullOrEmpty(entity.entityDef?.colliderPosition)
? Vector2.zero
: Utils.StringUtils.StringToVector2(entity.entityDef.colliderPosition);
}
/// <summary>
/// 当鼠标进入描边区域时调用。
/// </summary>
protected virtual void OnMouseEnter()
{
if (!CanShow)
@@ -105,11 +193,17 @@ namespace Entity
Show();
}
/// <summary>
/// 当鼠标离开描边区域时调用。
/// </summary>
protected virtual void OnMouseExit()
{
Hide();
}
/// <summary>
/// 当鼠标停留在描边区域时每帧调用。
/// </summary>
protected virtual void OnMouseOver()
{
if (!Program.Instance.CanOpenRightMenu || !CanShow)
@@ -121,6 +215,10 @@ namespace Entity
}
}
/// <summary>
/// 获取右键菜单项列表。
/// </summary>
/// <returns>包含菜单项名称和对应回调函数的列表。</returns>
protected virtual List<(string name, UnityAction callback)> GetMenu()
{
var result = new List<(string name, UnityAction callback)>();
@@ -133,20 +231,31 @@ namespace Entity
return result;
}
/// <summary>
/// 将当前实体变为默认实体。
/// </summary>
protected void BecomeDefault()
{
entity.Kill();
EntityManager.Instance.GenerateDefaultEntity(Program.Instance.FocusedDimensionId, entity.Position);
entity?.Kill();
if (entity != null)
EntityManager.Instance.GenerateDefaultEntity(Program.Instance.FocusedDimensionId, entity.Position);
}
/// <summary>
/// 开始玩家对实体的操控。
/// </summary>
protected void StartControl()
{
entity.PlayerControlled = true;
}
/// <summary>
/// 结束玩家对实体的操控。
/// </summary>
protected void EndControl()
{
entity.PlayerControlled = false;
}
}
}
}