mirror of
http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite.git
synced 2025-11-20 08:57:13 +08:00
(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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
441
Client/Assets/Scripts/Entity/EntityPathManager.cs
Normal file
441
Client/Assets/Scripts/Entity/EntityPathManager.cs
Normal 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实例,用于排序(优先队列)。
|
||||
/// 优先比较 FCost,FCost 相等时优先比较 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 方法
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/Entity/EntityPathManager.cs.meta
Normal file
3
Client/Assets/Scripts/Entity/EntityPathManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55b59820532642b287ed7713a3c788c5
|
||||
timeCreated: 1759141469
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user