mirror of
http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite.git
synced 2025-11-20 07:37:12 +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:
@@ -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; // 如果没有指定条件,则条件始终不满足
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 的子类");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b711456a12f4bf6a29b0de14a2d7d8f
|
||||
timeCreated: 1754982046
|
||||
37
Client/Assets/Scripts/AI/BehaviorTreeBase.cs
Normal file
37
Client/Assets/Scripts/AI/BehaviorTreeBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
398
Client/Assets/Scripts/AI/CompositeNodeBase.cs
Normal file
398
Client/Assets/Scripts/AI/CompositeNodeBase.cs
Normal 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; // 重置内部状态
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/CompositeNodeBase.cs.meta
Normal file
3
Client/Assets/Scripts/AI/CompositeNodeBase.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60ed77d7b0d8475b915d95040d33bb34
|
||||
timeCreated: 1759058538
|
||||
45
Client/Assets/Scripts/AI/ConstantNode.cs
Normal file
45
Client/Assets/Scripts/AI/ConstantNode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/ConstantNode.cs.meta
Normal file
3
Client/Assets/Scripts/AI/ConstantNode.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdb3950442f3454db89d7f566a4c5404
|
||||
timeCreated: 1759059830
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2497fdaa11d3554287c58d696dab7e9
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30648f750dce43e493f5e94cb735988c
|
||||
timeCreated: 1754974329
|
||||
63
Client/Assets/Scripts/AI/JobNode_AttackTarget.cs
Normal file
63
Client/Assets/Scripts/AI/JobNode_AttackTarget.cs
Normal 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; // 清除目标
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/JobNode_AttackTarget.cs.meta
Normal file
3
Client/Assets/Scripts/AI/JobNode_AttackTarget.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e60188f6bce4388b2b5b096c264c61c
|
||||
timeCreated: 1759412540
|
||||
23
Client/Assets/Scripts/AI/JobNode_Idle.cs
Normal file
23
Client/Assets/Scripts/AI/JobNode_Idle.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/JobNode_Idle.cs.meta
Normal file
3
Client/Assets/Scripts/AI/JobNode_Idle.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 491d56d0c0404b349f88cbcfffb37da7
|
||||
timeCreated: 1759076237
|
||||
109
Client/Assets/Scripts/AI/JobNode_MoveToAttackRange.cs
Normal file
109
Client/Assets/Scripts/AI/JobNode_MoveToAttackRange.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee95e8f250364dafb2e7d7af55f30123
|
||||
timeCreated: 1759411138
|
||||
120
Client/Assets/Scripts/AI/JobNode_Wander.cs
Normal file
120
Client/Assets/Scripts/AI/JobNode_Wander.cs
Normal 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; // 重置卡住计数
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/JobNode_Wander.cs.meta
Normal file
3
Client/Assets/Scripts/AI/JobNode_Wander.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50e73e1612a44e15a3899f586a308450
|
||||
timeCreated: 1759228892
|
||||
76
Client/Assets/Scripts/AI/LeafNodeBase.cs
Normal file
76
Client/Assets/Scripts/AI/LeafNodeBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Client/Assets/Scripts/AI/LeafNodeBase.cs.meta
Normal file
3
Client/Assets/Scripts/AI/LeafNodeBase.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed23d197e8fe4e34b317c9828589338a
|
||||
timeCreated: 1759058958
|
||||
Reference in New Issue
Block a user