From a48ccca5f4eb357869ba664f7c390af6d2dbc7a4 Mon Sep 17 00:00:00 2001 From: m0_75251201 Date: Sun, 20 Jul 2025 20:41:37 +0800 Subject: [PATCH] =?UTF-8?q?(client)=20feat:=E6=B7=BB=E5=8A=A0=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E6=A0=91=E5=92=8C=E5=B7=A5=E4=BD=9C=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client/Assets/Scripts/AI/AIBase.cs | 63 +++++++++ .../Scripts/{Entity => AI}/AIBase.cs.meta | 0 Client/Assets/Scripts/AI/JobBase.cs | 38 +++++ .../Assets/Scripts/Data/AttributesDefine.cs | 13 ++ Client/Assets/Scripts/Data/PawnDefine.cs | 3 +- Client/Assets/Scripts/Entity/AIBase.cs | 35 ----- Client/Assets/Scripts/Entity/Entity.cs | 30 +++- Client/Assets/Scripts/Managers/Generator.cs | 9 +- Client/Assets/Scripts/Prefab/EntityPrefab.cs | 66 ++++++++- Client/Assets/Scripts/Utils/Resolver.cs | 131 ++++++++++++++++++ Client/Assets/Scripts/Utils/Resolver.cs.meta | 3 + Client/Data/Core/Define/Item/Weapon.xml | 3 + 12 files changed, 350 insertions(+), 44 deletions(-) create mode 100644 Client/Assets/Scripts/AI/AIBase.cs rename Client/Assets/Scripts/{Entity => AI}/AIBase.cs.meta (100%) create mode 100644 Client/Assets/Scripts/AI/JobBase.cs delete mode 100644 Client/Assets/Scripts/Entity/AIBase.cs create mode 100644 Client/Assets/Scripts/Utils/Resolver.cs create mode 100644 Client/Assets/Scripts/Utils/Resolver.cs.meta diff --git a/Client/Assets/Scripts/AI/AIBase.cs b/Client/Assets/Scripts/AI/AIBase.cs new file mode 100644 index 0000000..e835155 --- /dev/null +++ b/Client/Assets/Scripts/AI/AIBase.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AI +{ + public abstract class AIBase + { + public List children = new(); + + public virtual JobBase GetJob(Entity.Entity target) + { + foreach (var aiBase in children) + { + var job = aiBase.GetJob(target); + if (job != null) + return job; + } + return null; + } + } + + public class ContinuousMove : AIBase + { + override public JobBase GetJob(Entity.Entity target) + { + + return null; + } + } + + public class TrackPlayer : AIBase + { + + } + public class WanderNode : AIBase + { + } + public class ConditionalAI : AIBase + { + // 条件函数,返回 true 表示满足条件 + private Func condition; + + // 构造函数,传入条件函数 + public ConditionalAI(Func conditionFunc) + { + condition = conditionFunc; + } + + public override JobBase GetJob(Entity.Entity target) + { + // 检查条件是否满足 + if (condition != null && condition(target)) + { + // 如果条件满足,继续查找子节点的任务 + return base.GetJob(target); + } + + // 条件不满足,直接返回 null + return null; + } + } +} \ No newline at end of file diff --git a/Client/Assets/Scripts/Entity/AIBase.cs.meta b/Client/Assets/Scripts/AI/AIBase.cs.meta similarity index 100% rename from Client/Assets/Scripts/Entity/AIBase.cs.meta rename to Client/Assets/Scripts/AI/AIBase.cs.meta diff --git a/Client/Assets/Scripts/AI/JobBase.cs b/Client/Assets/Scripts/AI/JobBase.cs new file mode 100644 index 0000000..56c003c --- /dev/null +++ b/Client/Assets/Scripts/AI/JobBase.cs @@ -0,0 +1,38 @@ +using Base; +using Unity.VisualScripting; + +namespace AI +{ + public abstract class JobBase + { + public Entity.Entity entity; + private int timeoutTicks = 1000; + public bool Running=>timeoutTicks > 0; + + public virtual void StartJob(Entity.Entity target) + { + entity = target; + } + + protected abstract void UpdateJob(); + + public bool Update() + { + if(!Running) + return false; + UpdateJob(); + timeoutTicks--; + if (timeoutTicks <= 0) + { + StopJob(); + } + return true; + } + public virtual void StopJob() + { + timeoutTicks = 0; + } + } + + +} \ No newline at end of file diff --git a/Client/Assets/Scripts/Data/AttributesDefine.cs b/Client/Assets/Scripts/Data/AttributesDefine.cs index a61b218..434e732 100644 --- a/Client/Assets/Scripts/Data/AttributesDefine.cs +++ b/Client/Assets/Scripts/Data/AttributesDefine.cs @@ -11,5 +11,18 @@ namespace Data public int attackSpeed = 2; public int attackRange = 3; public int attackTargetCount = 1; + public AttributesDef Clone() + { + return new AttributesDef + { + health = this.health, + moveSpeed = this.moveSpeed, + attack = this.attack, + defense = this.defense, + attackSpeed = this.attackSpeed, + attackRange = this.attackRange, + attackTargetCount = this.attackTargetCount + }; + } } } \ No newline at end of file diff --git a/Client/Assets/Scripts/Data/PawnDefine.cs b/Client/Assets/Scripts/Data/PawnDefine.cs index 54dd572..8620459 100644 --- a/Client/Assets/Scripts/Data/PawnDefine.cs +++ b/Client/Assets/Scripts/Data/PawnDefine.cs @@ -7,6 +7,7 @@ namespace Data { public class PawnDef : Define { + public AttributesDef attributes; public string aiController; public string texturePath = null; public DrawingOrderDef @@ -15,7 +16,7 @@ namespace Data drawingOrder_left, drawingOrder_right; - public string[] behaviorTree; + public BehaviorTreeDef behaviorTree; public DrawingOrderDef GetDrawingOrder(Orientation orientation) diff --git a/Client/Assets/Scripts/Entity/AIBase.cs b/Client/Assets/Scripts/Entity/AIBase.cs deleted file mode 100644 index d6bf6fa..0000000 --- a/Client/Assets/Scripts/Entity/AIBase.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace Entity -{ - public abstract class AIBase - { - public virtual bool Run(Entity target) - { - foreach (var aiBase in children) - { - if (aiBase.Run(target)) - return true; - } - - return false; - } - public List children=new(); - } - - public class ContinuousMove : AIBase - { - override public bool Run(Entity target) - { - target.gameObject.transform.position += - Time.deltaTime * target.runtimeAttributes.moveSpeed * target.direction; - return true; - } - } - - public class TrackPlayer : AIBase - { - - } -} \ No newline at end of file diff --git a/Client/Assets/Scripts/Entity/Entity.cs b/Client/Assets/Scripts/Entity/Entity.cs index da4ad2a..fecd20a 100644 --- a/Client/Assets/Scripts/Entity/Entity.cs +++ b/Client/Assets/Scripts/Entity/Entity.cs @@ -1,22 +1,42 @@ using System.Collections.Generic; +using AI; using Base; +using Data; using UnityEngine; namespace Entity { public abstract class Entity:MonoBehaviour,ITick { + public string name; public bool playerControlled = false; - public List aiTree=new(); - public Data.AttributesDef runtimeAttributes; + public AIBase aiTree; + public JobBase currentJob; + public AttributesDef runtimeAttributes; public Vector3 direction; + + private const int WarningInterval = 5000; + private int warningTicks = 0; + public void Tick() { - foreach (var aiBase in aiTree) + if (currentJob == null || !currentJob.Running) { - if (aiBase.Run(this)) - break; + currentJob = aiTree.GetJob(this); + if (currentJob == null) + { + if (warningTicks<=0) + { + Debug.LogWarning($"{GetType().Name}类型的{name}没有分配到任何工作,给行为树末尾添加等待行为,避免由于没有工作导致无意义的反复查找工作导致性能问题"); + warningTicks += WarningInterval; + } + + warningTicks--; + return; + } } + + currentJob.Update(); } public virtual void TryAttck() diff --git a/Client/Assets/Scripts/Managers/Generator.cs b/Client/Assets/Scripts/Managers/Generator.cs index b3b4c47..80d5a06 100644 --- a/Client/Assets/Scripts/Managers/Generator.cs +++ b/Client/Assets/Scripts/Managers/Generator.cs @@ -1,3 +1,4 @@ +using Prefab; using UnityEngine; namespace Managers @@ -5,11 +6,17 @@ namespace Managers public class Generator:MonoBehaviour { public GameObject entityLevel; - + public EntityPrefab entityPrefab; public void GenerateEntity(Data.PawnDef pawnDef, Vector3 pos) { + if (entityPrefab == null || pawnDef == null) + return; + GameObject entity = Instantiate(entityPrefab.gameObject, pos, Quaternion.identity, entityLevel.transform); + // entity.name = pawnDef.name; + var entityComponent = entity.GetComponent(); + entityComponent?.Init(pawnDef); } } } \ No newline at end of file diff --git a/Client/Assets/Scripts/Prefab/EntityPrefab.cs b/Client/Assets/Scripts/Prefab/EntityPrefab.cs index 8c61ffa..eaa7c50 100644 --- a/Client/Assets/Scripts/Prefab/EntityPrefab.cs +++ b/Client/Assets/Scripts/Prefab/EntityPrefab.cs @@ -1,9 +1,71 @@ +using System; +using AI; +using Data; +using Entity; +using Unity.VisualScripting; using UnityEngine; namespace Prefab { - public class EntityPrefab:MonoBehaviour + public class EntityPrefab : MonoBehaviour { - + public Entity.Entity entity; + public void Init(Data.PawnDef pawnDef) + { + entity.runtimeAttributes = pawnDef.attributes.Clone(); + entity.aiTree = ConvertToAIBase(pawnDef.behaviorTree); + } + public static AIBase ConvertToAIBase(BehaviorTreeDef behaviorTreeDef) + { + if (behaviorTreeDef == null) + return null; + AIBase aiBase = CreateAIBaseInstance(behaviorTreeDef.className); + if (behaviorTreeDef.childTree != null) + { + foreach (var child in behaviorTreeDef.childTree) + { + if (child != null) + { + aiBase.children.Add(ConvertToAIBase(child)); + } + } + } + return aiBase; + } + + // 使用反射根据 className 创建具体的 AIBase 子类实例 + private static AIBase CreateAIBaseInstance(string className) + { + if (string.IsNullOrEmpty(className)) + throw new ArgumentException("className 不能为空"); + + // 定义可能的命名空间列表 + 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 的子类"); + } } } \ No newline at end of file diff --git a/Client/Assets/Scripts/Utils/Resolver.cs b/Client/Assets/Scripts/Utils/Resolver.cs new file mode 100644 index 0000000..d2611e5 --- /dev/null +++ b/Client/Assets/Scripts/Utils/Resolver.cs @@ -0,0 +1,131 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace Utils +{ + public static class Resolver + { + /// + /// 将字符串表达式解析为一个谓词函数,该函数可以用于筛选实体对象。 + /// + /// 表示条件的字符串表达式。格式示例:"entity.Id > 10" 或 "entity.Name == 'John'"。 + /// 返回一个 Func<Entity.Entity, bool> 类型的委托,表示解析后的谓词函数。 + /// 当输入表达式的格式不正确时抛出此异常。 + /// 当表达式中包含不支持的操作符或数据类型时抛出此异常。 + /// + /// 表达式的格式必须符合以下规则: + /// - 表达式由三部分组成:属性路径、操作符和值,用空格分隔。 + /// - 属性路径格式为 "entity.PropertyName",其中 PropertyName 是实体类中的一个公共属性或字段。 + /// - 操作符可以是以下之一:">", "<", ">=", "<=", "==", "!="。 + /// - 值的类型必须与属性的类型匹配,并且支持以下类型:string, int, long, float, double, decimal, bool, DateTime, Guid 和枚举类型。 + /// + /// 注意事项: + /// - 字符串值需要用单引号或双引号括起来,例如 'John' 或 "John"。 + /// - 对于可为空类型(Nullable),会自动处理其底层类型的转换。 + /// - 字符串比较默认使用不区分大小写的 Equals 方法。 + /// + public static Func ParsePredicate(string expression) + { + // 格式示例:"entity.Id > 10" 或 "entity.Name == 'John'" + var parts = expression.Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 3) + throw new FormatException( + "Invalid expression format. Expected format: 'entity.Property Operator Value'"); + + // 解析属性和操作符 + var propPath = parts[0].Split('.')[1]; // "Id" 或 "Name" + var op = parts[1]; // ">", "==" 等 + + // 创建表达式参数 + var param = Expression.Parameter(typeof(Entity.Entity), "entity"); + + // 获取属性访问表达式 + var propAccess = propPath.Split('.') + .Aggregate(param, Expression.PropertyOrField); + + // 获取属性类型 + var propType = propAccess.Type; + + // 解析值并转换为适当类型 + object value; + var valueStr = parts[2].Trim(); + + try + { + if (propType == typeof(string)) + // 处理字符串值(去除引号) + value = valueStr.Trim('\'', '"'); + else if (propType == typeof(int)) + value = int.Parse(valueStr); + else if (propType == typeof(long)) + value = long.Parse(valueStr); + else if (propType == typeof(float)) + value = float.Parse(valueStr); + else if (propType == typeof(double)) + value = double.Parse(valueStr); + else if (propType == typeof(decimal)) + value = decimal.Parse(valueStr); + else if (propType == typeof(bool)) + value = bool.Parse(valueStr); + else if (propType == typeof(DateTime)) + value = DateTime.Parse(valueStr); + else if (propType == typeof(Guid)) + value = Guid.Parse(valueStr); + else if (propType.IsEnum) + value = Enum.Parse(propType, valueStr); + else + throw new NotSupportedException($"Type {propType.Name} is not supported"); + } + catch (Exception ex) + { + throw new FormatException($"Failed to parse value '{valueStr}' for type {propType.Name}", ex); + } + + // 创建常量表达式(确保类型匹配) + var constant = Expression.Constant(value, propType); + + // 处理可为空类型的情况 + if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var underlyingType = Nullable.GetUnderlyingType(propType); + propAccess = Expression.Property(propAccess, "Value"); + constant = Expression.Constant(Convert.ChangeType(value, underlyingType), underlyingType); + } + + // 创建比较表达式 + Expression comparison; + if (propType == typeof(string) && (op == "==" || op == "!=")) + { + // 字符串特殊处理:使用Equals方法进行不区分大小写的比较 + var equalsMethod = + typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }); + var methodCall = Expression.Call( + propAccess, + equalsMethod, + constant, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + + comparison = op == "==" ? methodCall : Expression.Not(methodCall); + } + else + { + // 其他类型使用标准二元运算符 + comparison = op switch + { + ">" => Expression.GreaterThan(propAccess, constant), + "<" => Expression.LessThan(propAccess, constant), + ">=" => Expression.GreaterThanOrEqual(propAccess, constant), + "<=" => Expression.LessThanOrEqual(propAccess, constant), + "==" => Expression.Equal(propAccess, constant), + "!=" => Expression.NotEqual(propAccess, constant), + _ => throw new NotSupportedException($"Operator {op} not supported") + }; + } + + // 编译为委托 + var lambda = Expression.Lambda>(comparison, param); + return lambda.Compile(); + } + } +} \ No newline at end of file diff --git a/Client/Assets/Scripts/Utils/Resolver.cs.meta b/Client/Assets/Scripts/Utils/Resolver.cs.meta new file mode 100644 index 0000000..5c5fd3c --- /dev/null +++ b/Client/Assets/Scripts/Utils/Resolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: acd36958b991488f92c4b9a8b903c0be +timeCreated: 1753012536 \ No newline at end of file diff --git a/Client/Data/Core/Define/Item/Weapon.xml b/Client/Data/Core/Define/Item/Weapon.xml index 4065b5f..cc66050 100644 --- a/Client/Data/Core/Define/Item/Weapon.xml +++ b/Client/Data/Core/Define/Item/Weapon.xml @@ -25,6 +25,9 @@ Resources\Item\YellowBullet.png + + +