每日算法 — 使用java实现魔兽世界:A星寻路与仇恨系统AI决策

引言

在多人在线角色扮演游戏(MMORPG)中,敌人的AI行为直接决定了游戏的挑战性与可玩性。以魔兽世界这类经典MMORPG为场景,敌人AI需要解决两个核心问题:一是如何在复杂地形中追踪玩家,二是如何在多个目标中选择攻击对象。本文将用Java实现一个简化版的敌人AI系统,涵盖A星寻路算法与基于威胁值的仇恨系统,帮助读者理解游戏AI中的路径规划与决策逻辑。

A星寻路算法原理

A星(A*)算法是游戏开发中最常用的路径搜索算法。它在Dijkstra算法的基础上引入启发式函数,通过评估公式 f(n) = g(n) + h(n) 来指导搜索方向:

  • g(n):从起点到当前节点n的实际代价
  • h(n):从节点n到终点的估计代价(启发函数)

在网格地图中,通常使用曼哈顿距离作为启发函数:

h(n) = |x1 - x2| + |y1 - y2|

A星算法保证在启发函数可采纳(即从不高估实际代价)的前提下,能找到最短路径。

仇恨系统原理

仇恨(Threat/Threat Table)系统是MMORPG中敌人选择目标的经典机制。每个敌人维护一个威胁值列表,记录每个玩家对其造成的威胁程度。威胁值通常由以下因素加权计算:

  1. 伤害输出:对敌人造成的伤害按比例转化为威胁值
  2. 治疗行为:治疗被敌人锁定的目标会产生仇恨
  3. 距离因素:靠近敌人的玩家可能获得额外威胁
  4. 特殊技能:嘲讽类技能强制提升威胁值

敌人AI每帧更新威胁列表,并始终攻击威胁值最高的目标。

有限状态机驱动敌人行为

敌人的行为可以用有限状态机(FSM)建模:

  • 巡逻(Patrol):在出生点周围随机移动
  • 警戒(Alert):发现玩家但距离较远,准备追击
  • 追击(Chase):使用A星寻路追踪威胁最高的目标
  • 攻击(Attack):进入攻击范围后进行攻击
  • 逃跑(Flee):血量过低时远离玩家

状态之间的转换由距离、血量、威胁值等条件触发。

Java实现

以下是一个完整的简化版敌人AI系统实现,包含A星寻路、仇恨计算与状态机:

import java.util.*;

/**
 * 魔兽世界风格敌人AI系统:A星寻路 + 仇恨系统 + 有限状态机
 */
public class WoWEnemyAI {

    // 地图节点
    static class Node implements Comparable<Node> {
        int x, y;
        double g, h; // g: 实际代价, h: 启发估计
        Node parent;

        Node(int x, int y) {
            this.x = x;
            this.y = y;
        }

        double f() { return g + h; }

