第4篇 | 基于 HarmonyOS 开发战旗小游戏项目AI自动攻击引擎介绍

前言:

基于前面第三篇的介绍,这边来说下AI自动攻击引擎,这边和第二篇一样采用的基本的逻辑算法编写,没有额外采用网络上的游戏引擎.

代码如下:
TypeScript 复制代码
import { Unit, Faction, Position } from '../common/GameModels';
import { GameEngine } from '../engine/GameEngine';
import { GamePhase } from '../common/GameConstants';

// AI引擎 - 控制敌方单位行动
export class AIEngine {
  private gameEngine: GameEngine;

  constructor(gameEngine: GameEngine) {
    this.gameEngine = gameEngine;
  }

  // 执行AI回合
  executeTurn(): Promise<void> {
    return new Promise((resolve: () => void): void => {
      const enemyUnits: Unit[] = this.gameEngine.getGameState().units
        .filter((u: Unit): boolean => u.faction === Faction.ENEMY && u.isAlive && !u.hasActed);

      if (enemyUnits.length === 0) {
        resolve();
        return;
      }

      // 逐个执行敌方单位行动
      this.executeUnitActions(enemyUnits, 0, resolve);
    });
  }

  // 逐个执行单位行动(带延迟以便观察)
  private executeUnitActions(units: Unit[], index: number, resolve: () => void): void {
    if (index >= units.length) {
      resolve();
      return;
    }

    const unit = units[index];
    if (!unit.isAlive || unit.hasActed) {
      this.executeUnitActions(units, index + 1, resolve);
      return;
    }

    // 执行当前单位的行动
    this.executeUnitAction(unit);

    // 延迟后执行下一个单位
    setTimeout((): void => {
      this.executeUnitActions(units, index + 1, resolve);
    }, 800);
  }

  // 执行单个单位的行动
  private executeUnitAction(unit: Unit): void {
    // 1. 尝试攻击(优先攻击)
    const attackTarget = this.findBestAttackTarget(unit);
    if (attackTarget) {
      this.gameEngine.attackUnit(unit, attackTarget);
      return;
    }

    // 2. 如果没有攻击目标,则移动
    const moveTarget = this.findBestMoveTarget(unit);
    if (moveTarget) {
      this.gameEngine.moveUnit(unit, moveTarget);
      
      // 移动后尝试攻击
      const newAttackTarget = this.findBestAttackTarget(unit);
      if (newAttackTarget) {
        this.gameEngine.attackUnit(unit, newAttackTarget);
      } else {
        unit.hasActed = true;
      }
    } else {
      // 无法移动,标记为已行动
      unit.hasActed = true;
    }
  }

  // 寻找最佳攻击目标
  private findBestAttackTarget(unit: Unit): Position | null {
    const attackablePositions: Position[] = this.gameEngine.getAttackablePositions(unit);
    if (attackablePositions.length === 0) return null;

    // 评估每个攻击目标的价值
    let bestTarget: Position | null = null;
    let bestValue = -1;

    for (let i = 0; i < attackablePositions.length; i++) {
      const pos = attackablePositions[i];
      const targetUnit = this.gameEngine.getUnitAt(pos);
      if (!targetUnit) continue;

      // 计算目标价值(优先攻击血量低、攻击力高的单位)
      const value = this.evaluateTargetValue(unit, targetUnit);
      if (value > bestValue) {
        bestValue = value;
        bestTarget = pos;
      }
    }

    return bestTarget;
  }

  // 评估攻击目标的价值
  private evaluateTargetValue(attacker: Unit, target: Unit): number {
    let value = 0;

    // 1. 优先攻击将军(高价值目标)
    if (target.type === 4) { // GENERAL
      value += 100;
    }

    // 2. 优先攻击血量低的单位(可以击杀)
    const hpPercent = target.hp / target.maxHp;
    if (hpPercent < 0.3) {
      value += 50; // 可能击杀
    }

    // 3. 考虑攻击力威胁
    value += target.attack * 0.5;

    // 4. 远程单位优先攻击近战单位
    if (attacker.type === 1 || attacker.type === 3) { // ARCHER or MAGE
      if (target.type === 0 || target.type === 2) { // WARRIOR or CAVALRY
        value += 20;
      }
    }

    // 5. 扣除距离惩罚(越近越好)
    const distance = Math.abs(attacker.position.x - target.position.x) +
                    Math.abs(attacker.position.y - target.position.y);
    value -= distance * 2;

    return value;
  }

