【Pygame】第15章 游戏人工智能基础、行为控制与寻路算法实现

摘要

人工智能是游戏开发中的重要组成部分,它能够赋予非玩家角色更自然的行为表现,使游戏世界显得更加真实、生动,并且具有挑战性。

在 2D 游戏中,AI 通常并不追求真正意义上的"智能",而是通过一系列规则、状态和算法,让角色表现出像是"有意识"一样的行动方式。常见的 AI 行为包括巡逻、追逐、逃避、攻击和路径寻找等,它们虽然实现逻辑不同,但本质上都属于"根据当前状态做出下一步决策"的过程。

本章将系统介绍游戏 AI 的基础概念,说明常见行为模式的设计思路,并进一步讲解经典的 A 星寻路算法及其在地图环境中的应用。我们还会讨论状态机、视野检测、动态决策和路径更新等内容,帮助读者建立完整的 AI 思维模型。最后,本章将展示如何使用 GPT-5.4 来生成 AI 行为代码和寻路系统。由于国内无法访问 OpenAI 官网,因此使用国内镜像站可以合法注册使用 GPT-5.4 最新模型。翻墙行为违反中国法律法规,请大家遵守法律,不要翻墙。国内镜像站提供了稳定、合法的 AI 服务访问渠道,完全能够满足学习和开发需求。

注册入口:AIGCBAR 镜像站

API 站注册入口:API 独立站

通过本章的学习,读者将能够为游戏角色设计出较为完整的智能行为系统。


15.1 游戏 AI 的基本概念

游戏 AI 和学术意义上的人工智能并不完全相同。

在游戏中,AI 的目标通常不是"完美决策",而是"足够像真的"。

也就是说,它更关注可玩性、表现力和互动感,而不是数学意义上的最优解。

例如,敌人不一定要每次都做最优路径选择,但它需要让玩家觉得"这个敌人会思考,会追我,会躲避,会寻找机会"。

从系统结构上看,游戏 AI 往往可以理解为一个输入到输出的决策过程。

输入包括玩家位置、地图信息、距离、视野、血量、时间、随机因素等;输出则是角色下一步的行为,例如移动、攻击、等待、转向或者逃离。

如果把这种过程抽象成一个模型,可以写成:

a_t = f\\left(s_t\\right)

其中,( s_t ) 表示当前时刻的状态,( a_t ) 表示 AI 在该状态下采取的动作,( f ) 则是决策函数。

在实际游戏里,这个函数通常不是高深算法,而是一组规则、条件判断和状态切换逻辑。


15.2 常见 AI 行为的设计思路

游戏里的 NPC 行为虽然种类很多,但大多数都可以归纳为几种基本模式。

巡逻表示角色按照预设路线或节点移动;追逐表示角色在发现目标后主动接近;逃避则是远离威胁;攻击表示在满足条件时触发动作;寻路则是为了在复杂地形中找到通往目标的可行路线。

这些行为往往不是孤立存在的,而是组合在一起构成完整的敌人逻辑。

例如,一个守卫可能先在路点间巡逻,当玩家进入视野范围后切换为追击状态;如果玩家距离过近,它会尝试攻击;如果自身血量过低,又可能切换为逃跑。

这说明 AI 的核心不是"写一个动作",而是"管理动作之间的切换关系"。

在设计 AI 时,最重要的是让行为切换自然。

如果敌人一会儿追、一会儿停、一会儿又完全无视玩家,玩家就会觉得角色行为不稳定。

因此,实际项目中通常会结合状态机、行为树、感知系统和路径系统,让 AI 的决策更稳定、更可控。


15.3 追逐与逃避行为的基础原理

追逐和逃避是最基础的两类空间行为。

它们本质上都和方向向量有关。

假设角色当前位置为 ( \vec{p} ),目标位置为 ( \vec{t} ),那么朝目标移动的方向可以表示为:

\\vec{d} = \\vec{t} - \\vec{p}

如果要让角色以恒定速度朝目标移动,就需要把方向向量归一化,再乘上速度值:

\\vec{v} = \\frac{\\vec{d}}{\\lVert \\vec{d} \\rVert} \\cdot v_s

其中,( v_s ) 是速度大小。

逃避行为则相反,方向向量变成:

\\vec{d} = \\vec{p} - \\vec{t}

