(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

@@ -1,77 +0,0 @@
using Data;
using Parsing;
using System;
using System.Collections.Generic;
namespace AI
{
public abstract class AIBase
{
public List<AIBase> children = new();
public abstract JobBase GetJob(Entity.Entity target);
public virtual void Init(BehaviorTreeDef def)
{
}
}
public class ThinkNode_Selector : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
foreach (var aiBase in children)
{
var job = aiBase.GetJob(target);
if (job != null)
return job;
}
return null;
}
}
public class ThinkNode_Conditional : ThinkNode_Selector
{
// 条件函数,返回 true 表示满足条件
private Func<Entity.Entity, bool> condition;
public override JobBase GetJob(Entity.Entity target)
{
// 检查条件是否满足
if (condition != null && condition(target))
{
// 如果条件满足,继续查找子节点的任务
return base.GetJob(target);
}
// 条件不满足,直接返回 null
return null;
}
public override void Init(BehaviorTreeDef def)
{
base.Init(def); // 调用基类的Init方法
if (!string.IsNullOrEmpty(def.value))
{
try
{
// 使用 ConditionDelegateFactory 来解析 def.value 并创建条件委托
this.condition = ConditionDelegateFactory.CreateConditionDelegate(
def.value,
typeof(Entity.Entity),
typeof(ConditionFunctions) // 指定查找条件函数的类
);
}
catch (Exception)
{
this.condition = (e) => false;
}
}
else
{
this.condition = (e) => false; // 如果没有指定条件,则条件始终不满足
}
}
}
}

View File