        @Override
        public int compareTo(Node o) {
            return Double.compare(this.f(), o.f());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Node)) return false;
            Node node = (Node) o;
            return x == node.x && y == node.y;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }

    // A星寻路器
    static class AStarPathfinder {
        private final int[][] grid; // 0表示可通行, 1表示障碍物
        private final int rows, cols;
        private final int[][] directions = {{-1,0},{1,0},{0,-1},{0,1}};

        AStarPathfinder(int[][] grid) {
            this.grid = grid;
            this.rows = grid.length;
            this.cols = grid[0].length;
        }

        // 曼哈顿距离启发函数
        private double heuristic(int x1, int y1, int x2, int y2) {
            return Math.abs(x1 - x2) + Math.abs(y1 - y2);
        }

        List<Node> findPath(int startX, int startY, int endX, int endY) {
            PriorityQueue<Node> openSet = new PriorityQueue<>();
            Set<Node> closedSet = new HashSet<>();
            Node start = new Node(startX, startY);
            start.g = 0;
            start.h = heuristic(startX, startY, endX, endY);
            openSet.add(start);

            while (!openSet.isEmpty()) {
                Node current = openSet.poll();
                if (current.x == endX && current.y == endY) {
                    return reconstructPath(current);
                }
                closedSet.add(current);

                for (int[] d : directions) {
                    int nx = current.x + d[0];
                    int ny = current.y + d[1];
                    if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue;
                    if (grid[nx][ny] == 1) continue; // 障碍物

                    Node neighbor = new Node(nx, ny);
                    if (closedSet.contains(neighbor)) continue;

                    double tentativeG = current.g + 1; // 每步代价为1
                    boolean inOpen = openSet.stream().anyMatch(n -> n.equals(neighbor));
                    if (!inOpen || tentativeG < neighbor.g) {
                        neighbor.g = tentativeG;
                        neighbor.h = heuristic(nx, ny, endX, endY);
                        neighbor.parent = current;
                        if (!inOpen) openSet.add(neighbor);
                    }
                }
            }
            return Collections.emptyList(); // 无路径
        }

        private List<Node> reconstructPath(Node node) {
            List<Node> path = new ArrayList<>();
            while (node != null) {
                path.add(0, node);
                node = node.parent;
            }
            return path;
        }
    }

    // 仇恨表条目
    static class ThreatEntry {
        String targetId;
        double threatValue;
        long lastUpdateTime;

        ThreatEntry(String targetId, double threatValue) {
            this.targetId = targetId;
            this.threatValue = threatValue;
            this.lastUpdateTime = System.currentTimeMillis();
        }
    }

    // 仇恨系统
    static class ThreatSystem {
        private final Map<String, ThreatEntry> threatTable = new HashMap<>();
        private static final double DECAY_RATE = 0.95; // 威胁值衰减率
        private static final long DECAY_INTERVAL = 1000; // 衰减间隔(ms)

        // 添加威胁值
        void addThreat(String targetId, double amount) {
            threatTable.merge(targetId, new ThreatEntry(targetId, amount),
                (old, neu) -> {
                    old.threatValue += amount;
                    old.lastUpdateTime = System.currentTimeMillis();
                    return old;
                });
        }

        // 获取最高威胁目标
        String getTopThreatTarget() {
            applyDecay();
            return threatTable.values().stream()
                .max(Comparator.comparingDouble(e -> e.threatValue))
                .map(e -> e.targetId)
                .orElse(null);
        }

        // 威胁值自然衰减,模拟时间推移后的仇恨下降
        void applyDecay() {
            long now = System.currentTimeMillis();
            threatTable.values().forEach(e -> {
                long diff = now - e.lastUpdateTime;
                int decayCount = (int)(diff / DECAY_INTERVAL);
                for (int i = 0; i < decayCount; i++) {
                    e.threatValue *= DECAY_RATE;
                }
                e.lastUpdateTime = now;
            });
            threatTable.values().removeIf(e -> e.threatValue < 1.0);
        }

        Map<String, ThreatEntry> getThreatTable() { return threatTable; }
    }

    // 敌人状态枚举
    enum State { PATROL, ALERT, CHASE, ATTACK, FLEE }

    // 敌人AI主体
    static class Enemy {
        String id;
        int x, y;
        int hp, maxHp;
        int attackRange = 2;
        int alertRange = 8;
        int chaseRange = 12;
        State state = State.PATROL;
        ThreatSystem threatSystem = new ThreatSystem();
        AStarPathfinder pathfinder;
        List<Node> currentPath = new ArrayList<>();
        int pathIndex = 0;
        Random rand = new Random();

        Enemy(String id, int x, int y, int hp, int[][] grid) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.hp = hp;
            this.maxHp = hp;
            this.pathfinder = new AStarPathfinder(grid);
        }

        // 每帧更新AI状态
        void update(Map<String, Player> players) {
            switch (state) {
                case PATROL -> handlePatrol(players);
                case ALERT -> handleAlert(players);
                case CHASE -> handleChase(players);
                case ATTACK -> handleAttack(players);
                case FLEE -> handleFlee(players);
            }
        }

        void handlePatrol(Map<String, Player> players) {
            // 扫描范围内玩家
            for (Player p : players.values()) {
                double dist = distance(x, y, p.x, p.y);
                if (dist <= alertRange) {
                    threatSystem.addThreat(p.id, 10); // 发现玩家获得基础威胁
                    state = State.ALERT;
                    return;
                }
            }
            // 随机巡逻
            if (rand.nextDouble() < 0.2) {
                int dx = rand.nextInt(3) - 1;
                int dy = rand.nextInt(3) - 1;
                x += dx; y += dy;
            }
        }

        void handleAlert(Map<String, Player> players) {
            String targetId = threatSystem.getTopThreatTarget();
            if (targetId == null) { state = State.PATROL; return; }
            Player target = players.get(targetId);
            double dist = distance(x, y, target.x, target.y);
            if (dist <= attackRange) {
                state = State.ATTACK;
            } else if (dist <= chaseRange) {
                state = State.CHASE;
            } else {
                state = State.PATROL;
            }
        }

        void handleChase(Map<String, Player> players) {
            String targetId = threatSystem.getTopThreatTarget();
            if (targetId == null) { state = State.PATROL; return; }
            Player target = players.get(targetId);
            double dist = distance(x, y, target.x, target.y);

            if (dist <= attackRange) {
                state = State.ATTACK;
                return;
            }
            if (hp < maxHp * 0.2) {
                state = State.FLEE;
                return;
            }

            // 使用A星寻路追踪目标
            if (currentPath.isEmpty() || pathIndex >= currentPath.size() || rand.nextDouble() < 0.1) {
                currentPath = pathfinder.findPath(x, y, target.x, target.y);
                pathIndex = 0;
            }
            if (!currentPath.isEmpty() && pathIndex < currentPath.size()) {
                Node next = currentPath.get(pathIndex++);
                x = next.x; y = next.y;
            }
        }

        void handleAttack(Map<String, Player> players) {
            String targetId = threatSystem.getTopThreatTarget();
            if (targetId == null) { state = State.PATROL; return; }
            Player target = players.get(targetId);
            double dist = distance(x, y, target.x, target.y);
            if (dist > attackRange) {
                state = State.CHASE;
                return;
            }
            // 模拟攻击并产生额外仇恨反馈
            System.out.printf("[%s] 攻击玩家 %s,造成 %d 点伤害%n", id, target.id, 15);
            threatSystem.addThreat(target.id, 15 * 0.5); // 被攻击者仇恨增加
        }

        void handleFlee(Map<String, Player> players) {
            // 向远离最近玩家的方向逃跑
            Player nearest = players.values().stream()
                .min(Comparator.comparingDouble(p -> distance(x, y, p.x, p.y)))
                .orElse(null);
            if (nearest != null) {
                int dx = Integer.compare(x, nearest.x); // 远离方向
                int dy = Integer.compare(y, nearest.y);
                x += dx; y += dy;
            }
            if (hp > maxHp * 0.5) state = State.PATROL;
        }

        double distance(int x1, int y1, int x2, int y2) {
            return Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
        }
    }

    static class Player {
        String id;
        int x, y;
        Player(String id, int x, int y) { this.id = id; this.x = x; this.y = y; }
    }

    public static void main(String[] args) {
        // 0表示空地, 1表示障碍物
        int[][] grid = {
            {0,0,0,0,0,0,0,0,0,0},
            {0,1,1,0,0,0,1,1,0,0},
            {0,0,0,0,1,0,0,0,0,0},
            {0,0,1,0,0,0,1,0,0,0},
            {0,0,0,0,0,0,0,0,1,0},
            {0,1,0,0,1,1,0,0,0,0},
            {0,0,0,0,0,0,0,1,0,0},
            {0,0,1,0,0,0,0,0,0,0},
            {0,0,0,0,1,0,0,0,1,0},
            {0,0,0,0,0,0,0,0,0,0}
        };

        Enemy boss = new Enemy("Boss-1", 0, 0, 100, grid);
        Map<String, Player> players = new HashMap<>();
        players.put("Player-A", new Player("Player-A", 9, 9));
        players.put("Player-B", new Player("Player-B", 8, 8));

        // 模拟战斗:Player-A造成大量伤害吸引仇恨
        boss.threatSystem.addThreat("Player-A", 80);
        boss.threatSystem.addThreat("Player-B", 30);

        System.out.println("=== 魔兽世界风格敌人AI模拟 ===");
        for (int tick = 0; tick < 15; tick++) {
            boss.update(players);
            System.out.printf("Tick %d: 状态=%s, 位置=(%d,%d), 最高威胁=%s%n",
                tick, boss.state, boss.x, boss.y,
                boss.threatSystem.getTopThreatTarget());
        }
    }
}