也就是说,追逐是"朝着目标加速",逃避是"朝着相反方向加速"。

这类行为非常适合用于基础敌人、跟踪单位、弹性敌人、恐慌单位等角色类型。

不过,单纯的追逐和逃避只是最基础的模型。

在复杂地形中,如果中间存在墙壁、障碍或不可通行区域,就不能只看直线距离,而必须结合路径系统来决定实际行动路线。

这也是寻路算法存在的原因。


15.4 行为状态机的基本思想

状态机是组织游戏 AI 最常见的方法之一。

它的核心思想是:一个角色在某一时刻只处于某一个状态中,而状态之间通过条件进行切换。

比如敌人可能拥有"巡逻""警觉""追击""攻击""返回原位"五种状态。

每个状态负责不同的行为逻辑,而状态转移则由距离、视野、血量、计时器等条件控制。

状态机的优势在于清晰、直观、容易调试。

如果 AI 行为并不特别复杂,状态机几乎是最实用的方案。

它不像一些复杂的学习型方法那样难以控制,而是更适合游戏开发中"可解释、可调参、可复现"的需求。

从逻辑上说,状态机可以写成一个有限状态系统。

如果状态集合为 ( S = { s_1, s_2, \dots, s_n } ),动作集合为 ( A ),那么状态转移可以表示为:

\\delta : S \\times A \\rightarrow S

也就是说,在当前状态和当前条件共同作用下,系统会转移到下一个状态。

这种表达方式非常适合 AI 的行为组织,也很容易扩展。


15.5 A 星寻路算法为什么重要

在没有障碍物的空旷环境中,追逐目标只需要朝直线方向移动即可。

但只要地图中存在墙壁、障碍、封闭区域或者复杂走廊,直线移动就会失效。

这时,AI 就需要一种能够在网格地图中找到可行路径的方法。

A 星算法就是最经典、最常用的寻路方案之一。

A 星寻路的核心思想,是在"已走成本"和"预估剩余成本"之间寻找平衡。

对于某个节点 ( n ),它的总代价通常表示为:

f(n) = g(n) + h(n)

其中:

  • ( g(n) ):从起点走到当前节点的真实代价
  • ( h(n) ):从当前节点到终点的启发式估计
  • ( f(n) ):综合总代价

A 星算法每一步都会优先选择 ( f ) 值最小的节点进行扩展,因此它既能保证找到路径,又比纯粹盲目搜索更高效。

在网格地图中,常用的启发式函数是曼哈顿距离或者欧几里得距离。

如果只允许上下左右移动,曼哈顿距离最合适:

h = \|x_1 - x_2\| + \|y_1 - y_2\|

如果允许斜向移动,也可以使用更接近几何直线的估计方式。

启发式函数越合理,寻路效率通常越高。


15.6 A 星算法的搜索过程

A 星算法一般会维护两个集合:开放列表和关闭列表。

开放列表保存"还没有处理,但值得继续扩展"的节点;关闭列表保存"已经处理过,不需要重复访问"的节点。

算法每次从开放列表中取出代价最低的节点,检查它是否已经到达终点。如果没有,就继续扩展它的邻居节点,并计算它们的代价。

这个过程看上去像是在不断试探,但实际上它是非常有方向感的搜索。

因为启发式函数会引导搜索朝着目标靠近,所以 A 星不会像暴力遍历那样浪费太多时间。

对于大多数 2D 游戏地图而言,A 星已经足够实用。

不过,A 星也不是万能的。

如果地图特别大、障碍特别多,或者路径需要频繁重算,A 星也会有性能压力。

因此在实际项目中,常常会结合路径缓存、分区寻路、局部避障等手段进一步优化。


15.7 视野检测与敌人感知

AI 不是随时都应该知道玩家在哪。

为了让敌人的行为更自然,通常需要加入感知系统,例如视野范围、角度检测和距离判断。

只有当玩家进入敌人的视野范围后,它才从巡逻状态切换到警觉或追击状态。

最基础的感知方式是距离判断。

如果玩家与敌人之间的距离满足:

d = \\sqrt{\\left(x_1 - x_2\\right)\^2 + \\left(y_1 - y_2\\right)\^2}

并且 ( d ) 小于某个阈值,那么敌人就认为玩家进入了有效范围。

如果还需要判断方向,则可以进一步比较角度,确认玩家是否位于敌人前方。