  // 寻找最佳移动目标
  private findBestMoveTarget(unit: Unit): Position | null {
    const movablePositions: Position[] = this.gameEngine.getMovablePositions(unit);
    if (movablePositions.length === 0) return null;

    // 找到最近的敌方单位
    const enemyUnits: Unit[] = this.gameEngine.getGameState().units
      .filter((u: Unit): boolean => u.faction === Faction.PLAYER && u.isAlive);

    if (enemyUnits.length === 0) return null;

    // 评估每个可移动位置的价值
    let bestPosition: Position | null = null;
    let bestValue = -1000;

    for (let i = 0; i < movablePositions.length; i++) {
      const pos = movablePositions[i];
      const value = this.evaluateMovePosition(unit, pos, enemyUnits);
      if (value > bestValue) {
        bestValue = value;
        bestPosition = pos;
      }
    }

    return bestPosition;
  }

  // 评估移动位置的价值
  private evaluateMovePosition(unit: Unit, pos: Position, enemyUnits: Unit[]): number {
    let value = 0;

    // 1. 接近敌方单位(移动范围允许的情况下)
    let minDistanceToEnemy = 100;
    for (let i = 0; i < enemyUnits.length; i++) {
      const enemy = enemyUnits[i];
      const distance = Math.abs(pos.x - enemy.position.x) +
                      Math.abs(pos.y - enemy.position.y);
      minDistanceToEnemy = Math.min(minDistanceToEnemy, distance);
    }

    // 对于近战单位,想要接近敌人
    if (unit.attackRange === 1) {
      value += (10 - minDistanceToEnemy) * 10; // 越近越好
    }

    // 对于远程单位,保持距离但进入攻击范围
    if (unit.attackRange > 1) {
      if (minDistanceToEnemy <= unit.attackRange && minDistanceToEnemy > 1) {
        value += 50; // 理想攻击位置
      } else if (minDistanceToEnemy > unit.attackRange) {
        value += (unit.attackRange - minDistanceToEnemy + 10) * 5; // 需要接近
      }
    }

    // 2. 优先选择有利地形
    const terrain = this.gameEngine.getTerrainAt(pos);
    if (terrain === 1) { // FOREST
      value += 10; // 防御加成
    } else if (terrain === 2) { // MOUNTAIN
      value += 15; // 更多防御加成
    } else if (terrain === 3) { // WATER
      value -= 100; // 避免水域
    }

    // 3. 避免过于冒险(低血量单位应该更保守)
    const hpPercent = unit.hp / unit.maxHp;
    if (hpPercent < 0.3) {
      // 低血量,寻找防御位置
      if (terrain === 1 || terrain === 2) {
        value += 30;
      }
      // 远离敌方
      value += minDistanceToEnemy * 3;
    }

    return value;
  }
}

AIEngine 是回合制策略游戏中控制敌方单位自动行动的人工智能引擎,基于 TypeScript 实现。它依赖 GameEngine 提供的游戏状态与操作接口,通过启发式评估函数为每个敌方单位独立决策最优行动方案。AI 采用贪心策略,按"先攻击、后移动、再攻击"的优先级链执行,无需搜索树或蒙特卡洛模拟,属于轻量级规则驱动型 AI。

一:设计

  • 架构层次: 位于 GameEngine 之上,消费其查询与操作接口,不直接修改 GameState
  • 决策模型: 启发式评分 + 贪心选择,每个单位独立决策,无全局协同
  • 执行方式: 异步串行,单位间 800ms 延迟,便于 UI 播放动画
  • 扩展性: 评估函数可独立替换,支持从规则驱动升级为搜索驱动