@@ -1,74 +0,0 @@
using Data;
using System;
namespace AI
{
public static class BehaviorTree
{
/// <summary>
/// 将行为树定义转换为 AIBase 类型。
/// </summary>
/// <param name="behaviorTreeDef">行为树定义。</param>
/// <returns>转换后的 AIBase 实例。</returns>
public static AIBase ConvertToAIBase(BehaviorTreeDef behaviorTreeDef)
{
if (behaviorTreeDef == null)
return null;
var aiBase = CreateAIBaseInstance(behaviorTreeDef.className);
aiBase.Init(behaviorTreeDef);
if (behaviorTreeDef.childTree != null)
{
foreach (var child in behaviorTreeDef.childTree)
{
if (child != null)
{
aiBase.children.Add(ConvertToAIBase(child));
}
}
}
return aiBase;
}
/// <summary>
/// 使用反射根据类名创建 AIBase 的具体子类实例。
/// </summary>
/// <param name="className">类名。</param>
/// <returns>创建的 AIBase 子类实例。</returns>
private static AIBase CreateAIBaseInstance(string className)
{
if (string.IsNullOrEmpty(className))
throw new ArgumentException("className 不能为空");
if (className.Equals("AIBase", StringComparison.OrdinalIgnoreCase))
{
return (AIBase)Activator.CreateInstance(typeof(AIBase));
}
// 定义可能的命名空间列表
var possibleNamespaces = new[] { "AI" };
foreach (var ns in possibleNamespaces)
{
try
{
// 获取当前程序集
var assembly = typeof(AIBase).Assembly;
// 尝试查找类型
var type = assembly.GetType($"{ns}.{className}");
if (type != null && typeof(AIBase).IsAssignableFrom(type))
{
// 如果找到合适的类型,则创建实例并返回
return (AIBase)Activator.CreateInstance(type);
}
}
catch
{
// 忽略单个命名空间的错误,继续尝试下一个命名空间
}
}
// 如果所有命名空间都未找到对应的类型,抛出异常
throw new InvalidOperationException($"无法找到类型 {className} 或该类型不是 AIBase 的子类");
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6b711456a12f4bf6a29b0de14a2d7d8f
timeCreated: 1754982046

View File

@@ -0,0 +1,37 @@
using Data;
using Parsing;
using System;
using System.Collections.Generic;
using Managers;
using UnityEngine;
namespace AI
{
public enum Status
{
Ready,
Running, // 节点正在执行中
Success, // 节点成功完成
Failure // 节点失败
}
public abstract class BehaviorTreeBase
{
// 所有行为树节点通用的核心成员
public Status CurrentStatus = Status.Ready;
public Entity.Entity SelfEntity { get; private set; }
public virtual void Init(BehaviorTreeDef def, Entity.Entity selfEntity)
{
SelfEntity = selfEntity;
}
// 所有行为树节点都必须实现的执行逻辑
public abstract Status Tick();
public virtual void Reset()
{
CurrentStatus = Status.Ready;
}
}
}

View File

@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.Text;
using Data;
using Parsing;
using UnityEngine;
namespace AI
{
public abstract class CompositeNodeBase:BehaviorTreeBase
{
// 组合节点特有的成员:子行为树列表
protected List<BehaviorTreeBase> children = new();
/// <summary>
/// 初始化组合行为树节点及其子节点。
/// </summary>
/// <param name="def">行为树定义。</param>
/// <param name="selfEntity">关联的实体。</param>
public override void Init(BehaviorTreeDef def, Entity.Entity selfEntity)
{
base.Init(def, selfEntity); // 调用基类Init处理 SelfEntity 和 blackboard
if (def.childTree == null) return;
foreach (var childDef in def.childTree)
{
var childNode = Utils.BehaviorTreeUtils.ConvertToAIBase(childDef);
if (childNode == null) continue;
children.Add(childNode);
childNode.Init(childDef, selfEntity); // 递归初始化子节点
}
}
/// <summary>
/// 重置组合行为树节点的状态及其所有子节点的状态。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类Reset重置自身状态
foreach (var child in children)
{
child?.Reset(); // 确保子节点不为 null 时才调用 Reset
}
}
/// <summary>
/// 返回表示当前行为树节点及其子节点层次结构的字符串。
/// </summary>
/// <returns>行为树的层次结构字符串。</returns>
public override string ToString()
{
return ToString(0); // 从根节点开始初始缩进为0
}
/// <summary>
/// 返回表示当前行为树节点及其子节点层次结构的字符串,带有指定缩进。
/// </summary>
/// <param name="indent">当前节点的缩进级别。</param>
/// <returns>行为树的层次结构字符串。</returns>
protected virtual string ToString(int indent)
{
StringBuilder sb = new StringBuilder();
string indentStr = new string(' ', indent * 2); // 每个级别2个空格
sb.AppendLine($"{indentStr}[{this.GetType().Name}] ");
foreach (var child in children)
{
// 确保子节点不为 null 且支持 ToString(int) 方法
if (child is CompositeNodeBase compositeChild)
{
sb.Append(compositeChild.ToString(indent + 1));
}
else if (child != null) // 对于叶子节点或非CompositeNodeBase的BehaviorTreeBase
{
// 如果子节点不是复合节点但有自己的ToString实现直接调用其ToString
// 否则打印其类型和ID/名称
string childIndentStr = new string(' ', (indent + 1) * 2);
sb.AppendLine($"{childIndentStr}[{child.GetType().Name}] ");
}
}
return sb.ToString();
}
}
public class ThinkNode_Selector : CompositeNodeBase
{
// 用于跟踪正在运行的子节点索引
protected int currentChildIndex = 0;
/// <summary>
/// 执行选择器节点的逻辑。它会按顺序执行子节点,直到一个子节点成功或正在运行。
/// </summary>
/// <returns>节点的执行状态。</returns>
public override Status Tick()
{
// 如果是从 Running 状态恢复,则从上次停止的子节点开始
// 否则,从第一个子节点开始
for (var i = currentChildIndex; i < children.Count; i++)
{
var child = children[i];
if (child == null) // 处理 null 子节点,避免 NullReferenceException
{
Debug.LogWarning($"选择器节点在索引 {i} 处有空子节点。跳过。");
continue; // 跳过此 null 子节点
}
CurrentStatus = child.Tick(); // 执行当前子节点的 Tick
switch (CurrentStatus)
{
case Status.Success:
currentChildIndex = 0;
return Status.Success;
case Status.Running:
// 如果子节点正在运行,选择器也正在运行
// 记录当前子节点的索引,以便下次 Tick 时继续
currentChildIndex = i;
return Status.Running;
}
}
currentChildIndex = 0;
return Status.Failure;
}
/// <summary>
/// 重置选择器节点的状态,包括子节点索引。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的 Reset 方法
currentChildIndex = 0; // 重置子节点索引
}
}
public class ThinkNode_Conditional : ThinkNode_Selector
{
// 条件函数,返回 true 表示满足条件
private Func<Entity.Entity, bool> condition;
/// <summary>
/// 初始化条件节点,创建条件委托。
/// </summary>
/// <param name="def">行为树定义。</param>
/// <param name="selfEntity">关联的实体。</param>
public override void Init(BehaviorTreeDef def, Entity.Entity selfEntity)
{
base.Init(def, selfEntity); // 调用基类的 Init 方法,初始化子节点和 SelfEntity
if (!string.IsNullOrEmpty(def.value))
{
try
{
condition = ConditionDelegateFactory.CreateConditionDelegate(
def.value,
typeof(Entity.Entity),
typeof(ConditionFunctions) // 指定查找条件函数的类
);
}
catch (Exception ex)
{
// 记录错误,并使条件始终不满足
Debug.LogError($"无法为 '{def.value}' 创建条件委托: {ex.Message}");
condition = (e) => false;
}
}
else
{
condition = (e) => false; // 如果没有指定条件,则条件始终不满足
}
}
/// <summary>
/// 执行条件节点的逻辑。首先检查条件,如果条件满足则执行其子节点(作为选择器)。
/// </summary>
/// <returns>节点的执行状态。</returns>
public override Status Tick()
{
// 在执行任何子节点之前,先检查条件
if (condition == null || !condition(SelfEntity)) // 使用 SelfEntity 来执行条件检查
{
// 如果条件不满足,则直接返回 Failure
currentChildIndex = 0; // 条件不满足时,重置子节点索引
return Status.Failure;
}
// 如果条件满足,则像父类 Selector 一样执行子节点
return base.Tick();
}
}
public class ThinkNode_Sequence : CompositeNodeBase
{
// 用于跟踪正在运行的子节点索引
private int currentChildIndex = 0;
/// <summary>
/// 执行序列器节点的逻辑。它会按顺序执行子节点,直到一个子节点失败或正在运行。
/// </summary>
/// <returns>节点的执行状态。</returns>
public override Status Tick()
{
// 从上次停止的子节点或者从第一个子节点开始
for (var i = currentChildIndex; i < children.Count; i++)
{
var child = children[i];
if (child == null) // 处理 null 子节点,避免 NullReferenceException
{
Debug.LogWarning($"序列器节点在索引 {i} 处有空子节点。跳过。");
// Null 子节点对于 Sequence 来说是失败的表现
currentChildIndex = 0;
return Status.Failure;
}
CurrentStatus = child.Tick(); // 执行当前子节点的 Tick
switch (CurrentStatus)
{
case Status.Failure:
// 如果子节点失败,序列器也失败
// 重置索引,以便下次从头开始
currentChildIndex = 0;
return Status.Failure;
case Status.Running:
// 如果子节点正在运行,序列器也正在运行
// 记录当前子节点的索引,以便下次 Tick 时继续
currentChildIndex = i;
return Status.Running;
}
// 如果子节点返回 Success则继续尝试下一个子节点 (这是 Sequence 的核心)
}
// 如果所有子节点都返回 Success (即循环结束且没有返回 Failure 或 Running)
// 则 Sequence 也返回 Success。
// 重置索引
currentChildIndex = 0;
return Status.Success;
}
/// <summary>
/// 重置序列器节点的状态,包括子节点索引。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的 Reset 方法
currentChildIndex = 0; // 重置子节点索引
}
}
public enum BranchExecutionPhase
{
None, // 初始或重置状态
EvaluatingCondition, // 正在执行条件节点
ExecutingTrueBranch, // 正在执行真分支节点
ExecutingFalseBranch // 正在执行假分支节点
}
public class ThinkNode_Branch : CompositeNodeBase
{
// 内部状态,用于跟踪分支节点当前正在执行哪个子节点
private BranchExecutionPhase _currentPhase = BranchExecutionPhase.None;
// 用于防止日志刷屏
private bool _hasLoggedMisconfigurationWarning = false;
/// <summary>
/// 初始化分支节点。
/// </summary>
/// <param name="def">行为树定义。</param>
/// <param name="selfEntity">关联的实体。</param>
public override void Init(BehaviorTreeDef def, Entity.Entity selfEntity)
{
base.Init(def, selfEntity); // 调用基类的 Init 方法,初始化子节点和 SelfEntity
_currentPhase = BranchExecutionPhase.None; // 确保初始化时状态为 None
_hasLoggedMisconfigurationWarning = false; // 确保 Init 时也重置警告标志
}
/// <summary>
/// 执行分支节点的逻辑。它会首先评估条件树,然后根据条件结果执行真分支或假分支。
/// </summary>
/// <returns>节点的执行状态。</returns>
public override Status Tick()
{
// 如果之前有警告但现在配置正确了,就重置警告标志
if (_hasLoggedMisconfigurationWarning && children.Count >= 2)
{
_hasLoggedMisconfigurationWarning = false;
}
// 确保至少有两个子节点 (条件树和真树)
if (children.Count < 2)
{
if (!_hasLoggedMisconfigurationWarning) // 只有在未记录过警告时才记录
{
Debug.LogWarning(
$"分支节点至少需要2个子节点(条件和真分支)。当前实例配置错误。 " +
$"此实例的进一步警告将在重置或更正后才显示。 " +
$"实体: {SelfEntity?.name ?? "N/A"}。"); // 增加更多上下文信息
_hasLoggedMisconfigurationWarning = true; // 标记已记录
}
Reset(); // 重置内部状态 (包括_currentPhase 和 _hasLoggedMisconfigurationWarning)
return Status.Failure;
}
// 获取条件树和真树
var conditionTree = children[0];
var trueTree = children[1];
// 假树是可选的
var falseTree = children.Count > 2 ? children[2] : null;
// 如果是首次 Tick 或者 Reset 后,将阶段设置为评估条件
if (_currentPhase == BranchExecutionPhase.None)
{
_currentPhase = BranchExecutionPhase.EvaluatingCondition;
}
Status result;
switch (_currentPhase)
{
case BranchExecutionPhase.EvaluatingCondition:
// 执行条件树
if (conditionTree == null)
{
Debug.LogError("分支节点: 条件树为空。返回失败。");
Reset();
return Status.Failure;
}
result = conditionTree.Tick();
if (result == Status.Running)
{
// 条件正在运行,则整个分支也运行
return Status.Running;
}
if (result == Status.Success)
{
// 条件成功,转移到执行真树阶段
_currentPhase = BranchExecutionPhase.ExecutingTrueBranch;
// 继续执行真树(因为条件成功,我们可以立即尝试执行真树)
goto case BranchExecutionPhase.ExecutingTrueBranch;
}
// result == Status.Failure
// 条件失败,转移到执行假树阶段
_currentPhase = BranchExecutionPhase.ExecutingFalseBranch;
// 继续执行假树(如果存在)
goto case BranchExecutionPhase.ExecutingFalseBranch;
case BranchExecutionPhase.ExecutingTrueBranch:
// 执行真树
if (trueTree == null)
{
Debug.LogError("分支节点: 真分支树为空。返回失败。");
Reset();
return Status.Failure;
}
result = trueTree.Tick();
if (result == Status.Running)
{
// 真树正在运行,则整个分支也运行
return Status.Running;
}
// 真树完成 (成功或失败),分支节点完成,重置状态
Reset();
return result;
case BranchExecutionPhase.ExecutingFalseBranch:
// 执行假树 (如果存在)
if (falseTree != null)
{
result = falseTree.Tick();
if (result == Status.Running)
{
// 假树正在运行,则整个分支也运行
return Status.Running;
}
// 假树完成 (成功或失败),分支节点完成,重置状态
Reset();
return result;
}
// 没有假树,且条件失败,则分支节点失败
Reset();
return Status.Failure;
case BranchExecutionPhase.None:
default:
// 不应该到达这里,如果到达表示有逻辑错误
Debug.LogError($"分支节点处于意外阶段: {_currentPhase}。返回失败。");
Reset();
return Status.Failure;
}
}
/// <summary>
/// 重置分支节点的状态,包括内部执行阶段。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的 Reset 方法来确保子节点也重置
_currentPhase = BranchExecutionPhase.None; // 重置内部状态
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60ed77d7b0d8475b915d95040d33bb34
timeCreated: 1759058538

View File

@@ -0,0 +1,45 @@
namespace AI
{
/// <summary>
/// 行为树叶节点:始终返回 Status.Success。
/// </summary>
public class ConstantNode_Success : BehaviorTreeBase
{
/// <summary>
/// 执行节点逻辑,总是返回 Status.Success。
/// </summary>
/// <returns>Status.Success</returns>
public override Status Tick()
{
return Status.Success;
}
}
/// <summary>
/// 行为树叶节点:始终返回 Status.Failure。
/// </summary>
public class ConstantNode_Failure : BehaviorTreeBase
{
/// <summary>
/// 执行节点逻辑,总是返回 Status.Failure。
/// </summary>
/// <returns>Status.Failure</returns>
public override Status Tick()
{
return Status.Failure;
}
}
/// <summary>
/// 行为树叶节点:始终返回 Status.Running。
/// </summary>
public class ConstantNode_Running : BehaviorTreeBase
{
/// <summary>
/// 执行节点逻辑,总是返回 Status.Running。
/// </summary>
/// <returns>Status.Running</returns>
public override Status Tick()
{
return Status.Running;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cdb3950442f3454db89d7f566a4c5404
timeCreated: 1759059830

View File

@@ -1,477 +0,0 @@
using Data;
using Managers;
using UnityEngine;
namespace AI
{
public abstract class JobBase
{
public Entity.Entity entity;
protected int timeoutTicks = 180;
protected bool isCompleted = false; // 新增:表示工作是否已完成
// Running 现在可以基于 isCompleted 来判断
public bool Running => !isCompleted && timeoutTicks > 0;
public bool IsCompleted => isCompleted; // 新增:提供公共访问器
protected abstract void UpdateJob();
public virtual void StartJob(Entity.Entity target)
{
entity = target;
timeoutTicks = 180; // 重置超时,确保新工作开始时是活跃的
isCompleted = false; // 开始新工作时,重置完成状态
}
public bool Update()
{
if (IsCompleted) // 如果已经完成直接返回false
return false;
if (!Running) // 如果因为超时而不再运行,则标记为完成
{
StopJob(); // 确保完成状态被设置
return false;
}
UpdateJob();
timeoutTicks--;
return true;
}
public virtual void StopJob()
{
timeoutTicks = 0;
isCompleted = true; // 显式标记工作已完成
}
// 可以添加一个方法来重置任务,以便它可以被重新使用
public virtual void ResetJob()
{
timeoutTicks = 180; // 重置超时
isCompleted = false; // 重置完成状态
entity = null; // 清除关联实体
}
}
public class WanderJob : JobBase
{
public override void StartJob(Entity.Entity target)
{
base.StartJob(target);
Vector3 move = new(Random.Range(-10, 10), Random.Range(-10, 10));
var targetPosition = entity.transform.position + move;
entity.SetTarget(targetPosition);
}
protected override void UpdateJob()
{
entity.TryMove();
}
override public void StopJob()
{
base.StopJob();
}
}
public class IdleJob : JobBase
{
override public void StartJob(Entity.Entity target)
{
base.StartJob(target);
timeoutTicks = 500;
}
protected override void UpdateJob()
{
}
}
public class MoveJob : JobBase
{
protected override void UpdateJob()
{
entity.TryMove();
}
}
public class AttackJob : JobBase
{
private Entity.Entity attackTarget;
// StartJob 方法:用于初始化任务,寻找初始攻击目标
override public void StartJob(Entity.Entity performerEntityContext) // 参数名更明确,通常是发起任务的实体
{
base.StartJob(performerEntityContext);
// 1. 任务执行者自身有效性检查
if (!entity)
{
StopJob(); // 调用StopJob来结束任务
return;
}
attackTarget = FindNewHostileTarget();
if (!attackTarget)
{
StopJob(); // 调用StopJob来结束任务
}
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (!entity || entity.IsDead)
{
StopJob();
return;
}
if (!attackTarget || attackTarget.IsDead)
{
attackTarget = FindNewHostileTarget(); // 尝试寻找新的攻击目标
if (!attackTarget)
{
StopJob();
return;
}
}
var weapon = entity.GetCurrentWeapon();
var attackRange = 0f;
if (weapon != null)
{
attackRange = weapon.Attributes.attackRange;
}
var distanceSq = (entity.Position - attackTarget.Position).sqrMagnitude;
var effectiveAttackRangeSq = attackRange * attackRange; // 将攻击范围也平方
entity.SetTarget(attackTarget.Position);
if (weapon != null && distanceSq <= effectiveAttackRangeSq)
{
entity.TryAttack();
}
else
{
entity.TryMove();
}
}
/// <summary>
/// 查找执行实体最近的敌对目标。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNewHostileTarget()
{
if (!entity) return null;
return EntityManager.Instance.FindNearestEntityByRelation(
entity.currentDimensionId, // 搜索维度ID
entity.entityPrefab, // 执行实体的Prefab ID用于关系判断
Relation.Hostile)?.entity; // 寻找敌对关系的目标
}
}
public class AdvancedAttackJob : JobBase
{
private Entity.Entity attackTarget;
// 常量用于配置远程AI行为的风筝参数
private const float KITING_THRESHOLD_MULTIPLIER = 0.5f; // 当目标距离小于 (攻击范围 * 此乘数) 时,远程单位开始尝试远离
private const float KITING_BUFFER_DISTANCE = 5.0f; // 当远程单位远离时,目标点会是当前位置向反方向偏移此距离
// StartJob 方法:用于初始化任务,寻找初始攻击目标
override public void StartJob(Entity.Entity performerEntityContext)
{
base.StartJob(performerEntityContext);
if (entity == null)
{
StopJob();
return;
}
attackTarget = FindNewHostileTarget();
if (attackTarget == null)
{
StopJob();
}
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (entity == null || entity.IsDead)
{
StopJob();
return;
}
// 2. 攻击目标检查
if (attackTarget == null || attackTarget.IsDead)
{
attackTarget = FindNewHostileTarget(); // 尝试寻找新的攻击目标
if (attackTarget == null)
{
StopJob();
return;
}
}
// 获取武器和其属性
var weapon = entity.GetCurrentWeapon();
var attackRange = 0f;
var isRangedWeapon = false; // 标识是否为远程武器
if (weapon != null)
{
if (weapon.Attributes != null)
{
attackRange = weapon.Attributes.attackRange;
}
// 使用 WeaponType 来判断武器类型,更明确
isRangedWeapon = weapon.Type == WeaponType.Ranged;
}
var distanceSq = (entity.Position - attackTarget.Position).sqrMagnitude;
var effectiveAttackRangeSq = attackRange * attackRange; // 将攻击范围平方
// ---- 核心AI行为决策 ( AdvancedAttackJob 的智能之处) ----
if (isRangedWeapon)
{
// 远程单位的风筝Kiting逻辑
var kitingDistance = attackRange * KITING_THRESHOLD_MULTIPLIER;
var kitingThresholdSq = kitingDistance * kitingDistance; // 过近距离的平方
// 1. 如果目标过于接近 (小于风筝阈值),尝试远离
if (distanceSq < kitingThresholdSq)
{
// 计算一个远离目标的点作为移动目标
var directionAway = (entity.Position - attackTarget.Position).normalized;
var fleePosition = entity.Position + directionAway * KITING_BUFFER_DISTANCE;
entity.SetTarget(fleePosition); // 设置远离点为新的移动目标
entity.TryMove(); // 优先执行移动操作以拉开距离
// 在此状态下不进行攻击,专注于 reposition
}
// 2. 如果目标在最佳攻击范围内 (即在风筝阈值和有效攻击范围之间),则停止移动并攻击
else if (distanceSq <= effectiveAttackRangeSq)
{
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动,专注于攻击
entity.TryAttack();
}
// 3. 如果目标太远 (超出有效攻击范围),则移动靠近目标
else
{
entity.SetTarget(attackTarget.Position); // 设置目标位置为移动目标
entity.TryMove();
}
}
else // 近战单位或没有武器的单位
{
entity.SetTarget(attackTarget.Position);
if (weapon != null && distanceSq <= effectiveAttackRangeSq)
{
entity.TryAttack();
}
else
{
entity.TryMove();
}
}
}
/// <summary>
/// 查找执行实体最近的敌对目标。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNewHostileTarget()
{
if (!entity) return null;
return EntityManager.Instance.FindNearestEntityByRelation(
entity.currentDimensionId,
entity.entityPrefab,
Relation.Hostile)?.entity;
}
}
public class FleeJob : JobBase
{
// 逃跑时在远离方向上设置的目标点偏移距离。让AI尝试跑向更远的点。
private const float FLEE_BUFFER_DISTANCE = 10.0f;
// 远程单位在逃跑时,如果敌人距离已超出其攻击范围的多少倍,可以认为已经足够安全。
// 此时实体将停止紧急移动可以等待timeoutTicks结束或进行其他决策。
private const float RANGED_SAFE_DISTANCE_MULTIPLIER = 2.0f;
// 对于近战或无武器单位,敌人必须在这个距离内,才触发紧急逃跑。
// 超过这个距离,实体可能认为已经足够安全,可以停止紧急移动。
private const float MELEE_PERIL_DISTANCE = 15.0f;
private const float MELEE_PERIL_DISTANCE_SQUARED = MELEE_PERIL_DISTANCE * MELEE_PERIL_DISTANCE;
// **新增**:目标刷新间隔 (以Ticks计)
private const int TARGET_REFRESH_INTERVAL_TICKS = 30; // 每30帧Ticks刷新一次目标
// **新增**缓存的敌对目标避免频繁调用FindNearestHostileTarget
private Entity.Entity _cachedHostileTarget;
// **新增**:目标刷新计时器
private int _targetRefreshTimer;
// **新增**:侧走参数 (用于躲避远程攻击)
private const float EVASION_SIDE_STEP_STRENGTH = 0.5f; // 侧走分量强度,与主方向叠加,需要调整
private int _sideStepDirection = 1; // 1 for right, -1 for left
private int _sideStepTimer;
private const int EVASION_SIDE_STEP_INTERVAL_TICKS = 20; // 每20帧切换一次侧走方向
public FleeJob(int initialTimeoutTicks = 150)
{
timeoutTicks = initialTimeoutTicks;
_targetRefreshTimer = 0; // 初始化计时器让第一次UpdateJob立即刷新目标
_sideStepTimer = 0;
_sideStepDirection = Random.Range(0, 2) * 2 - 1; // 随机初始化左右方向
}
public override void StartJob(Entity.Entity performerEntityContext)
{
base.StartJob(performerEntityContext);
_cachedHostileTarget = null; // 任务开始时清空缓存,确保第一次更新会查找新目标
_targetRefreshTimer = 0; // 确保立即查找目标
_sideStepTimer = 0;
_sideStepDirection = Random.Range(0, 2) * 2 - 1; // 随机初始化左右方向
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (!entity || entity.IsDead)
{
StopJob();
return;
}
// 2. 目标刷新逻辑 (避免每帧都调用FindNearestHostileTarget)
_targetRefreshTimer--;
if (!_cachedHostileTarget || _cachedHostileTarget.IsDead || _targetRefreshTimer <= 0)
{
_cachedHostileTarget = FindNearestHostileTarget();
_targetRefreshTimer = TARGET_REFRESH_INTERVAL_TICKS; // 重置计时器
}
// 使用缓存的目标
var hostileTarget = _cachedHostileTarget;
if (!hostileTarget)
{
// 如果没有敌对目标可逃跑,则停止此工作。
StopJob();
return;
}
// 3. 计算与敌对目标的距离
var distanceSq = (entity.Position - hostileTarget.Position).sqrMagnitude;
// 4. 获取敌方目标武器信息,判断敌方是否为远程单位
var enemyWeapon = hostileTarget.GetCurrentWeapon();
var isEnemyRanged = enemyWeapon is { Type: WeaponType.Ranged };
// 获取自己的武器信息 (虽然逃跑主要看敌方武器,但自身安全距离判断需要)
var selfWeapon = entity.GetCurrentWeapon();
var isSelfRanged = selfWeapon is { Type: WeaponType.Ranged };
// 5. 计算一个远离目标的基础方向
var directionAway = (entity.Position - hostileTarget.Position).normalized;
var moveTargetDirection = directionAway; // 默认是纯远离方向
// 6. 如果敌方是远程单位,叠加侧走逻辑
if (isEnemyRanged)
{
_sideStepTimer--;
if (_sideStepTimer <= 0)
{
_sideStepDirection *= -1; // 切换侧走方向 (左或右)
_sideStepTimer = EVASION_SIDE_STEP_INTERVAL_TICKS;
}
// 计算侧走方向 (在X-Z平面上垂直于远离方向)
var perpendicularDirection = GetPerpendicularVectorXZ(directionAway) * _sideStepDirection;
// 侧走策略:在远离方向上叠加一个侧走分量
// 让侧走分量与远离分量结合,形成一个弧线或斜向移动
moveTargetDirection = (directionAway * (1 - EVASION_SIDE_STEP_STRENGTH) +
perpendicularDirection * EVASION_SIDE_STEP_STRENGTH).normalized;
// 或者更简单地moveTargetDirection = (directionAway + perpendicularDirection * EVASION_SIDE_STEP_STRENGTH).normalized;
// 需要根据实际效果调整EVASION_SIDE_STEP_STRENGTH的数值和结合方式。
}
// 7. 确定最终移动目标点
var fleePosition = entity.Position + moveTargetDirection * FLEE_BUFFER_DISTANCE;
// 8. 结合自己的武器类型,决定何时停止紧急移动 (和之前的逻辑类似)
if (isSelfRanged)
{
var selfAttackRange = 0f;
if (selfWeapon != null && selfWeapon.Attributes != null)
{
selfAttackRange = selfWeapon.Attributes.attackRange;
}
var safeDistanceSq = (selfAttackRange * RANGED_SAFE_DISTANCE_MULTIPLIER) *
(selfAttackRange * RANGED_SAFE_DISTANCE_MULTIPLIER);
// 如果目标在“安全距离”内,则实体需要继续远离
if (distanceSq < safeDistanceSq)
{
entity.SetTarget(fleePosition);
entity.TryMove();
}
else
{
// 已经达到足够的安全距离,此时实体可以停止紧急移动。
// FleeJob会继续运行直到timeoutTicks耗尽给予一定的缓冲时间。
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动
}
}
else // 自己是近战单位或没有武器
{
// 只要目标在“危险距离”内,就一直向外逃跑
if (distanceSq < MELEE_PERIL_DISTANCE_SQUARED)
{
entity.SetTarget(fleePosition);
entity.TryMove();
}
else
{
// 已经拉开足够远的距离,停止紧急逃跑.
// 同样FleeJob会继续运行直到timeoutTicks耗尽。
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动
}
}
}
override public void StopJob()
{
base.StopJob();
if (entity != null)
{
entity.SetTarget(entity.Position); // 确保实体停止任何正在进行的逃跑移动
}
_cachedHostileTarget = null; // 清除缓存目标以便下次StartJob时重新查找
}
/// <summary>
/// 查找执行实体最近的敌对目标。此方法可能耗时,应避免频繁调用。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNearestHostileTarget()
{
if (entity == null) return null;
return EntityManager.Instance.FindNearestEntityByRelation(
entity.currentDimensionId, // 搜索维度ID
entity.entityPrefab, // 执行实体的Prefab ID用于关系判断
Relation.Hostile)?.entity; // 寻找敌对关系的目标
}
/// <summary>
/// 获取给定向量在X-Z平面上的垂直向量。
/// 假定Y轴是世界向上方向。
/// </summary>
/// <param name="direction">原始方向向量。</param>
/// <returns>在X-Z平面上垂直于原始向量的向量顺时针旋转90度。</returns>
private Vector3 GetPerpendicularVectorXZ(Vector3 direction)
{
// 创建一个只保留XZ分量的向量
var flatDirection = new Vector3(direction.x, 0, direction.z).normalized;
// 顺时针旋转90度 (x,z) => (z,-x)
return new Vector3(flatDirection.z, 0, -flatDirection.x);
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d2497fdaa11d3554287c58d696dab7e9

View File

@@ -1,49 +0,0 @@
namespace AI
{
public class JobGiver_ContinuousMove : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return new MoveJob();
}
}
public class JobGiver_RandomWander : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return new WanderJob();
}
}
public class JobGiver_Idel : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return new IdleJob();
}
}
public class JobGiver_AttackJob : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return Managers.EntityManager.Instance.ExistsHostile(target.currentDimensionId, target.entityPrefab) ? new AttackJob() : null;
}
}
public class JobGiver_AdvancedAttackJob : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return Managers.EntityManager.Instance.ExistsHostile(target.currentDimensionId, target.entityPrefab) ? new AdvancedAttackJob() : null;
}
}
public class JobGiver_FleeJob : AIBase
{
public override JobBase GetJob(Entity.Entity target)
{
return new FleeJob();
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 30648f750dce43e493f5e94cb735988c
timeCreated: 1754974329

View File

@@ -0,0 +1,63 @@
using Data;
using Managers;
namespace AI
{
/// <summary>
/// AI行为树叶子节点让 SelfEntity 向最近的敌对实体攻击一次。
/// </summary>
public class JobNode_AttackTarget : LeafNodeBase
{
private Entity.Entity _targetHostileEntity; // 目标敌对实体
/// <summary>
/// 执行攻击最近敌对目标的逻辑。
/// </summary>
/// <returns>行为树节点状态。</returns>
protected override Status ExecuteLeafLogic()
{
// 如果没有当前目标或者当前目标已无效null或死亡则需要重新寻找目标
if (!_targetHostileEntity || _targetHostileEntity.IsDead)
{
// 1. 寻找最近的敌对实体
var hostileEntityRecord = EntityManager.Instance.FindNearestEntityByRelation(
SelfEntity.currentDimensionId,
SelfEntity.entityPrefab,
Relation.Hostile);
// 如果没有找到敌对目标,任务失败
if (!hostileEntityRecord || !hostileEntityRecord.entity)
{
// Debug.Log($"[{SelfEntity.entityDef.defName}] 行为节点<攻击目标>: 未找到敌对目标。");
return Status.Failure; // 没有敌人可攻击
}
_targetHostileEntity = hostileEntityRecord.entity;
}
// 确保找到的目标仍然有效
if (!_targetHostileEntity || _targetHostileEntity.IsDead)
{
// 如果在找到后立即目标死亡(极小概率),或者某种原因导致目标失效,也视为失败
// Debug.Log($"[{SelfEntity.entityDef.defName}] 行为节点<攻击目标>: 目标 [{_targetHostileEntity?.entityDef?.defName ?? "未知"}] 在验证时无效。");
_targetHostileEntity = null; // 清除无效目标
return Status.Failure;
}
// 2. 设置攻击方向
// 攻击方向是从 SelfEntity 指向目标实体
SelfEntity.attackDirection = _targetHostileEntity.Position - SelfEntity.Position;
// 3. 尝试攻击
var attackInitiated = SelfEntity.TryAttack();
return attackInitiated ? Status.Success : Status.Failure;
}
/// <summary>
/// 重置此攻击节点的所有内部状态。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的 Reset
_targetHostileEntity = null; // 清除目标
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1e60188f6bce4388b2b5b096c264c61c
timeCreated: 1759412540

View File

@@ -0,0 +1,23 @@
using Data;
namespace AI
{
public class JobNode_Idle:LeafNodeBase
{
protected override Status ExecuteLeafLogic()
{
return RemainingFrames == 0 ? Status.Success : Status.Running;
}
public override void Init(BehaviorTreeDef def, Entity.Entity selfEntity)
{
base.Init(def, selfEntity);
if (string.IsNullOrEmpty(def.value)) return;
if (int.TryParse(def.value, out var result))
{
TimeoutFrames=result;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 491d56d0c0404b349f88cbcfffb37da7
timeCreated: 1759076237

View File

@@ -0,0 +1,109 @@
using Data;
using Managers;
using UnityEngine;
namespace AI
{
public class JobNode_MoveToAttackRange : LeafNodeBase
{
private Entity.Entity _targetHostileEntity; // 目标敌对实体
private Vector3 _targetMovePosition; // 目标移动位置
private bool _isPathSet; // 指示当前是否已设置路径
private Vector3 _lastKnownSelfPosition; // 上次已知自身位置
private int _stuckFrameCount; // 卡住帧计数
// 卡住检测的常量
private const int MAX_STUCK_FRAMES = 10; // 实体在多少帧内没有显著移动,则认为卡住
private const float STUCK_POSITION_THRESHOLD_SQ = 0.001f; // 位置变化的平方距离阈值
// 便利属性,获取实体的攻击范围
private float AttackRange => SelfEntity.AttributesNow.attackRange;
/// <summary>
/// 执行移动到攻击范围的逻辑。
/// </summary>
/// <returns>行为树节点状态。</returns>
protected override Status ExecuteLeafLogic()
{
// 如果路径尚未设置或目标实体无效null或死亡则需要重新寻找目标并设置路径
if (!_isPathSet || !_targetHostileEntity || _targetHostileEntity.IsDead)
{
// 1. 寻找最近的敌对实体
var hostileEntityRecord = EntityManager.Instance.FindNearestEntityByRelation(
SelfEntity.currentDimensionId,
SelfEntity.entityPrefab,
Relation.Hostile);
// 如果没有找到敌对目标,任务失败
if (!hostileEntityRecord || !hostileEntityRecord.entity)
{
return Status.Failure;
}
_targetHostileEntity = hostileEntityRecord.entity;
// 2. 计算目标移动点(在敌对目标的攻击距离边缘)
// 目标点是:从敌对实体指向 SelfEntity 的向量方向上,距离敌对实体 AttackRange 远的点。
// 这样做是为了让 SelfEntity 停在敌对实体攻击范围内,而不是直接重叠。
Vector3 directionToSelf = (SelfEntity.Position - _targetHostileEntity.Position).normalized;
_targetMovePosition = _targetHostileEntity.Position + directionToSelf * AttackRange;
// 3. 通知 SelfEntity 设置路径目标
SelfEntity.SetTarget(_targetMovePosition);
_isPathSet = true;
_lastKnownSelfPosition = SelfEntity.Position; // 初始化上次已知位置,用于卡住检测
_stuckFrameCount = 0; // 重置卡住计数器
return Status.Running; // 路径已设置,开始移动
}
// 路径已设置,继续管理移动和卡住检测
// 4. 检查是否已到达目标点
if (SelfEntity.OnTargetPoint)
{
return Status.Success; // 成功移动到攻击范围
}
// 5. 卡住检测
// 计算当前位置与上次已知位置的平方距离,避免开方运算,提高性能
float currentPositionChangeSq = (SelfEntity.Position - _lastKnownSelfPosition).sqrMagnitude;
if (currentPositionChangeSq < STUCK_POSITION_THRESHOLD_SQ)
{
_stuckFrameCount++;
if (_stuckFrameCount >= MAX_STUCK_FRAMES)
{
// 实体长时间未移动,被判定为卡住,需要重新规划路径
Debug.LogWarning(
$"[{SelfEntity.entityDef.defName}] 行为节点<移动到攻击范围>: 实体卡住了! 重新计算到目标 [{_targetHostileEntity.entityDef.defName}] 的路径。");
_isPathSet = false; // 重置此标志,在下一帧会触发重新寻找目标和设置路径
return Status.Running; // 尽管卡住,但节点仍然在尝试完成任务
}
}
else
{
// 实体有移动,重置卡住计数器
_stuckFrameCount = 0;
}
// 更新上次已知位置
_lastKnownSelfPosition = SelfEntity.Position;
// 6. 持续移动
SelfEntity.TryMove();
return Status.Running; // 正在向目标移动
}
/// <summary>
/// 重置此移动节点的所有内部状态。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的 Reset重置 CurrentStatus 和 _elapsedFrames
_targetHostileEntity = null;
_targetMovePosition = Vector3.zero;
_isPathSet = false;
_lastKnownSelfPosition = Vector3.zero;
_stuckFrameCount = 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ee95e8f250364dafb2e7d7af55f30123
timeCreated: 1759411138

View File

@@ -0,0 +1,120 @@
using Data;
using UnityEngine;
namespace AI
{
public class JobNode_Wander : LeafNodeBase
{
private float _wanderRange = 10f; // 徘徊的最大范围
private float _minWanderDistance = 2f; // 随机目标点距离当前位置的最小距离,避免原地踏步
private int _maxTargetSearchAttempts = 20; // 寻找可达目标点的最大尝试次数
private float _positionCheckThreshold = 0.001f; // 检测是否卡住的距离阈值
private const int _maxStuckFrames = 5; // 超过此帧数认为卡住
private Vector3 _wanderTargetPosition; // 当前徘徊的目标点
private Vector3 _lastPosition; // 上一帧的实体位置
private int _stuckFrameCount; // 累计在同一位置的帧数
/// <summary>
/// 核心业务逻辑,用于执行随机徘徊。
/// LeafNodeBase 会在计时器管理后调用此方法。
/// </summary>
/// <returns>节点的执行状态Running, Success, Failure。</returns>
protected override Status ExecuteLeafLogic()
{
// 如果没有目标点,尝试寻找一个
if (_wanderTargetPosition == Vector3.zero) // Vector3.zero 作为未设置目标的标志
{
if (!FindRandomWanderTarget())
{
// 无法找到有效目标点,判定为失败
Debug.LogError(
$"实体 {SelfEntity.currentDimensionId} 在 {_wanderRange} 范围内尝试 {_maxTargetSearchAttempts} 次后未能找到徘徊目标。");
return Status.Failure;
}
// 找到目标后,设置初始上一帧位置,并重置卡住计数
_lastPosition = SelfEntity.Position;
_stuckFrameCount = 0;
}
// 尝试沿着路径移动
SelfEntity.TryMove();
// 检查是否到达目标点
if (SelfEntity.OnTargetPoint)
{
return Status.Success;
}
// 检查是否卡住
var currentPosition = SelfEntity.Position;
// 使用 SqrMagnitude 比 Distance 略快,因为它避免了开方根运算
if (Vector3.SqrMagnitude(currentPosition - _lastPosition) <
_positionCheckThreshold * _positionCheckThreshold)
{
_stuckFrameCount++;
if (_stuckFrameCount > _maxStuckFrames)
{
Debug.LogWarning(
$"实体 {SelfEntity.currentDimensionId} 在 {currentPosition} 卡住 {_stuckFrameCount} 帧。当前目标: {_wanderTargetPosition}");
return Status.Failure; // 卡住,返回失败
}
}
else
{
_stuckFrameCount = 0; // 位置发生变化,重置卡住计数
_lastPosition = currentPosition;
}
// 既未成功也未失败,继续运行
return Status.Running;
}
/// <summary>
/// 寻找一个随机且可达的徘徊目标点。
/// </summary>
/// <returns>如果成功找到并设置目标点则返回true否则返回false。</returns>
private bool FindRandomWanderTarget()
{
var currentEntityPosition = SelfEntity.Position;
var currentDimensionId = SelfEntity.currentDimensionId;
for (var i = 0; i < _maxTargetSearchAttempts; i++)
{
// 生成相对当前位置的随机偏移
var randomOffset = Random.insideUnitCircle * _wanderRange;
var potentialTarget =
currentEntityPosition + new Vector3(randomOffset.x, randomOffset.y, 0); // 假定2D平面Z轴不变
// 确保新目标点与当前位置有足够的距离
if (Vector3.Distance(potentialTarget, currentEntityPosition) < _minWanderDistance)
{
continue; // 距离太近,重试
}
// 检查目标点的可达性
var travelCost = Program.Instance.GetDimension(currentDimensionId).landform
.GetTravelCost(potentialTarget);
if (travelCost < 1.0f) // 小于1表示可达
{
_wanderTargetPosition = potentialTarget;
SelfEntity.SetTarget(_wanderTargetPosition);
return true;
}
}
return false; // 达到最大尝试次数仍未找到有效目标
}
/// <summary>
/// 重置叶节点的状态,包括基类状态和内部计时器。
/// </summary>
public override void Reset()
{
base.Reset(); // 调用基类的Reset方法将CurrentStatus设为Ready并重置_elapsedFrames
_wanderTargetPosition = Vector3.zero; // 重置目标点
_lastPosition = Vector3.zero; // 重置上一帧位置
_stuckFrameCount = 0; // 重置卡住计数
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 50e73e1612a44e15a3899f586a308450
timeCreated: 1759228892

View File

@@ -0,0 +1,76 @@
using System;
namespace AI
{
public abstract class LeafNodeBase : BehaviorTreeBase
{
// 已经运行的帧数
protected int _elapsedFrames;
// 默认超时帧数
protected int _timeoutFrames = 180;
/// <summary>
/// 获取或设置该叶节点的超时帧数。确保超时帧数至少为1。
/// </summary>
public int TimeoutFrames
{
get => _timeoutFrames;
set => _timeoutFrames = Math.Max(1, value);
}
// 剩余帧数
public int RemainingFrames => TimeoutFrames - _elapsedFrames;
/// <summary>
/// 抽象方法,由具体的叶节点实现其核心业务逻辑。
/// <see cref="LeafNodeBase"/> 会在计时器管理后调用此方法。
/// </summary>
/// <returns>节点的执行状态Running, Success, Failure。</returns>
protected abstract Status ExecuteLeafLogic();
/// <summary>
/// <see cref="LeafNodeBase"/> 的 Tick 方法,包含了计时器和状态管理逻辑。
/// 此方法被密封,子类应实现 <see cref="ExecuteLeafLogic"/> 而不是重写 Tick。
/// </summary>
public sealed override Status Tick()
{
if (CurrentStatus == Status.Ready)
{
CurrentStatus = Status.Running;
_elapsedFrames = 0;
}
if (CurrentStatus == Status.Running)
{
_elapsedFrames++;
if (_elapsedFrames > _timeoutFrames)
{
CurrentStatus = Status.Failure;
Reset();
return Status.Failure;
}
var logicResult = ExecuteLeafLogic();
if (logicResult != Status.Running)
{
CurrentStatus = logicResult;
Reset();
}
return logicResult;
}
return CurrentStatus;
}
/// <summary>
/// 重置叶节点的状态,包括基类状态和内部计时器。
/// </summary>
public override void Reset()
{
base.Reset();
_elapsedFrames = 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ed23d197e8fe4e34b317c9828589338a
timeCreated: 1759058958