这种感知系统可以让 AI 更像"看见"了玩家,而不是无条件全局追踪。

这样一来,敌人的行为会更合理,也更有游戏性。


15.8 路径更新与动态环境

在很多游戏中,地图并不是静态不变的。

门会打开,桥会塌陷,障碍会消失,角色会推动箱子,关卡结构也可能因为剧情或战斗而变化。

这意味着 AI 的路径不能永远固定不变,而需要定期重新计算。

这就是路径更新机制的重要性。

例如,敌人可以每隔若干帧重新规划一次路径,而不是每一帧都重新算。

这样既能保证它跟得上环境变化,也不会造成过高的计算负担。

在复杂场景中,有时还会加入局部避障逻辑。

也就是说,即使全局路径已经规划好,AI 在移动过程中仍然要检测前方有没有临时障碍,然后适当绕开。

这类思路可以让寻路系统既稳定又灵活。


15.9 使用 GPT-5.4 生成 AI 代码

在实际开发中,AI 行为和寻路系统往往涉及较多逻辑,尤其是状态切换、路径规划、目标检测和行为封装等部分。

这类内容很适合通过合理的提示词来生成基础框架,再由开发者根据项目需要进行修改和扩展。

下面给出一个适合生成敌人 AI 系统的提示词块:

text 复制代码
请用 Pygame 实现一个完整的敌人 AI 系统,要求:

1. 使用有限状态机组织 AI 行为
2. 实现巡逻、追击、攻击、逃跑等行为
3. 集成 A 星寻路算法
4. 支持视野检测和距离判断
5. 支持路径重新规划
6. 提供完整可运行代码
7. 代码中加入详细中文注释
8. 使用字体文件路径加载字体,不使用系统字体枚举
9. 结构清晰,方便后续扩展

如果你希望代码更偏项目化,也可以补充:

text 复制代码
额外要求:
1. 敌人发现玩家后播放警觉状态
2. 玩家离开视野后返回巡逻
3. 追击时遇到障碍会自动寻路
4. 支持地图网格碰撞
5. 敌人距离玩家过近时进入攻击状态

15.10 综合实战:敌人巡逻、追击与 A 星寻路演示

下面这个示例会把本章的核心内容整合起来,包括:

  • 基础 AI 行为
  • 巡逻和追击
  • A 星寻路
  • 网格地图障碍检测
  • 目标视野判断
  • 安全字体加载

为了让示例更清晰,这里使用方格地图来表示障碍和可通行区域。

python 复制代码
import pygame
import sys
import os
import math
import heapq
import random

pygame.init()
screen = pygame.display.set_mode((960, 640))
pygame.display.set_caption("游戏AI基础与寻路算法演示")
clock = pygame.time.Clock()

def get_font(size):
    font_paths = [
        r"C:\Windows\Fonts\simhei.ttf",
        r"C:\Windows\Fonts\msyh.ttc",
        r"C:\Windows\Fonts\simsun.ttc",
    ]
    for path in font_paths:
        if os.path.exists(path):
            try:
                return pygame.font.Font(path, size)
            except:
                pass
    return pygame.font.Font(None, size)

font = get_font(24)

TILE_SIZE = 32
GRID_W = 30
GRID_H = 20

grid = [[0 for _ in range(GRID_W)] for _ in range(GRID_H)]

for x in range(GRID_W):
    grid[0][x] = 1
    grid[GRID_H - 1][x] = 1

for y in range(GRID_H):
    grid[y][0] = 1
    grid[y][GRID_W - 1] = 1

for x in range(4, 10):
    grid[6][x] = 1

for y in range(8, 15):
    grid[y][14] = 1

for x in range(17, 24):
    grid[12][x] = 1

class Node:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.g = 0
        self.h = 0
        self.f = 0
        self.parent = None

    def __lt__(self, other):
        return self.f < other.f

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

def heuristic(a, b):
    return abs(a.x - b.x) + abs(a.y - b.y)

