1. 从标题说起:为什么“不用游戏引擎”这件事值得聊
第一次看到“游戏引擎都没用!纯AI又上线了一款蚂蚁搬家小游戏!”这个标题,我的反应是:又是一个标题党。但点进去把整个项目跑了一遍之后,我改主意了——它确实没用Unity、没用Cocos、没用Laya,甚至连Phaser这类轻量级2D引擎都没碰,整个游戏就是原生Canvas 2D + 微信小游戏原生API硬写出来的,而AI在其中承担了从玩法设计、代码生成到美术资源描述的大部分工作。
这件事的价值不在于“蚂蚁搬家”这个题材有多新鲜,而在于它验证了一条路径:当AI能稳定产出Canvas级别的交互逻辑时,很多小体量游戏真的不需要引入引擎。引擎带来的包体膨胀、学习成本、构建链路复杂度,在“一个下午要出Demo”的场景下反而是负担。
这篇文章我想把这条路径完整拆开讲。适合三类人看:一是想用AI辅助做微信小游戏但不知道从哪下手的独立开发者;二是被引擎构建流程折磨过、想回归原生Canvas的前端;三是想搞清楚“AI编程在游戏这个具体场景里到底能干什么、不能干什么”的技术观察者。全文会围绕微信开发者工具、Canvas绘图、微信小游戏、AI辅助编程这几个核心关键词展开,把设计思路、代码细节、踩坑记录都摊开说。
先说结论:这个项目能跑起来,靠的不是AI有多神,而是把问题切得足够小。蚂蚁搬家这个玩法本身逻辑闭环很短——蚂蚁找食物、搬回巢、循环计分,没有物理碰撞、没有复杂状态机、没有网络同步。这种“小闭环”恰好落在当前AI代码生成的能力甜区里。换成MMO或者带物理的横版动作,纯AI硬写Canvas就是自找麻烦。
2. 整体设计思路:为什么敢把引擎扔掉
2.1 引擎到底帮你做了什么,这个项目又怎么补上
要判断“能不能不用引擎”,得先清楚引擎替你干了哪些活。以2D小游戏为例,引擎通常提供四块能力:渲染管线(精灵、图层、批处理)、资源管理(图集、预加载、释放)、场景与节点系统(生命周期、层级、变换)、周边工具链(编辑器、动画、粒子)。
蚂蚁搬家这个项目里,这四块被这样替代:
| 引擎能力 | 本项目替代方案 | 代价 |
|---|---|---|
| 渲染管线 | Canvas 2D 直接 drawImage / fillRect | 无批处理,但对象数量少(<50)无所谓 |
| 资源管理 | 微信小游戏 Image 对象 + 手动缓存 | 需要自己写加载计数和失败重试 |
| 场景节点 | 普通 JS 对象数组 + 每帧遍历 | 没有层级变换,坐标全靠手算 |
| 工具链 | AI生成代码 + 手写少量配置 | 没有可视化编辑器,改布局靠改数字 |
关键判断依据是对象数量和逻辑复杂度。蚂蚁搬家同屏最多十几只蚂蚁、几个食物点、一个巢穴,总绘制调用每帧不超过60次。这个量级下Canvas 2D的性能完全够用,引擎的批处理优势体现不出来。反过来,如果同屏要画几百个粒子或者上千个精灵,那还是老老实实上引擎。
2.2 AI在这个项目里的真实分工
很多人对“AI做游戏”有误解,以为是丢一句话进去就出来一个完整游戏。实际流程更像这样:
- 玩法拆解:人来做。我告诉AI“蚂蚁从巢出发,随机游走找食物,找到后沿原路返回,回巢加分”,这是人定的规则。
- 状态机设计:AI辅助。让AI把“游走/锁定食物/搬运/回巢”四个状态和转移条件列出来,它给的结构基本可用,但边界条件(比如食物被别的蚂蚁抢了怎么办)需要人补。
- 核心循环代码:AI生成。requestAnimationFrame的主循环、坐标更新、碰撞检测(其实就是距离判断),这些AI写得又快又对。
- 美术资源:AI描述 + 人调整。蚂蚁和食物用简单的几何图形拼,AI给出绘制参数,人微调颜色和比例。
- 调试与性能:人主导。AI不太会主动告诉你“这里每帧创建对象会导致GC抖动”,这类经验得自己上。
提示:把AI当成一个“写代码很快但不懂业务边界”的初级程序员,你负责定规则和验收,它负责敲键盘。这个心态摆正了,效率提升非常明显。
2.3 微信小游戏这个载体的特殊性
选微信小游戏而不是H5网页,有几个现实原因。一是分发,小游戏可以直接在微信里分享,用户点开就玩,不用装App;二是API能力,微信提供了wx.createCanvas、wx.onTouchStart这些封装好的接口,比浏览器里自己处理兼容性省事;三是变现路径,激励视频广告的接入在小游戏里是标准化的。
但代价是包体限制。微信小游戏主包不能超过4MB,整个包还要算上代码和资源。这就是为什么“不用引擎”在这里特别香——Unity打包出来的小游戏,光引擎运行时就好几MB,稍微加点资源就爆了。纯Canvas方案,代码压缩后几十KB,美术资源用代码画或者用极小的图,包体压力几乎为零。
3. 核心细节拆解:Canvas绘图与游戏循环的实操要点
3.1 Canvas初始化与自适应分辨率
微信小游戏里拿Canvas和浏览器不太一样。浏览器是document.getElementById('canvas'),小游戏里是wx.createCanvas()。而且小游戏默认会创建一个上屏Canvas,你直接用就行。
// 微信小游戏入口,game.js const canvas = wx.createCanvas() const ctx = canvas.getContext('2d') // 获取设备信息做自适应 const sysInfo = wx.getSystemInfoSync() const dpr = sysInfo.pixelRatio const screenWidth = sysInfo.windowWidth const screenHeight = sysInfo.windowHeight // 逻辑分辨率设为设计稿尺寸,物理分辨率乘dpr const DESIGN_WIDTH = 375 const DESIGN_HEIGHT = 667 canvas.width = screenWidth * dpr canvas.height = screenHeight * dpr // 计算缩放比,让设计稿铺满屏幕 const scaleX = canvas.width / DESIGN_WIDTH const scaleY = canvas.height / DESIGN_HEIGHT const scale = Math.min(scaleX, scaleY) // 之后所有绘制都基于设计稿坐标,用ctx.scale统一缩放 ctx.scale(scale, scale)这里有个坑我踩过:不要每帧都调用ctx.scale。scale是累积的,你在主循环里调一次,下一帧就在上一次基础上再缩放,画面会越来越大直到飞出屏幕。正确做法是在初始化时设置一次,或者在每帧开头用ctx.setTransform重置。
注意:
wx.getSystemInfoSync在新版基础库里有替代API,但为了兼容老版本,同步版本仍然可用。如果要做刘海屏适配,还要考虑safeArea字段。
3.2 蚂蚁对象的建模:用最少的字段表达完整状态
AI生成代码时容易犯的毛病是字段给太多。我让AI写蚂蚁对象,它一开始给了二十几个属性,什么legAngle、antennaPhase、bodySegmentCount,全是没用的。实际这个游戏只需要这些:
function createAnt(id) { return { id, x: NEST_X, y: NEST_Y, speed: 1.2 + Math.random() * 0.6, // 每只蚂蚁速度略有差异 state: 'wander', // wander | carry | return targetFood: null, angle: Math.random() * Math.PI * 2, // 游走方向 turnTimer: 0, // 转向计时,避免直线走太远 carryOffset: 0 // 搬运时食物相对蚂蚁的偏移,用于绘制 } }字段少的好处是状态转移清晰。wander状态下蚂蚁随机改变angle,检测到附近有食物就切carry;carry状态下锁定食物对象,朝巢穴方向移动;到达巢穴后食物消失,切回wander。整个状态机就三个状态,用if-else就能写清楚,不需要引入状态机库。
这里有个细节值得说:蚂蚁游走时不能完全随机转向。如果每帧都随机改角度,蚂蚁会原地抖动像抽风。我的做法是给一个turnTimer,每隔0.5到1.5秒才允许改变一次方向,中间保持直线。这样看起来才像真的在爬。
3.3 碰撞检测:距离判断就够了
食物和蚂蚁的“碰撞”本质是“蚂蚁是否靠近食物”。用圆形距离判断,比矩形碰撞更自然,代码也更短:
function checkFoodCollision(ant, foods) { for (let i = 0; i < foods.length; i++) { const food = foods[i] if (food.taken) continue const dx = ant.x - food.x const dy = ant.y - food.y const distSq = dx * dx + dy * dy // 用平方距离避免开根号,性能更好 if (distSq < FOOD_PICK_RADIUS * FOOD_PICK_RADIUS) { return food } } return null }FOOD_PICK_RADIUS这个值需要调。太小了蚂蚁会“穿过”食物不捡,太大了蚂蚁隔着老远就锁定目标显得假。我实测下来,食物绘制半径的1.5倍左右比较合适。如果食物画出来半径是8像素,那拾取半径设12像素。
提示:用平方距离比较是Canvas游戏里的常规优化。开根号在对象少的时候无所谓,但养成习惯没坏处,尤其是后面要加更多蚂蚁的时候。
3.4 绘制顺序与图层管理
Canvas没有图层概念,谁后画谁在上面。蚂蚁搬家的绘制顺序应该是:背景 → 巢穴 → 食物 → 蚂蚁 → 搬运中的食物 → UI文字。搬运中的食物要单独画在蚂蚁之后,否则会被蚂蚁身体盖住。
function render() { // 清屏 ctx.clearRect(0, 0, DESIGN_WIDTH, DESIGN_HEIGHT) drawBackground() drawNest() // 先画没被搬的食物 foods.forEach(f => { if (!f.taken) drawFood(f) }) // 画蚂蚁 ants.forEach(ant => drawAnt(ant)) // 再画被搬的食物,盖在蚂蚁上面 ants.forEach(ant => { if (ant.state === 'carry' && ant.targetFood) { drawFood(ant.targetFood, ant.x, ant.y - 10) } }) drawUI() }这个顺序不是随便定的。我一开始把食物全画在蚂蚁前面,结果搬运中的食物被蚂蚁身体挡住,看起来像食物消失了。调换之后视觉上才连贯。
4. 完整实操流程:从零到能玩的小游戏
4.1 环境准备与项目初始化
第一步是装微信开发者工具。官网下载对应系统的版本,安装后用微信扫码登录。然后新建项目,选择“小游戏”类型,填入AppID(没有的话可以选测试号),模板选“不使用任何模板”。
项目建好后目录结构是这样的:
├── game.js // 入口文件 ├── game.json // 配置文件 ├── project.config.json └── js/ ├── main.js // 主逻辑 ├── ant.js // 蚂蚁相关 └── utils.js // 工具函数game.json里要配置屏幕方向:
{ "deviceOrientation": "portrait", "showStatusBar": false }竖屏是这类休闲游戏的标准选择,单手操作方便。showStatusBar设为false可以让画面顶到状态栏,视觉更沉浸。
4.2 用AI生成核心循环的提示词写法
这是很多人关心的部分:怎么跟AI说,它才能生成能用的代码。我的经验是提示词要包含四要素:运行环境、输入输出、约束条件、代码风格。
一个实际用过的提示词长这样:
环境:微信小游戏,使用wx.createCanvas()获取Canvas,Canvas 2D API。 任务:写一个游戏主循环,使用requestAnimationFrame。 要求: 1. 每帧先清屏,再依次调用update()和render() 2. update()里更新所有蚂蚁位置和状态 3. render()里绘制背景、巢穴、食物、蚂蚁 4. 用deltaTime做时间无关的移动,避免不同帧率下速度不一致 5. 代码用ES6,不要用任何第三方库 输出:完整的main.js代码关键是第4条。如果不强调deltaTime,AI大概率给你写ant.x += ant.speed这种帧率相关的代码,在60Hz和120Hz设备上速度会差一倍。加上deltaTime后:
let lastTime = 0 function loop(timestamp) { const deltaTime = Math.min((timestamp - lastTime) / 16.67, 3) lastTime = timestamp update(deltaTime) render() requestAnimationFrame(loop) }Math.min(..., 3)是防止切后台回来时deltaTime过大导致蚂蚁瞬移。这个细节AI不会主动加,得自己补。
4.3 蚂蚁AI行为的实现细节
“蚂蚁搬家”的核心趣味在于蚂蚁的行为看起来有“智能”。纯随机游走太傻,全自动寻路又太假。我采用的是一种带偏好的随机游走:
function updateAntWander(ant, deltaTime) { ant.turnTimer -= deltaTime if (ant.turnTimer <= 0) { // 有一定概率朝巢穴反方向走,让蚂蚁散开 const awayFromNest = Math.atan2(ant.y - NEST_Y, ant.x - NEST_X) if (Math.random() < 0.3) { ant.angle = awayFromNest + (Math.random() - 0.5) * 1.2 } else { ant.angle += (Math.random() - 0.5) * 1.5 } ant.turnTimer = 30 + Math.random() * 60 // 0.5~1.5秒 } ant.x += Math.cos(ant.angle) * ant.speed * deltaTime ant.y += Math.sin(ant.angle) * ant.speed * deltaTime // 边界反弹 if (ant.x < 20 || ant.x > DESIGN_WIDTH - 20) { ant.angle = Math.PI - ant.angle ant.x = Math.max(20, Math.min(DESIGN_WIDTH - 20, ant.x)) } if (ant.y < 20 || ant.y > DESIGN_HEIGHT - 20) { ant.angle = -ant.angle ant.y = Math.max(20, Math.min(DESIGN_HEIGHT - 20, ant.y)) } }那个“30%概率朝远离巢穴方向走”的设定很关键。没有它,蚂蚁会全挤在巢穴附近打转,食物点根本找不到。加上之后,蚂蚁会自然向外扩散,找到食物的概率大幅提升。
4.4 食物生成与刷新机制
食物不能一次性全放出来,否则蚂蚁搬完就没事干了。我的做法是维持场上食物数量恒定:
const MAX_FOODS = 5 function maintainFoods() { const activeFoods = foods.filter(f => !f.taken) if (activeFoods.length < MAX_FOODS) { const newFood = spawnFood() foods.push(newFood) } // 清理已被搬走很久的食物对象,避免数组无限增长 if (foods.length > 50) { foods = foods.filter(f => !f.taken || f.takenTime > 3000) } }spawnFood要保证食物不生成在巢穴内部,否则蚂蚁一出巢就捡到,没有探索过程:
function spawnFood() { let x, y, distToNest do { x = 40 + Math.random() * (DESIGN_WIDTH - 80) y = 40 + Math.random() * (DESIGN_HEIGHT - 80) distToNest = Math.hypot(x - NEST_X, y - NEST_Y) } while (distToNest < 100) // 至少离巢穴100像素 return { x, y, taken: false, takenTime: 0 } }注意:
Math.hypot在部分老版本基础库上性能一般,如果食物生成频繁可以换成手写平方和。不过这个游戏里生成频率很低,用hypot可读性更好。
4.5 计分与难度递增
计分逻辑简单:每搬回一个食物加10分。但纯线性计分玩久了会腻,所以加了个难度递增:每搬回5个食物,蚂蚁速度提升5%,食物刷新间隔缩短。
let score = 0 let deliveredCount = 0 function onFoodDelivered() { score += 10 deliveredCount++ if (deliveredCount % 5 === 0) { const speedMultiplier = 1 + Math.floor(deliveredCount / 5) * 0.05 ants.forEach(ant => { ant.speed = ant.baseSpeed * speedMultiplier }) } }这里要注意:速度提升要有上限。我一开始没设上限,玩到后面蚂蚁快得像闪电,画面糊成一片。后来加了Math.min(speedMultiplier, 2.0),最多快一倍,体验就正常了。
5. 常见问题与排查技巧实录
5.1 画面闪烁与撕裂
现象:蚂蚁移动时边缘有锯齿或闪烁。
原因:Canvas默认没有开启抗锯齿,且每帧clearRect后立即绘制,如果绘制耗时超过一帧,会出现部分绘制的情况。
解决:一是确保ctx.imageSmoothingEnabled = true;二是如果用了离屏Canvas做预渲染,要等离屏绘制完成再上屏。蚂蚁这种简单图形其实不太会闪烁,出现闪烁多半是因为在render里做了耗时计算(比如每帧重新创建渐变对象)。把渐变对象缓存起来复用即可。
5.2 触摸事件坐标偏移
现象:点击屏幕上的蚂蚁没反应,或者点A位置触发了B位置。
原因:触摸事件返回的是物理像素坐标,而绘制用的是设计稿坐标,两者之间差了dpr和scale。
解决:统一转换。
wx.onTouchStart(e => { const touch = e.touches[0] // 物理坐标转设计稿坐标 const designX = touch.clientX / scale const designY = touch.clientY / scale // 用designX, designY去做碰撞判断 })这个坑几乎每个做小游戏的人都会踩一次。记住一个原则:所有游戏逻辑用设计稿坐标,只在输入和输出两端做转换。
5.3 后台切换回来蚂蚁瞬移
现象:切到微信聊天再切回来,蚂蚁位置突然跳变。
原因:requestAnimationFrame在后台会暂停,切回来时timestamp跳变,deltaTime巨大。
解决:前面提到的Math.min(deltaTime, 3)就是干这个的。另外可以在wx.onShow里重置lastTime:
wx.onShow(() => { lastTime = 0 // 下一帧重新计算 })5.4 常见问题速查表
| 问题 | 可能原因 | 排查方向 |
|---|---|---|
| 蚂蚁不动 | 主循环没启动 / deltaTime为0 | 检查requestAnimationFrame是否调用 |
| 食物捡不起来 | 拾取半径太小 / 碰撞判断用了物理坐标 | 打印蚂蚁和食物距离 |
| 画面模糊 | canvas.width没乘dpr | 检查初始化代码 |
| 分数不增加 | 回巢判断条件写错 | 检查蚂蚁到巢穴的距离阈值 |
| 包体超限 | 图片资源太大 | 改用代码绘制或压缩图片 |
| 真机白屏 | 用了开发者工具支持但真机不支持的API | 查基础库版本兼容性 |
5.5 几个只有踩过才知道的坑
坑一:不要在update里修改数组结构。比如在遍历ants的时候push新蚂蚁,会导致遍历异常。要修改就先收集再统一处理。
坑二:随机数种子。如果要做回放或者调试,Math.random()不可控。可以引入一个简单的伪随机函数,方便复现问题。
坑三:文字渲染性能。ctx.fillText在部分安卓机上很慢,如果每帧都要更新分数文字,建议把文字渲染到离屏Canvas缓存起来,只在分数变化时重绘。
坑四:AI生成的代码要过一遍。AI有时候会用Array.prototype.flat这种在小游戏环境里不一定支持的API,或者用const声明却在后面重新赋值。生成后花两分钟扫一遍,能省很多调试时间。
6. 关于AI辅助游戏开发的几点个人体会
这个项目做完之后,我对“AI能不能做游戏”这个问题有了更具体的答案。AI能极大加速“已知玩法的实现”,但很难“发明新玩法”。蚂蚁搬家的规则是我定的,AI只是把它翻译成了代码。如果让AI从零设计一个游戏,它大概率会给你一个俄罗斯方块或者贪吃蛇的变体,因为训练数据里这些最多。
另一个体会是代码审查能力变得比写代码能力更重要。AI一分钟能生成两百行代码,但里面可能有五处边界问题。你得能快速看出“这里没处理数组越界”“这个循环在极端情况下会死循环”。这种审查能力,恰恰来自你自己手写过足够多的代码。
最后说包体这件事。纯Canvas方案做出来的小游戏,代码压缩后不到100KB,加上几张用代码画的图,整个包体控制在200KB以内。这个数字意味着什么?意味着你可以把省下来的空间全用来做内容,而不是被引擎运行时吃掉。对于独立开发者来说,这是实打实的优势。
后续如果要把这个项目扩展下去,我会考虑加两个方向:一是多蚂蚁协作搬运,两只蚂蚁搬一个大食物,速度更慢但分数更高,增加策略性;二是天气系统,下雨时蚂蚁速度变慢、食物刷新变少,用简单的全局状态影响所有蚂蚁参数。这两个扩展都不需要引入引擎,现有的Canvas架构完全撑得住。