依赖关系

  • GameModels: Unit, Faction, Position 类型定义
  • GameEngine: getGameState(), getAttackablePositions(), getMovablePositions(), attackUnit(), moveUnit(), getUnitAt(), getTerrainAt() 接口
  • GameConstants: GamePhase 枚举(当前代码中未直接使用,但保留导入以备扩展)

二:类的接口介绍

AIEngine 是一个无状态决策器(除持有 gameEngine 引用外无内部状态),所有决策均基于当前 GameState 实时计算。其对外仅暴露 executeTurn() 一个方法,内部通过私有方法链完成完整的决策与执行流程。

AIEngine 方法总览

方法 可见性 类别 说明
executeTurn() public 入口 执行整个 AI 回合,返回 Promise
executeUnitActions() private 调度 串行调度各单位行动,含 800ms 延迟
executeUnitAction() private 决策 单个单位的行动决策与执行
findBestAttackTarget() private 评估 从可攻击位置中选出最优攻击目标
evaluateTargetValue() private 评估 计算攻击目标的价值分数
findBestMoveTarget() private 评估 从可移动位置中选出最优移动目标
evaluateMovePosition() private 评估 计算移动位置的价值分数

三:回合执行流程

3.1 executeTurn() 总控

executeTurn() 是 AI 引擎的唯一公共入口,返回 Promise<void>,使调用方可以 await 等待整个 AI 回合执行完毕。方法首先筛选所有存活且未行动的敌方单位,若无可用单位则立即 resolve,否则将单位列表传入递归调度器。

3.1.1 执行时序

  1. 从 GameState 中筛选 faction=ENEMY 且 isAlive=true 且 hasActed=false 的单位列表
  2. 若列表为空,立即 resolve Promise
  3. 调用 executeUnitActions(units, 0, resolve) 开始串行调度
  4. 所有单位行动完成后,resolve Promise,控制权交还 GameEngine

3.2 executeUnitActions() 串行调度

该方法通过递归 + setTimeout 实现单位间的串行执行与视觉延迟。每个单位执行完毕后等待 800ms 再处理下一个,便于 UI 层播放移动和攻击动画。

调度逻辑分支

条件 处理 说明
index >= units.length 调用 resolve() 所有单位已处理完毕
unit 已死亡或已行动 跳过,处理下一个 战斗中可能被反击击杀
unit 存活且未行动 执行行动,800ms 后处理下一个 正常行动流程

四:单位行动决策

executeUnitAction() 方法为每个单位按固定优先级链执行决策,遵循"能打就打,不能打就移动,移动后再打"的贪心策略。

  1. 调用 findBestAttackTarget() 检查当前位置是否有可攻击目标
  2. 若存在最优攻击目标,立即执行攻击并结束本回合
  3. 若无攻击目标,调用 findBestMoveTarget() 寻找最优移动位置
  4. 移动后再次调用 findBestAttackTarget() 检查新位置是否可攻击
  5. 若移动后可攻击,执行攻击;否则手动标记 hasActed=true 结束回合
  6. 若无法移动,直接标记 hasActed=true 结束回合

五:攻击目标评估

5.1 findBestAttackTarget() 选择逻辑

该方法遍历当前单位的所有可攻击位置,对每个位置上的敌方单位调用 evaluateTargetValue() 计算价值分数,选取得分最高的目标返回。若无有效目标则返回 null。

5.2 evaluateTargetValue() 评分体系

攻击目标评估函数是一个加权求和模型,包含五个独立评估维度。每个维度贡献一个分值,最终分数越高表示目标越值得攻击。

攻击目标评估维度

维度 条件 分值 设计意图
将军优先 target.type === 4 (GENERAL) +100 将军是核心单位,优先消灭可致胜
残血击杀 hp/maxHp < 0.3 +50 低血量单位更可能被击杀,减少敌方行动次数
威胁评估 始终 +target.attack * 0.5 高攻击力单位威胁更大,优先消除
兵种克制 远程打近战 +20 利用射程优势,远程先消耗近战
距离惩罚 始终 -distance * 2 距离越远越不利于后续追击