def astar(grid_map, start, end):
    rows = len(grid_map)
    cols = len(grid_map[0]) if rows > 0 else 0

    start_node = Node(start[0], start[1])
    end_node = Node(end[0], end[1])

    open_list = []
    closed_set = set()

    heapq.heappush(open_list, start_node)

    while open_list:
        current = heapq.heappop(open_list)
        closed_set.add((current.x, current.y))

        if current == end_node:
            path = []
            while current:
                path.append((current.x, current.y))
                current = current.parent
            return path[::-1]

        neighbors = [
            (0, -1), (0, 1), (-1, 0), (1, 0)
        ]

        for dx, dy in neighbors:
            nx = current.x + dx
            ny = current.y + dy

            if nx < 0 or nx >= cols or ny < 0 or ny >= rows:
                continue

            if grid_map[ny][nx] == 1:
                continue

            if (nx, ny) in closed_set:
                continue

            neighbor = Node(nx, ny)
            neighbor.g = current.g + 1
            neighbor.h = heuristic(neighbor, end_node)
            neighbor.f = neighbor.g + neighbor.h
            neighbor.parent = current

            existing = next((n for n in open_list if n == neighbor), None)
            if existing and existing.g <= neighbor.g:
                continue

            heapq.heappush(open_list, neighbor)

    return None

class Player:
    def __init__(self, x, y):
        self.position = pygame.math.Vector2(x, y)
        self.speed = 3
        self.radius = 12

    def update(self):
        keys = pygame.key.get_pressed()
        move = pygame.math.Vector2(0, 0)

        if keys[pygame.K_LEFT]:
            move.x -= 1
        if keys[pygame.K_RIGHT]:
            move.x += 1
        if keys[pygame.K_UP]:
            move.y -= 1
        if keys[pygame.K_DOWN]:
            move.y += 1

        if move.length() > 0:
            move = move.normalize() * self.speed
            self.position += move

        self.position.x = max(16, min(self.position.x, GRID_W * TILE_SIZE - 16))
        self.position.y = max(16, min(self.position.y, GRID_H * TILE_SIZE - 16))

    def draw(self, surface):
        pygame.draw.circle(surface, (60, 180, 255), (int(self.position.x), int(self.position.y)), self.radius)

