Unity3D战争策略游戏开发:从A*寻路到多兵种AI协同设计

张开发
2026/4/12 16:53:28 15 分钟阅读

分享文章

Unity3D战争策略游戏开发:从A*寻路到多兵种AI协同设计
1. 战争策略游戏中的A*寻路实战第一次在Unity里实现A算法时我被它的高效惊艳到了。想象一下你在地铁站找最短路线时大脑会自动避开墙壁和障碍物——A算法就是让游戏角色拥有这种智能。在战争策略游戏中这个算法决定了你的士兵能否聪明地绕过城墙攻击敌人。具体实现时我习惯用PriorityQueue来处理开放列表。下面这段代码展示了核心逻辑public ListNode FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode grid.NodeFromWorldPoint(startPos); Node targetNode grid.NodeFromWorldPoint(targetPos); HeapNode openSet new HeapNode(grid.MaxSize); HashSetNode closedSet new HashSetNode(); openSet.Add(startNode); while (openSet.Count 0) { Node currentNode openSet.RemoveFirst(); closedSet.Add(currentNode); if (currentNode targetNode) { return RetracePath(startNode, targetNode); } foreach (Node neighbour in grid.GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) continue; int newMovementCost currentNode.gCost GetDistance(currentNode, neighbour); if (newMovementCost neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost newMovementCost; neighbour.hCost GetDistance(neighbour, targetNode); neighbour.parent currentNode; if (!openSet.Contains(neighbour)) openSet.Add(neighbour); } } } return null; }实际项目中遇到过路径抖动问题当多个单位同时寻路时会出现跳舞现象。解决方案是引入路径平滑算法和移动预测用二次贝塞尔曲线处理拐角。还有个坑是地形权重沼泽地形应该比平原移动成本更高这需要在网格初始化时设置不同的移动惩罚值。2. 多兵种AI状态机设计给兽人战士和精灵射手写AI时我意识到不能用同一套逻辑。就像你不能要求弓箭手像坦克一样冲锋不同兵种需要独立的状态机。我的解决方案是基类组合模式public abstract class UnitAI : MonoBehaviour { protected enum State { Idle, Move, Attack, Dead } protected State currentState; public abstract void UpdateAI(); protected virtual void ChangeState(State newState) { // 状态转换通用逻辑 } } public class MeleeAI : UnitAI { public override void UpdateAI() { switch(currentState) { case State.Idle: ScanForEnemies(); break; case State.Move: if (InAttackRange()) ChangeState(State.Attack); break; case State.Attack: if (!InAttackRange()) ChangeState(State.Move); break; } } }远程单位需要额外考虑弹道计算。我参考了《帝国时代》的抛物线算法加入重力模拟和提前量预测void FireProjectile() { Vector3 targetVelocity target.GetComponentRigidbody().velocity; float projectileSpeed arrowSpeed; Vector3 predictedPosition PredictTargetPosition(transform.position, target.position, targetVelocity, projectileSpeed); // 计算发射角度 float angle CalculateLaunchAngle(transform.position, predictedPosition, projectileSpeed); // 应用初始速度 rigidbody.velocity Quaternion.AngleAxis(angle, transform.right) * transform.forward * projectileSpeed; }调试时发现状态切换太频繁会导致单位抽搐后来加入了状态冷却时间和过渡动画。比如从移动到攻击会有0.3秒的准备动作这样看起来更自然。3. 群体协同作战系统当20个兽人战士同时冲向箭塔时如果都挤在同一个点就太假了。我参考了《全面战争》的阵型系统开发了基于势场的群体控制领导节点每个小队有个虚拟领导节点成员保持相对位置排斥力单位间保持最小间距路径跟随整体沿路径移动时保持队形void UpdateFormation() { foreach (Unit unit in squad) { Vector3 targetPos leader.position formationOffsets[unit.formationIndex]; // 叠加排斥力 foreach (Unit other in squad) { if (other ! unit) { float distance Vector3.Distance(unit.position, other.position); if (distance minSpacing) { Vector3 repelDir (unit.position - other.position).normalized; targetPos repelDir * (minSpacing - distance) * 0.5f; } } } unit.SetMoveTarget(targetPos); } }实战中发现密集阵型容易被范围技能克制于是增加了动态阵型切换。当检测到敌方法师施法时单位会自动散开类似《星际争霸》中的散兵操作。这需要实时计算安全位置void AvoidAOE(Vector3 epicenter, float radius) { foreach (Unit unit in affectedUnits) { Vector3 dir (unit.position - epicenter).normalized; Vector3 safePos epicenter dir * (radius safeMargin); unit.SetMoveTarget(safePos); } }4. 攻击优先级与决策系统新手常犯的错误是让所有单位攻击最近目标这会导致火力分散。好的策略游戏应该像下棋一样需要战术选择。我设计了基于权重的目标选择系统因素权重说明威胁值0.4高攻击单位优先距离0.3考虑移动耗时剩余血量0.2优先击杀残血单位类型0.1弓箭手优先打法系public Unit ChooseBestTarget(ListUnit targets) { Unit bestTarget null; float highestScore float.MinValue; foreach (Unit target in targets) { float distanceScore 1 / (Vector3.Distance(transform.position, target.position) 1); float hpScore (1 - target.health / target.maxHealth) * 0.5f; float typeScore GetTypeMatchScore(unitType, target.unitType); float threatScore target.attackPower * 0.01f; float totalScore distanceScore * 0.3f hpScore * 0.2f typeScore * 0.1f threatScore * 0.4f; if (totalScore highestScore) { highestScore totalScore; bestTarget target; } } return bestTarget; }曾遇到弓箭手集火坦克而忽略后排治疗师的问题后来加入了仇恨系统。治疗行为会增加仇恨值当仇恨值超过阈值时远程单位会切换目标。仇恨值随时间衰减void UpdateThreatTable() { foreach (Unit enemy in visibleEnemies) { // 基础仇恨 float baseThreat enemy.attackPower * 0.5f; // 治疗行为增加额外仇恨 if (enemy.lastAction ActionType.Heal) baseThreat * 2f; // 距离修正 float distanceModifier 1 / (Vector3.Distance(transform.position, enemy.position) 1); // 更新仇恨值 threatTable[enemy] threatTable.ContainsKey(enemy) ? threatTable[enemy] * 0.95f baseThreat * distanceModifier : baseThreat * distanceModifier; } }5. 性能优化实战技巧当测试场景出现100单位时帧数直接掉到20以下。通过Profiler分析发现主要瓶颈在两个方面寻路计算和碰撞检测。我的优化方案如下寻路优化使用Job System并行计算路径分层寻路大地图用粗网格近距离用精细网格路径共享相同目标点的单位共享路径计算结果// 使用Burst编译的Job [BurstCompile] struct PathfindingJob : IJobParallelFor { public NativeArrayVector3 startPositions; public NativeArrayVector3 targetPositions; public NativeArrayNativeListVector3 results; public void Execute(int index) { // 寻路计算逻辑... } }碰撞检测优化将单位碰撞体改为胶囊体比Mesh Collider高效使用Physics.OverlapSphereNonAlloc避免GC按阵营分区检测减少不必要的计算void DetectEnemies() { Collider[] hits new Collider[maxDetect]; // 预分配内存 int count Physics.OverlapSphereNonAlloc(transform.position, detectRadius, hits, enemyLayer); for (int i 0; i count; i) { // 处理检测到的敌人 } }还有个容易被忽视的优化点是动画系统。最初每个单位都有自己的Animator当数量超过50时开销巨大。后来改用GPU Instancing合并相同动画并使用Animator Controller的共享功能性能提升了60%。6. 调试与可视化工具开发AI系统时看不到内部状态就像蒙眼调试。我建立了一套可视化调试系统路径绘制用Gizmos显示当前路径和障碍物状态标识单位头顶显示当前状态(移动/攻击/闲置)感知范围绘制扇形视野范围决策日志记录重要决策原因void OnDrawGizmosSelected() { // 绘制移动路径 if (currentPath ! null) { Gizmos.color Color.yellow; for (int i 0; i currentPath.Count - 1; i) { Gizmos.DrawLine(currentPath[i], currentPath[i1]); } } // 绘制攻击范围 Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); // 绘制视野范围 DrawViewCone(transform.position, transform.forward, viewAngle, viewDistance); }遇到最头疼的bug是单位偶尔会卡在墙角。最终解决方案是结合了三种检测移动超时检测超过预期时间未到达碰撞体穿透检测路径阻塞检测当触发任一条件时单位会尝试以下恢复步骤后退一段距离重新计算路径如果仍失败短暂无敌穿过障碍IEnumerator HandleStuck() { isStuck true; // 后退 Vector3 retreatDir -transform.forward; float retreatTime 0.5f; while (retreatTime 0) { transform.position retreatDir * moveSpeed * Time.deltaTime; retreatTime - Time.deltaTime; yield return null; } // 重新寻路 CalculateNewPath(); isStuck false; }7. 数据驱动的AI配置硬编码AI参数是灾难的开始。我改用ScriptableObject来配置不同兵种的行为参数[CreateAssetMenu] public class AIConfig : ScriptableObject { [Header(Movement)] public float moveSpeed 3f; public float rotationSpeed 10f; public float stoppingDistance 1f; [Header(Combat)] public float attackRange 2f; public float attackRate 1f; public int attackDamage 10; [Header(Detection)] public float viewDistance 10f; [Range(0,360)] public float viewAngle 90f; public float memoryDuration 5f; }这样设计师可以自由调整平衡性而不需要改代码。更进一步我实现了动态难度调整(DDA)系统根据玩家表现实时调整AI参数void UpdateDifficulty() { float playerPerformance CalculatePlayerPerformance(); float difficultyModifier Mathf.Lerp(0.8f, 1.2f, playerPerformance); foreach (Unit enemy in activeEnemies) { enemy.moveSpeed * difficultyModifier; enemy.attackRate * difficultyModifier; enemy.viewDistance * difficultyModifier; } }项目后期引入了行为树插件对于复杂BOSS行为特别有用。比如一个龙形BOSS会有阶段转换阶段一地面爪击血量70%起飞火球血量30%召唤小怪狂暴// 行为树节点示例 public class BossPhaseNode : BehaviorTree.Node { public float phaseThreshold; public BehaviorTree.Node phaseNode; public override Status Evaluate() { if (boss.health / boss.maxHealth phaseThreshold) { return phaseNode.Evaluate(); } return Status.Failure; } }

更多文章