评分示例:

假设一名法师(type=3, 远程)评估两个目标:近战战士(attack=8, hp=30%, 距离2)和将军(attack=5, hp=80%, 距离3)。

评分对比示例

评估维度 战士得分 将军得分
将军优先 0 +100
残血击杀 +50 0
威胁评估 +4 (8*0.5) +2.5 (5*0.5)
兵种克制 +20 +20
距离惩罚 -4 (2*2) -6 (3*2)
合计 70 116.5

结果将军得分(116.5)高于战士(70),AI 会优先攻击将军。这符合"优先消灭核心目标"的设计意图,但也暴露了将军优先权重过高可能导致忽略击杀机会的问题。

六:移动目标评估

6.1 findBestMoveTarget() 选择逻辑

该方法遍历当前单位的所有可移动位置,对每个位置调用 evaluateMovePosition() 计算价值分数,选取得分最高的位置返回。评估时需传入所有存活玩家单位列表,用于计算距离关系。

6.2 evaluateMovePosition() 评分体系

移动位置评估函数同样采用加权求和模型,从兵种定位、地形优势和生存策略三个维度综合评估。

6.2.1 近战单位定位

近战单位(attackRange === 1)的移动目标是接近敌人,评分公式为:(10 - minDistanceToEnemy) * 10。距离越近得分越高,最大加分 100(贴脸),最小 0(距离10+)。这驱动近战单位始终向最近的敌人冲锋。

6.2.2 远程单位定位

远程单位(attackRange > 1)追求"在攻击范围内但保持安全距离"的理想位置:

远程单位移动评分规则

距离条件 评分 含义
1 < 距离 <= attackRange +50 理想攻击位置,可攻击且安全
距离 > attackRange +(attackRange - 距离 + 10) * 5 需接近,但仍向有利方向移动
距离 = 1 无额外加分 贴脸危险,远程不应近身

远程单位在距离=1时不会获得理想位置加分,但也不会主动远离。这是一个潜在的改进点:远程单位应主动回避近身接触。

6.2.3 地形偏好

地形评分规则

地形 类型 评分 原因
0 平原 0 基准,无特殊效果
1 森林 +10 提供防御加成
2 山地 +15 更高防御加成
3 水域 -100 不可通行,强烈回避

6.2.4 低血量生存策略

当单位血量低于 30% 时,触发保守策略:森林或山地额外 +30 分(寻找掩体),同时 minDistanceToEnemy * 3 作为远离敌人的加分。这使得低血量单位倾向于退入有利地形并拉开距离,而非继续进攻。

低血量策略对比

状态 近战行为 远程行为 地形偏好
正常血量 冲向最近敌人 进入攻击范围 轻微偏好防御地形
低血量(<30%) 仍冲向敌人* 仍进入攻击范围* 强烈偏好防御地形+远离敌人

七、与 GameEngine 的交互

7.1 调用的 GameEngine 接口

AIEngine 对 GameEngine 的接口依赖

接口 用途 调用场景
getGameState() 获取全局状态和单位列表 executeTurn(), findBestMoveTarget()
getAttackablePositions() 获取可攻击位置列表 findBestAttackTarget()
getMovablePositions() 获取可移动位置列表 findBestMoveTarget()
getUnitAt() 获取指定位置的单位 findBestAttackTarget()
getTerrainAt() 获取指定位置的地形 evaluateMovePosition()
attackUnit() 执行攻击操作 executeUnitAction()
moveUnit() 执行移动操作 executeUnitAction()

总结:

  • 异步串行执行 + 延迟动画,用户体验友好,可观察每个 AI 单位的行动过程
  • 攻击-移动-攻击的三段式决策链,模拟真实战术中的"移动接敌→攻击"行为
  • 评估函数多维度加权,涵盖兵种克制、地形利用、生存策略等战术要素
  • 低血量保守策略体现了生存意识,避免单位无意义送死
  • 与 GameEngine 解耦,仅通过公共接口交互,便于替换 AI 策略