class Enemy:
    def __init__(self, x, y, grid_map):
        self.position = pygame.math.Vector2(x, y)
        self.speed = 2.2
        self.radius = 12
        self.grid_map = grid_map
        self.state = "patrol"
        self.target = None
        self.waypoints = [
            pygame.math.Vector2(3 * TILE_SIZE + 16, 3 * TILE_SIZE + 16),
            pygame.math.Vector2(10 * TILE_SIZE + 16, 3 * TILE_SIZE + 16),
            pygame.math.Vector2(10 * TILE_SIZE + 16, 10 * TILE_SIZE + 16),
            pygame.math.Vector2(3 * TILE_SIZE + 16, 10 * TILE_SIZE + 16),
        ]
        self.waypoint_index = 0
        self.path = []
        self.path_index = 0
        self.path_timer = 0

    def can_see_player(self, player):
        distance = (player.position - self.position).length()
        return distance < 220

    def move_towards(self, target_pos):
        direction = target_pos - self.position
        if direction.length() > 0:
            direction = direction.normalize()
            self.position += direction * self.speed

    def update_patrol(self):
        target = self.waypoints[self.waypoint_index]
        self.move_towards(target)
        if (target - self.position).length() < 6:
            self.waypoint_index = (self.waypoint_index + 1) % len(self.waypoints)

    def update_path_chase(self, player):
        self.path_timer += 1
        if self.path_timer >= 30 or not self.path:
            self.path_timer = 0
            start = (int(self.position.x // TILE_SIZE), int(self.position.y // TILE_SIZE))
            end = (int(player.position.x // TILE_SIZE), int(player.position.y // TILE_SIZE))
            self.path = astar(self.grid_map, start, end)
            self.path_index = 0

        if self.path and self.path_index < len(self.path):
            tx = self.path[self.path_index][0] * TILE_SIZE + TILE_SIZE / 2
            ty = self.path[self.path_index][1] * TILE_SIZE + TILE_SIZE / 2
            target_pos = pygame.math.Vector2(tx, ty)
            self.move_towards(target_pos)

            if (target_pos - self.position).length() < 6:
                self.path_index += 1

    def update(self, player):
        distance = (player.position - self.position).length()

        if distance < 40:
            self.state = "attack"
        elif self.can_see_player(player):
            self.state = "chase"
            self.target = player
        else:
            self.state = "patrol"
            self.target = None
            self.path = []
            self.path_index = 0

        if self.state == "patrol":
            self.update_patrol()
        elif self.state == "chase":
            self.update_path_chase(player)
        elif self.state == "attack":
            pass

    def draw(self, surface):
        color = (255, 70, 70) if self.state != "attack" else (255, 220, 80)
        pygame.draw.circle(surface, color, (int(self.position.x), int(self.position.y)), self.radius)

        if self.path:
            points = []
            for gx, gy in self.path:
                points.append((gx * TILE_SIZE + TILE_SIZE // 2, gy * TILE_SIZE + TILE_SIZE // 2))
            if len(points) > 1:
                pygame.draw.lines(surface, (255, 255, 0), False, points, 2)

player = Player(100, 100)
enemy = Enemy(300, 300, grid)

running = True
while running:
    dt = clock.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    player.update()
    enemy.update(player)

    screen.fill((25, 25, 35))

    for y in range(GRID_H):
        for x in range(GRID_W):
            rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
            if grid[y][x] == 1:
                pygame.draw.rect(screen, (70, 70, 80), rect)
            else:
                pygame.draw.rect(screen, (40, 40, 50), rect)
            pygame.draw.rect(screen, (55, 55, 65), rect, 1)

    player.draw(screen)
    enemy.draw(screen)

    info1 = font.render("方向键控制玩家移动", True, (255, 255, 255))
    info2 = font.render("敌人会巡逻  发现玩家后进行 A 星追击", True, (255, 255, 255))
    info3 = font.render(f"敌人状态: {enemy.state}", True, (255, 255, 255))

    screen.blit(info1, (10, 10))
    screen.blit(info2, (10, 40))
    screen.blit(info3, (10, 70))

    pygame.display.flip()

pygame.quit()
sys.exit()

15.11 本章总结

本章介绍了游戏 AI 的基础概念,重点讲解了追逐、逃避、巡逻、状态机和 A 星寻路等核心内容。

AI 在游戏中的作用,不是制造真正意义上的"智能",而是为角色赋予合理、可理解、可交互的行为模式。

当你能让敌人看起来会思考、会判断、会寻找路径时,游戏的可玩性和紧张感都会明显提升。

需要特别记住的是,AI 设计的重点不是复杂,而是适合游戏。

一个好的游戏 AI,应该在性能、可控性和表现力之间找到平衡。

它既要能做出像样的判断,也要方便调试和扩展。

本章所讲的状态机和 A 星算法,就是构建这类系统最常用、也最实用的基础工具。

本章知识点回顾

知识点 主要内容
AI 行为 巡逻、追击、逃避、攻击
状态机 用有限状态组织行为
启发式搜索 A 星算法的核心思想
路径代价 ( f(n) = g(n) + h(n) )
视野检测 距离判断、范围判断
路径更新 动态地图中的重新规划

课后练习

  1. 为敌人增加视野锥检测。
  2. 实现攻击冷却时间。
  3. 让敌人在失去目标后回到巡逻点。
  4. 给 A 星算法加入对角线移动。
  5. 实现多个敌人共享同一张地图的寻路系统。

下章预告

在下一章中,我们将学习存档系统,掌握游戏数据的持久化保存、读取和恢复技术。

相关推荐
DeepSCRM2 小时前
出海转化率低?拆解DeepSeek如何成为跨境营销的“破壁”利器
人工智能
imbackneverdie2 小时前
怎么将AI生成的图片转成可编辑的矢量图?
图像处理·人工智能·aigc·科研绘图·ai工具·gemini·ai生图
Gofarlic_OMS2 小时前
SolidEdge专业许可证管理工具选型关键评估标准
java·大数据·运维·服务器·人工智能
搬砖者(视觉算法工程师)2 小时前
为何英伟达的世界动作大模型DreamZero在机器人技术基准测试中表现如此出色?
人工智能
Σίσυφος19002 小时前
SO(3) (本质理解)
人工智能
龙文浩_2 小时前
AI深度学习中的张量计算&函数&索引&形状的代码案例
人工智能·深度学习
YunQuality2 小时前
当SPC焕发新生:云质信息重构制造质量管理新范式
人工智能·软件需求·工业软件
永霖光电_UVLED2 小时前
英特尔斥资142亿美元回购爱尔兰Fab 34晶圆厂股权
人工智能
智算菩萨2 小时前
【Pygame】第17章 游戏用户界面系统与菜单交互设计实现
游戏·ui·pygame