关键设计要点解析

A星寻路中的启发函数选择:本文使用曼哈顿距离,适用于四方向移动的网格地图。若支持斜向移动,可改用欧几里得距离或对角线距离。启发函数的可采纳性保证了算法的最优性。

仇恨系统的衰减机制:威胁值不会永久累积,而是通过指数衰减模拟”遗忘”效果。这防止了离线玩家或远距离玩家持续占据仇恨列表首位,使战斗动态更加真实。

状态机转换的边界条件:状态之间的转换需要仔细设计边界条件。例如从CHASE到ATTACK的切换需要同时满足距离条件和路径可达性;从CHASE到FLEE的切换则基于血量阈值。

复杂度分析

  • A星寻路:时间复杂度为 O(b^d),其中 b 是分支因子(网格中为4),d 是解的深度。在最优启发函数下接近 O(d)。空间复杂度为 O(V),V 为访问的节点数。
  • 仇恨系统更新:单次添加为 O(1),获取最高威胁目标为 O(T),T 为威胁表中的目标数。衰减操作为 O(T)。
  • 整体AI更新:每帧 O(T + P),P 为A星寻路的路径长度。

扩展方向

  1. 分层A星(Hierarchical A*):在大地图中使用粗粒度路径规划,接近目标时切换到细粒度网格。
  2. 群体仇恨联动:当敌人被攻击时,通知周围同阵营敌人共享仇恨信息。
  3. 动态避障:在移动过程中实时检测其他单位,使用RVO(Reciprocal Velocity Obstacles)避免碰撞。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注