AI帮你找BugCocos Creator 游戏 AI 自动化测试实战全指南
🤖 AI帮你找BugCocos Creator 游戏 AI 自动化测试实战全指南
这句话,几乎每个游戏开发者都说过。你自己测了几十遍没问题,游戏一上线,玩家5分钟内就触发了一个角色卡在墙里的Bug,还有人发现了你根本不知道存在的内存泄漏……
问题的根本在于:人工测试的路径覆盖率极低。一个普通玩家的操作方式,和你预设的测试路径相差甚远。
但如果有一个”AI玩家”,每天24小时不间断地以各种奇怪方式玩你的游戏,自动记录所有异常呢?
本文将带你了解 AI 游戏自动化测试的核心原理,并在 Cocos Creator 项目中从零搭建一套”AI测试智能体”系统。
游戏测试与普通Web/App测试有本质区别,这使得传统自动化框架(Selenium/Playwright)完全不适用:
| 维度 | 普通软件测试 | 游戏测试 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
🐛 游戏中最难靠人工发现的Bug类型
玩家在边角、斜坡、门框等特殊地形处,角色会卡进碰撞体或穿过墙壁。需要AI以各种随机路径移动才能覆盖所有边缘情况。
粒子系统未正确回收、音频节点累积、节点池溢出。短时间测试不会触发,需要AI连续运行几小时才能暴露。人工很难坚持测试这么长时间。
同时触发多个技能、在特定帧数内同时死亡、资源恰好为0时触发攻击。这些组合在人工测试中几乎不会主动去试。
特定场景下同屏粒子数激增、DrawCall突破阈值、物理计算帧率骤降。需要持续性能监控才能精确定位触发条件。
一套完整的AI游戏测试系统,由五个相互协作的模块组成:
📡 环境感知模块
负责”看懂”游戏画面。实时采集:角色位置、生命值/魔法值、周围NPC状态、场景资源、内存/CPU/FPS等性能指标。是整个系统的”眼睛”。
🎮 行为模拟模块
负责”像玩家一样”操作游戏。使用强化学习(DQN算法)训练AI掌握游戏技能,在探索过程中刻意尝试各种边缘操作(卡角、快速技能切换等),不断发现新测试路径。
🐛 Bug检测模块
负责”判断是否出了问题”。分三层:① 规则检测(HP<0、位置卡死等);② 数据异常检测(CPU/内存突增);③ 日志分析(用NLP分类错误类型)。
📋 测试管理 + 📊 报告生成
前者负责调度多个AI测试Agent并行运行,后者将所有发现的Bug整理成可读的HTML/Markdown报告,含截图、复现路径、性能曲线,直接推送给开发者。
在 Cocos Creator 中,最实用的方案是把测试Agent直接嵌入游戏本身(仅在开发/测试构建中启用),通过 TypeScript 脚本控制游戏逻辑并监控异常。
import {_decorator, Component, Node, Vec3, director, sys,
profiler, macro
} from 'cc';
const { ccclass, property } = _decorator;
/** 测试动作枚举 */
enum TestAction {
MOVE_UP = 'moveUp',
MOVE_DOWN = 'moveDown',
MOVE_LEFT = 'moveLeft',
MOVE_RIGHT = 'moveRight',
ATTACK = 'attack',
SKILL_1 = 'skill1',
SKILL_2 = 'skill2',
JUMP = 'jump',
INTERACT = 'interact',
}
/** Bug记录结构 */
interface BugRecord {
type: string;
severity: 'critical' | 'high' | 'medium' | 'low';
description: string;
position?: Vec3;
timestamp: number;
frameCount: number;
screenshot?: string; // base64截图(可选)
}
@ccclass('GameTestAgent')
export class GameTestAgent extends Component {
// ========== 配置参数 ==========
@property({ tooltip: '是否启用AI测试(仅开发环境)' })
enableTesting: boolean = false;
@property({ tooltip: '测试持续时间(秒),0=无限' })
testDuration: number = 300; // 默认测试5分钟
@property({ tooltip: '每次动作之间的间隔(秒)' })
actionInterval: number = 0.1;
@property({ tooltip: '卡死检测阈值(秒),角色超过此时间不动视为卡死' })
stuckThreshold: number = 3.0;
// ========== 内部状态 ==========
private playerNode: Node | null = null;
private lastPosition: Vec3 = new Vec3();
private positionUnchangedTime: number = 0;
private actionTimer: number = 0;
private testTimer: number = 0;
private frameCount: number = 0;
private bugs: BugRecord[] = [];
// 性能监控历史
private fpsHistory: number[] = [];
private memHistory: number[] = [];
private maxFpsDropCount: number = 0;
// Q-learning简化版:动作权重(随运行调整)
private actionWeights: Record<TestAction, number> = {
[TestAction.MOVE_UP]: 1.0,
[TestAction.MOVE_DOWN]: 1.0,
[TestAction.MOVE_LEFT]: 1.0,
[TestAction.MOVE_RIGHT]: 1.0,
[TestAction.ATTACK]: 1.5, // 攻击动作权重稍高
[TestAction.SKILL_1]: 1.5,
[TestAction.SKILL_2]: 1.5,
[TestAction.JUMP]: 1.2,
[TestAction.INTERACT]: 1.2,
};
start() {
if (!this.enableTesting) return;
if (!CC_DEBUG) {
console.warn('⚠️ GameTestAgent 仅应在 DEBUG 构建中启用!');
return;
}
this.playerNode = director.getScene()?.getChildByName('Player') ?? null;
if (!this.playerNode) {
console.error('❌ 未找到 Player 节点,测试中止');
return;
}
this.lastPosition.set(this.playerNode.worldPosition);
console.log('🤖 AI测试智能体已启动');
console.log(`⏱️ 测试时长: ${this.testDuration > 0 ? this.testDuration + '秒' : '无限'}`);
}
update(dt: number) {
if (!this.enableTesting || !this.playerNode) return;
this.frameCount++;
this.testTimer += dt;
// 检查测试是否超时
if (this.testDuration > 0 && this.testTimer >= this.testDuration) {
this.finishTest();
return;
}
// 执行动作
this.actionTimer += dt;
if (this.actionTimer >= this.actionInterval) {
this.actionTimer = 0;
this.executeRandomAction();
}
// 持续监控
this.monitorPlayerState(dt);
this.monitorPerformance();
}
// ===== 行为模拟:按权重随机选择动作 =====
private executeRandomAction() {
const actions = Object.keys(this.actionWeights) as TestAction[];
const weights = actions.map(a => this.actionWeights[a]);
const totalWeight = weights.reduce((s, w) => s + w, 0);
let rand = Math.random() * totalWeight;
let selectedAction: TestAction = actions[0];
for (let i = 0; i < actions.length; i++) {
rand -= weights[i];
if (rand <= 0) {
selectedAction = actions[i];
break;
}
}
this.dispatchAction(selectedAction);
}
private dispatchAction(action: TestAction) {
// 通过全局事件系统驱动游戏逻辑
// 注意:需要在游戏PlayerController中监听这些事件
director.getScene()?.emit(`test:action`, { action, frame: this.frameCount });
}
// ===== 状态监控:检测卡死和逻辑异常 =====
private monitorPlayerState(dt: number) {
if (!this.playerNode) return;
const currentPos = this.playerNode.worldPosition;
// 检测卡死:位置长时间不变
if (Vec3.distance(currentPos, this.lastPosition) < 0.01) {
this.positionUnchangedTime += dt;
if (this.positionUnchangedTime >= this.stuckThreshold) {
this.reportBug({
type: 'stuck',
severity: 'high',
description: `角色在位置 (${currentPos.x.toFixed(1)}, ${currentPos.y.toFixed(1)}, ${currentPos.z.toFixed(1)}) 卡死超过 ${this.stuckThreshold} 秒`,
position: currentPos.clone(),
});
// 尝试自动脱困(向随机方向强制移动)
this.attemptUnstuck();
this.positionUnchangedTime = 0; // 重置计时
}
} else {
this.positionUnchangedTime = 0;
this.lastPosition.set(currentPos);
}
// 检测HP/MP是否异常(从场景获取玩家数据)
this.checkPlayerStats();
}
private checkPlayerStats() {
// 通过场景事件获取玩家当前状态
const stats = { hp: 0, mp: 0 }; // 实际项目中从PlayerController获取
director.getScene()?.emit('test:getPlayerStats', (data: { hp: number; mp: number }) => {
stats.hp = data.hp;
stats.mp = data.mp;
});
if (stats.hp < 0) {
this.reportBug({
type: 'hp_negative',
severity: 'critical',
description: `玩家HP值为负数:${stats.hp}(应不低于0)`,
});
}
if (stats.mp < 0) {
this.reportBug({
type: 'mp_negative',
severity: 'medium',
description: `玩家MP值为负数:${stats.mp}(应不低于0)`,
});
}
}
// ===== 性能监控 =====
private monitorPerformance() {
// 每60帧(约1秒)采集一次
if (this.frameCount % 60 !== 0) return;
const fps = Math.round(1 / (director.getDeltaTime() || 0.016));
this.fpsHistory.push(fps);
// 检测FPS骤降
if (fps < 30 && this.fpsHistory.length > 5) {
this.maxFpsDropCount++;
if (this.maxFpsDropCount >= 3) {
this.reportBug({
type: 'fps_drop',
severity: 'high',
description: `FPS连续 ${this.maxFpsDropCount} 秒低于30帧(当前:${fps} FPS)`,
});
this.maxFpsDropCount = 0;
}
} else {
this.maxFpsDropCount = 0;
}
// Cocos Creator 内存监控(通过sys)
if (sys.isNative) {
// 原生平台可获取真实内存数据
const memUsage = (sys as any).getMemoryUsageInMB?.() ?? 0;
this.memHistory.push(memUsage);
if (this.memHistory.length > 2) {
const prev = this.memHistory[this.memHistory.length - 2];
const curr = this.memHistory[this.memHistory.length - 1];
// 单秒内存增长超过10MB,可能存在泄漏
if (curr - prev > 10) {
this.reportBug({
type: 'memory_spike',
severity: 'high',
description: `检测到内存异常增长:+${(curr - prev).toFixed(1)} MB/s(当前:${curr.toFixed(0)} MB)`,
});
}
}
}
}
// ===== 尝试自动脱困 =====
private attemptUnstuck() {
// 向反方向随机移动尝试脱困
const escapeActions = [
TestAction.MOVE_UP, TestAction.MOVE_DOWN,
TestAction.MOVE_LEFT, TestAction.MOVE_RIGHT, TestAction.JUMP
];
for (let i = 0; i < 5; i++) {
const act = escapeActions[Math.floor(Math.random() * escapeActions.length)];
this.dispatchAction(act);
}
}
// ===== Bug上报 =====
private reportBug(bug: Omit<BugRecord, 'timestamp' | 'frameCount'>) {
const record: BugRecord = {
...bug,
timestamp: Date.now(),
frameCount: this.frameCount,
};
this.bugs.push(record);
const severityIcon = {
critical: '🔴', high: '🟠', medium: '🟡', low: '🟢'
}[bug.severity];
console.error(
`${severityIcon} [AI测试] BUG发现 [${bug.type}]: ${bug.description}`,
`| 帧:${this.frameCount} | 时间:${this.testTimer.toFixed(1)}s`
);
}
// ===== 测试结束:生成报告 =====
private finishTest() {
this.enabled = false; // 停止update
const report = this.generateReport();
console.log('\n' + '='.repeat(60));
console.log('📊 AI自动化测试报告');
console.log('='.repeat(60));
console.log(report);
console.log('='.repeat(60) + '\n');
// 将报告保存到本地文件(仅原生平台)
if (sys.isNative) {
// 可在此处使用 jsb.fileUtils 保存报告
}
}
private generateReport(): string {
const duration = this.testTimer.toFixed(1);
const totalBugs = this.bugs.length;
const criticalBugs = this.bugs.filter(b => b.severity === 'critical').length;
const highBugs = this.bugs.filter(b => b.severity === 'high').length;
const avgFPS = this.fpsHistory.length > 0
? (this.fpsHistory.reduce((s, v) => s + v, 0) / this.fpsHistory.length).toFixed(1)
: 'N/A';
let report = `测试时长: ${duration}s | 总帧数: ${this.frameCount}\n`;
report += `Bug统计: 🔴严重×${criticalBugs} 🟠高×${highBugs} | 总计×${totalBugs}\n`;
report += `平均帧率: ${avgFPS} FPS\n\n`;
report += `Bug详情:\n`;
this.bugs.forEach((bug, i) => {
const icon = { critical:'🔴', high:'🟠', medium:'🟡', low:'🟢' }[bug.severity];
report += ` ${i+1}. ${icon}[${bug.type}] ${bug.description}\n`;
report += ` 位置:${bug.position ? `(${bug.position.x.toFixed(1)},${bug.position.y.toFixed(1)})` : '未记录'} | 帧:${bug.frameCount}\n`;
});
return report;
}
}
import { _decorator, Component, director, Vec3, RigidBody2D, input, Input } from 'cc';const { ccclass } = _decorator;
/**
* 测试桥接组件:挂载到PlayerController同节点
* 监听AI测试Agent发出的动作事件,驱动真实的玩家控制逻辑
* 测试完毕后可直接删除,不影响正式代码
*/
@ccclass('GameTestBridge')
export class GameTestBridge extends Component {
private moveSpeed: number = 5;
onLoad() {
if (!CC_DEBUG) return;
// 监听AI测试智能体发出的动作指令
director.getScene()?.on('test:action', this.onTestAction, this);
// 向AI测试智能体提供玩家状态
director.getScene()?.on('test:getPlayerStats', this.provideStats, this);
console.log('🎮 GameTestBridge 已激活');
}
onDestroy() {
director.getScene()?.off('test:action', this.onTestAction, this);
director.getScene()?.off('test:getPlayerStats', this.provideStats, this);
}
private onTestAction(data: { action: string; frame: number }) {
const rb = this.getComponent(RigidBody2D);
// 将AI动作映射到实际游戏操作
switch (data.action) {
case 'moveUp': this.move(new Vec3(0, this.moveSpeed, 0), rb); break;
case 'moveDown': this.move(new Vec3(0, -this.moveSpeed, 0), rb); break;
case 'moveLeft': this.move(new Vec3(-this.moveSpeed, 0, 0), rb); break;
case 'moveRight': this.move(new Vec3(this.moveSpeed, 0, 0), rb); break;
case 'attack': this.triggerAttack(); break;
case 'skill1': this.triggerSkill(1); break;
case 'skill2': this.triggerSkill(2); break;
case 'jump': this.triggerJump(rb); break;
case 'interact': this.triggerInteract(); break;
}
}
private move(velocity: Vec3, rb: RigidBody2D | null) {
if (rb) {
rb.linearVelocity = { x: velocity.x, y: velocity.y };
} else {
// 无刚体时直接修改位置
this.node.setWorldPosition(
this.node.worldPosition.x + velocity.x * 0.1,
this.node.worldPosition.y + velocity.y * 0.1,
this.node.worldPosition.z
);
}
}
private triggerAttack() {
// 调用玩家的攻击方法(根据实际PlayerController接口调整)
this.node.emit('player:attack');
}
private triggerSkill(skillId: number) {
this.node.emit('player:skill', { id: skillId });
}
private triggerJump(rb: RigidBody2D | null) {
if (rb) rb.linearVelocity = { x: rb.linearVelocity.x, y: 8 };
this.node.emit('player:jump');
}
private triggerInteract() {
this.node.emit('player:interact');
}
private provideStats(callback: (data: { hp: number; mp: number }) => void) {
// 从实际玩家组件获取数值(根据项目实际接口调整)
const stats = { hp: 100, mp: 50 }; // 示例值
callback(stats);
}
第一层:规则检测(最直接)
| 规则类型 | 检测条件 | 严重程度 | 自动处理 |
|---|---|---|---|
|
|
|
严重 |
|
|
|
|
高 |
|
|
|
|
严重 |
|
|
|
|
严重 |
|
|
|
|
高 |
|
第二层:数据监控(最全面)
| 监控指标 | 预警阈值 | 严重阈值 | 平台 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
第三层:异常检测(最智能)
📊 统计异常检测
维护历史基线数据(如”正常情况下这个场景DrawCall平均150,标准差20″),当实时值偏离基线超过3个标准差时触发预警。能发现难以预先定义规则的”说不清楚但就是不对”的问题。
📝 日志NLP分析
游戏运行产生的错误日志用关键词分类:ERROR级别日志自动截图并记录调用栈,WARN级别聚合统计频次。高频WARN往往预示着真正的Bug。
❌ 纯人工测试的局限
-
测试人员每天有限的精力(8小时)
-
人工路径高度重复,边缘条件覆盖不足
-
无法持续7×24小时测试
-
内存泄漏、性能退化需要主动监控
-
测试报告依赖手工整理,容易遗漏
✅ AI自动化测试的优势
-
7×24小时持续运行,不疲劳
-
随机+权重组合,覆盖人工测试盲区
-
性能数据自动记录,趋势自动分析
-
Bug自动截图+复现路径记录
-
可并行启动多个实例,倍增覆盖率
| 测试维度 | 人工测试 | AI自动化测试 | 提升 |
|---|---|---|---|
|
|
|
|
+2-3倍 |
|
|
|
|
+3倍 |
|
|
|
|
+70% |
|
|
|
|
+167% |
|
|
|
|
质变提升 |
📐 分阶段接入策略
添加GameTestAgent组件,配置HP/MP/位置规则检测。不需要AI行为,只要有监控,就能发现大量现有问题。这是最低成本的起点。
添加GameTestBridge适配层,让AI能够驱动玩家控制器。配置动作权重,让高风险操作(快速技能切换、边缘位置移动)权重更高。
启用FPS/内存/DrawCall自动记录,配置阈值告警。在每次提交代码后的CI/CD流程中自动触发5分钟压力测试。
在CI/CD中启动3-5个并行测试实例,不同实例配置不同的动作权重偏好(激进型/探索型/战斗型),快速积累覆盖率。
⚙️ 推荐的CI/CD集成方式
if (!CC_DEBUG) return; 判断,确保Release包中完全不运行测试逻辑• AI测试无法替代人工体验测试:AI能发现技术Bug,但”手感不好”、”关卡太难”这类体验问题仍需人工判断• 别被误报淹没:初期阈值设置宽松些,避免大量低价值预警让开发者产生”狼来了”疲劳游戏质量的天花板,往往不是功能的缺失,而是那些在测试中漏掉的边缘Bug。
AI自动化测试解决的正是这个问题。它不知道疲劳,不会重复走同一条路径,会主动探索各种奇怪的操作组合,并在发现异常的第一时间精确记录复现路径。
| 方案 | 适合场景 | 接入成本 |
|---|---|---|
|
|
|
1天 |
|
|
|
1周 |
|
|
|
2-3周 |
夜雨聆风