8. AI队友与敌方
2026/4/15大约 5 分钟
8. AI队友与敌方:让电脑替你打架
8.1 行为树简介
MOBA 游戏的 AI 需要做很多决策:什么时候打小兵、什么时候打野怪、什么时候去支援队友、什么时候撤退……这些决策逻辑如果用一堆 if-else 写,代码会变成一团乱麻。
行为树(Behavior Tree)是游戏 AI 的标准解决方案。它把所有可能的行为组织成一棵树,AI 从根节点开始,按规则逐层评估,最终决定"做什么"。
行为树的基本结构
选择节点(Selector)—— 从左到右,执行第一个成功的子节点
├── 序列节点(Sequence)—— 从左到右,全部成功才算成功
│ ├── 条件:血量低于20%?
│ └── 动作:撤退回基地
├── 序列节点
│ ├── 条件:附近有敌方英雄?
│ ├── 条件:血量高于50%?
│ └── 动作:攻击敌方英雄
├── 序列节点
│ ├── 条件:有可以打的野怪?
│ └── 动作:去打野
└── 动作:沿路线清小兵(默认行为)理解行为树
想象你在点外卖:先看冰箱有没有剩饭(条件),有就热一下吃(动作);没有就看外卖APP(条件),选一家下单(动作);如果APP打不开,就去楼下便利店(动作)。行为树就是按优先级排列的"如果...就..."链条。
8.2 行为树节点实现
C#
// BTNode.cs - 行为树节点基类
using Godot;
public enum BTStatus { Success, Failure, Running }
public partial class BTNode : Node
{
public virtual BTStatus Tick(double delta, AIContext context)
{
return BTStatus.Failure;
}
}
// BTSelector.cs - 选择节点:执行第一个成功的子节点
public partial class BTSelector : BTNode
{
public override BTStatus Tick(double delta, AIContext context)
{
foreach (var child in GetChildren())
{
var btChild = child as BTNode;
if (btChild == null) continue;
var status = btChild.Tick(delta, context);
if (status != BTStatus.Failure)
return status; // 成功或运行中,直接返回
}
return BTStatus.Failure; // 全部失败
}
}
// BTSequence.cs - 序列节点:全部子节点成功才算成功
public partial class BTSequence : BTNode
{
public override BTStatus Tick(double delta, AIContext context)
{
foreach (var child in GetChildren())
{
var btChild = child as BTNode;
if (btChild == null) continue;
var status = btChild.Tick(delta, context);
if (status != BTStatus.Success)
return status; // 失败或运行中,直接返回
}
return BTStatus.Success; // 全部成功
}
}GDScript
# bt_node.gd - 行为树节点基类
class_name BTNode
extends Node
enum Status { SUCCESS, FAILURE, RUNNING }
func tick(delta: float, context: AIContext) -> int:
return Status.FAILURE
# bt_selector.gd - 选择节点
class_name BTSelector
extends BTNode
func tick(delta: float, context: AIContext) -> int:
for child in get_children():
var bt_child = child as BTNode
if bt_child == null:
continue
var status = bt_child.tick(delta, context)
if status != Status.FAILURE:
return status
return Status.FAILURE
# bt_sequence.gd - 序列节点
class_name BTSequence
extends BTNode
func tick(delta: float, context: AIContext) -> int:
for child in get_children():
var bt_child = child as BTNode
if bt_child == null:
continue
var status = bt_child.tick(delta, context)
if status != Status.SUCCESS:
return status
return Status.SUCCESS8.3 AI 上下文
AI 需要感知周围环境才能做决策。我们把环境信息打包成一个"上下文"对象:
C#
// AIContext.cs
using Godot;
using System.Collections.Generic;
public partial class AIContext : RefCounted
{
public HeroBase Hero { get; set; }
public float HpPercent => Hero.CurrentHp / Hero.GetMaxHp();
public float MpPercent => Hero.CurrentMp / Hero.Data.MaxMp;
// 感知信息
public List<Node3D> VisibleEnemies { get; set; } = new();
public List<Node3D> VisibleAllies { get; set; } = new();
public List<Node3D> NearbyMinions { get; set; } = new();
public List<Node3D> NearbyJungleMonsters { get; set; } = new();
public Node3D NearestEnemyTower { get; set; }
public Node3D NearestEnemy { get; set; }
public float DistanceToBase { get; set; }
// 游戏状态
public float GameTime { get; set; }
public int AlliedKills { get; set; }
public int EnemyKills { get; set; }
public int Gold { get; set; }
// 更新感知信息
public void UpdateSenses()
{
VisibleEnemies.Clear();
VisibleAllies.Clear();
NearbyMinions.Clear();
NearbyJungleMonsters.Clear();
float sightRange = 15.0f;
foreach (var node in Hero.GetTree().GetNodesInGroup("units"))
{
var unit = node as Node3D;
if (unit == null || unit == Hero) continue;
float dist = Hero.GlobalPosition.DistanceTo(unit.GlobalPosition);
if (dist > sightRange) continue;
int unitTeam = (int)unit.Get("team_id");
if (unitTeam != Hero.TeamId)
{
VisibleEnemies.Add(unit);
if (unit.IsInGroup("minions"))
NearbyMinions.Add(unit);
if (unit.IsInGroup("jungle"))
NearbyJungleMonsters.Add(unit);
}
else
{
VisibleAllies.Add(unit);
}
}
// 找最近的敌人
Node3D nearest = null;
float minDist = float.MaxValue;
foreach (var enemy in VisibleEnemies)
{
float d = Hero.GlobalPosition.DistanceTo(enemy.GlobalPosition);
if (d < minDist)
{
minDist = d;
nearest = enemy;
}
}
NearestEnemy = nearest;
}
}GDScript
# ai_context.gd
class_name AIContext
extends RefCounted
var hero: Node3D
var hp_percent: float:
get: return hero.current_hp / hero.get_max_hp()
var mp_percent: float:
get: return hero.current_mp / hero.data.max_mp
# 感知信息
var visible_enemies: Array[Node3D] = []
var visible_allies: Array[Node3D] = []
var nearby_minions: Array[Node3D] = []
var nearby_jungle_monsters: Array[Node3D] = []
var nearest_enemy_tower: Node3D
var nearest_enemy: Node3D
var distance_to_base: float
# 游戏状态
var game_time: float
var allied_kills: int
var enemy_kills: int
var gold: int
func update_senses():
visible_enemies.clear()
visible_allies.clear()
nearby_minions.clear()
nearby_jungle_monsters.clear()
var sight_range = 15.0
for node in hero.get_tree().get_nodes_in_group("units"):
var unit = node as Node3D
if unit == null or unit == hero:
continue
var dist = hero.global_position.distance_to(unit.global_position)
if dist > sight_range:
continue
var unit_team: int = unit.get("team_id")
if unit_team != hero.team_id:
visible_enemies.append(unit)
if unit.is_in_group("minions"):
nearby_minions.append(unit)
if unit.is_in_group("jungle"):
nearby_jungle_monsters.append(unit)
else:
visible_allies.append(unit)
# 找最近的敌人
var min_dist = INF
nearest_enemy = null
for enemy in visible_enemies:
var d = hero.global_position.distance_to(enemy.global_position)
if d < min_dist:
min_dist = d
nearest_enemy = enemy8.4 具体 AI 行为
撤退行为
C#
// BTRetreat.cs - 撤退条件
public partial class BTRetreat : BTNode
{
public override BTStatus Tick(double delta, AIContext context)
{
if (context.HpPercent < 0.2f)
{
// 血量低于20%,撤退回基地
var basePos = context.Hero.TeamId == 0
? new Vector3(-45, 1, 0)
: new Vector3(45, 1, 0);
context.Hero.MoveTo(basePos);
return BTStatus.Success;
}
return BTStatus.Failure;
}
}GDScript
# bt_retreat.gd
extends BTNode
func tick(delta: float, context: AIContext) -> int:
if context.hp_percent < 0.2:
# 血量低于20%,撤退回基地
var base_pos = Vector3(-45, 1, 0) if context.hero.team_id == 0 else Vector3(45, 1, 0)
context.hero.move_to(base_pos)
return Status.SUCCESS
return Status.FAILURE清兵行为
C#
// BTClearLane.cs - 清兵行为
public partial class BTClearLane : BTNode
{
public override BTStatus Tick(double delta, AIContext context)
{
if (context.NearbyMinions.Count == 0)
return BTStatus.Failure;
// 攻击最近的小兵
Node3D nearestMinion = null;
float minDist = float.MaxValue;
foreach (var minion in context.NearbyMinions)
{
float d = context.Hero.GlobalPosition.DistanceTo(minion.GlobalPosition);
if (d < minDist)
{
minDist = d;
nearestMinion = minion;
}
}
if (nearestMinion != null)
{
context.Hero.AttackTarget(nearestMinion);
return BTStatus.Running;
}
return BTStatus.Failure;
}
}GDScript
# bt_clear_lane.gd
extends BTNode
func tick(delta: float, context: AIContext) -> int:
if context.nearby_minions.is_empty():
return Status.FAILURE
# 攻击最近的小兵
var nearest_minion: Node3D = null
var min_dist = INF
for minion in context.nearby_minions:
var d = context.hero.global_position.distance_to(minion.global_position)
if d < min_dist:
min_dist = d
nearest_minion = minion
if nearest_minion:
context.hero.attack_target(nearest_minion)
return Status.RUNNING
return Status.FAILURE8.5 AI 控制器
把行为树和上下文组合起来,挂到 AI 英雄身上:
C#
// AIController.cs
public partial class AIController : Node
{
private BTNode _behaviorTree;
private AIContext _context;
private HeroBase _hero;
public override void _Ready()
{
_hero = GetParent<HeroBase>();
_context = new AIContext { Hero = _hero };
_behaviorTree = BuildBehaviorTree();
}
public override void _Process(double delta)
{
_context.UpdateSenses();
_behaviorTree.Tick(delta, _context);
}
private BTNode BuildBehaviorTree()
{
var root = new BTSelector();
// 优先级1:低血量撤退
var retreat = new BTSequence();
retreat.AddChild(new BTConditionLowHp(0.2f));
retreat.AddChild(new BTRetreat());
root.AddChild(retreat);
// 优先级2:有队友在打架,去帮忙
var teamfight = new BTSequence();
teamfight.AddChild(new BTConditionAllyInCombat());
teamfight.AddChild(new BTActionJoinTeamfight());
root.AddChild(teamfight);
// 优先级3:打野怪
var jungle = new BTSequence();
jungle.AddChild(new BTConditionJungleAvailable());
jungle.AddChild(new BTActionClearJungle());
root.AddChild(jungle);
// 默认行为:沿路线清兵
root.AddChild(new BTClearLane());
return root;
}
}GDScript
# ai_controller.gd
extends Node
var behavior_tree: BTNode
var context: AIContext
var hero: Node3D
func _ready():
hero = get_parent()
context = AIContext.new()
context.hero = hero
behavior_tree = build_behavior_tree()
func _process(delta):
context.update_senses()
behavior_tree.tick(delta, context)
func build_behavior_tree() -> BTNode:
var root = BTSelector.new()
# 优先级1:低血量撤退
var retreat = BTSequence.new()
retreat.add_child(BTConditionLowHp.new(0.2))
retreat.add_child(BTRetreat.new())
root.add_child(retreat)
# 优先级2:有队友在打架,去帮忙
var teamfight = BTSequence.new()
teamfight.add_child(BTConditionAllyInCombat.new())
teamfight.add_child(BTActionJoinTeamfight.new())
root.add_child(teamfight)
# 优先级3:打野怪
var jungle = BTSequence.new()
jungle.add_child(BTConditionJungleAvailable.new())
jungle.add_child(BTActionClearJungle.new())
root.add_child(jungle)
# 默认行为:沿路线清兵
root.add_child(BTClearLane.new())
return root章节导航
| 上一章 | 下一章 |
|---|---|
| ← 7. 装备与经济系统 | 9. 网络5v5对战 → |
