都说 2025 年什么工程问题最难估?Token 账单绝对算一个。标题里那句“使用 Deepseek 花费 49 亿 Token 打造丧尸射击肉鸽”,先不较真是真实数据还是夸张梗,它起码戳中了两件事:第一,大模型辅助开发一个可玩的游戏已经不是幻想;第二,如果流程不规划好,Token 消耗会膨胀到远超想象。这篇文章就围绕这个方向展开:想用 Deepseek 把“丧尸射击 + 肉鸽”做成可运行原型,应该怎么拆需求、怎么选技术栈、代码怎么组织,以及 Token 成本到底怎么控制。
如果你是想用 AI 辅助开发练手的开发者、正在研究肉鸽玩法落地的初学者,或者只是好奇“海量 Token 到底消耗在哪”的人,这篇文章都可以直接收藏。我会按真实开发顺序走:玩法拆解、环境搭建、模块实现、完整代码、运行验证、Token 成本策略、常见报错排查。全程基于 Python 和 Pygame 实现,代码可以直接复制运行。
1. 从 49 亿 Token 说起:这个项目到底在做什么
1.1 “49 亿 Token”是什么概念
先要把 Token 这个概念说清楚。大模型处理文本时,并不是按“字符”或“字节”计数,而是按 Token 计数。Token 是大模型分词之后的最小文本单元,可以粗略理解成“半个词”或者“一段连续字符”。对中文来说,1 个汉字大约对应 0.5 到 1 个 Token,具体与模型的分词器有关;对英文来说,1 个 Token 大约对应 0.7 到 1 个单词。
所以 49 亿 Token 是什么规模?如果按一个 Token 约等于一个汉字来估算,49 亿 Token 大概相当于几十亿个汉字。哪怕打一个对折,这也是几十万本小说的体量。更贴近现实的理解是:你在网页版用大模型聊天,一次长对话可能消耗几千到几万 Token;调用 API 生成一段 100 行代码,输入加上输出通常也要几千 Token。49 亿 Token 意味着极端夸张的对话次数与 API 调用量。
这里不判断标题数字的真伪,但它是一个很好的提醒:用大模型辅助编程,最大成本往往不是“模型不够聪明”,而是“上下文累积 + 反复调试 + 无效对话”带来的 Token 膨胀。当你让 AI 从零生成一个带肉鸽元素的完整游戏,而又没有拆分任务、没有控制上下文时,Token 消耗会比想象中快得多。
1.2 为什么用 AI 辅助开发游戏是可行的
很多新手会觉得“让 AI 写游戏”是很科幻的事,其实技术路线上已经完全可行。像丧尸射击肉鸽这类 2D 原型游戏,核心逻辑并不复杂:
- 玩家移动和射击;
- 丧尸从边缘生成并追踪玩家;
- 子弹与丧尸的碰撞判定;
- 击杀计数、波次递增;
- 死亡或升级时的随机三选一强化。
这些功能拆开来看,每个模块的代码量都不大。用 Deepseek 这类大模型辅助开发时,你可以把需求描述拆成小任务,让模型分别生成类、函数和配置,再自己拼装。Deepseek 对 Python、Pygame 这种常见组合的代码生成能力足够支撑原型开发,而且它还能在你报错的时候尝试解释原因、给出修复建议。
但需要提醒的是:AI 生成代码不等于项目完成。AI 容易在整体架构上出现重复、遗漏或风格不一致,特别是多个模块需要互相协作时,完全靠对话“一次生成完整项目”往往要反复调很多轮。这恰恰也是 Token 消耗的大头。所以这篇文章的代码组织方式,按照“单文件可运行原型 + 模块拆分讲解”来做,既方便大家复制运行,也方便配合 AI 做增量式开发。
1.3 这篇文章能带给你什么
读完这篇文章,你应该能做到三件事:
第一,自己从零搭出一个丧尸射击肉鸽原型,理解它的五个核心模块:玩家、子弹、丧尸、波次、随机升级。第二,学会用 Pygame 实现键盘移动、鼠标射击、碰撞检测、血条绘制和简单 UI。第三,知道在用 Deepseek 或类似大模型辅助开发时,Token 消耗从哪里来,以及如何用流程控制来减少浪费。
代码部分我给的是最小可玩原型,不涉及美术资源和复杂特效,但游戏循环、碰撞、升级逻辑都是完整的。运行起来之后,你会看到一只蓝色小方块角色在黑色地图上移动,丧尸从屏幕四周不断涌出,击杀足够数量后屏幕会弹出三个升级选项。
2. 丧尸射击肉鸽的需求拆解与技术选型
2.1 把玩法拆成五个核心模块
肉鸽(Roguelike)的核心体验是“每局不同,死掉重来”。丧尸射击则是外层的操作玩法。把两者揉到一起后,最小可玩版本至少需要五个模块:
| 模块 | 职责 | 对应代码对象 |
|---|---|---|
| 玩家控制 | WASD 或方向键移动,鼠标瞄准射击 | Player 类 |
| 子弹系统 | 生成、飞行、碰撞、生命周期 | Bullet 类 |
| 丧尸系统 | 从边缘生成、追踪玩家、受击掉血 | Zombie 类 |
| 波次系统 | 随时间增加丧尸数量和强度 | 主循环中的 wave 参数 |
| 肉鸽升级 | 击杀目标数量后随机三选一强化 | UPGRADE_POOL + apply_upgrade 函数 |
这五个模块不要一上来就混在一起写。先用类把对象拆出来,再用一个主循环把它们串起来,是 Pygame 项目最简单直观的组织方式。用 AI 辅助开发时,也建议按这个粒度分步提问,而不是丢给 AI 一句“帮我写个完整肉鸽游戏”。
2.2 为什么选择 Pygame 做原型
很多人会问,为什么不用 Unity、Godot 或者 Cocos?因为主题是“用 Deepseek 辅助开发 + 做可玩原型”,而不是“做一个商业游戏”。
Pygame 的优势在于:
- 安装简单,Python 环境一行 pip 命令即可;
- 没有编辑器依赖,一个
.py文件就能跑起来,方便大模型生成代码; - API 学习成本低,画圆、画方块、检测键盘鼠标、渲染文字都是基础操作;
- 社区例子多,AI 训练数据中 Pygame 相关代码密度高,生成质量稳定。
缺点是它不适合做大型游戏,性能上限和资源管理都比较弱。但对“丧尸射击肉鸽原型”来说,几百行代码就能验证核心玩法,这正好符合快速试错的目标。
如果你更熟悉前端,也可以用 JavaScript + Canvas 实现同样逻辑。但为了后续代码讲解统一,本文使用 Python + Pygame。
2.3 用 Deepseek 辅助编程时应该怎么提问
用大模型辅助开发游戏,提问方式直接决定生成质量和 Token 消耗。我最建议的姿势是“任务拆分 + 上下文最小化”。
简单说,不要在同一个对话里把所有需求一次性丢给模型。下面是一个反面例子:
“帮我用 pygame 写一个丧尸射击肉鸽游戏,包含玩家移动、鼠标射击、丧尸生成追踪、波次系统、升级三选一、血条、死亡重开、音效、粒子特效。”
这种提问方式有两个问题。第一,模型生成的代码往往是“看起来完整但跑不起来”的大杂烩。第二,后续每修一个 bug,你都需要把完整需求重新讲一遍,上下文越长,输入 Token 越多,费用越高。
更合适的提问方式是拆成几个小任务:
“用 pygame 实现一个 Player 类,WASD 控制移动,角色限制在 900x640 窗口范围内,速度为 4,提供 shoot 方法。”
“用 pygame 实现一个 Zombie 类,生成时从屏幕四周随机出现,每一帧朝玩家方向移动,血量、速度由 wave 参数决定。”
每个任务独立生成、独立验证,再放到同一个文件里整合。这样每次对话的上下文短,模型更容易给出可用代码,Token 消耗也更可控。
3. 环境准备:搭建可运行的游戏骨架
3.1 安装 Python 与 Pygame
本文所有代码基于 Python 3 环境。建议使用 Python 3.10 或更高版本,如果本机还没有安装 Python,请先到官网下载对应系统版本,然后打开命令行安装 Pygame:
python -m pip install pygame如果你所在网络环境下载较慢,可以临时使用国内镜像源:
python -m pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,验证版本是否正常:
python -c "import pygame; print(pygame.version.ver)"这里不指定具体 pygame 版本号,因为 2.x 版本的 API 基本一致。只要能看到类似2.5.x的输出,环境就没问题。
3.2 项目目录规划
原型阶段不需要太复杂的工程结构。建议新建一个目录,例如:
zombie-game/ └── game.py整个游戏放在game.py单文件里,方便复制运行。如果你后续想扩展,可以再拆成:
zombie-game/ ├── main.py ├── player.py ├── zombie.py ├── bullet.py ├── upgrades.py └── settings.py但第一版不建议拆太细,因为原型阶段的目标是快速看到游戏跑起来。
3.3 第一版骨架代码:窗口与主循环
先写一个最基础的 Pygame 骨架,确认窗口能打开、主循环能持续刷新。这一步很关键,很多“AI 生成代码跑不起来”的问题,最后都能追溯到主循环结构错误。
# game.py import sys import pygame pygame.init() SCREEN_WIDTH = 900 SCREEN_HEIGHT = 640 FPS = 60 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Zombie Shooter Roguelike") clock = pygame.time.Clock() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() screen.fill((12, 12, 12)) pygame.display.flip() clock.tick(FPS)解释一下每个部分的作用:
pygame.init()初始化所有 Pygame 模块;set_mode创建窗口,900x640 是原型分辨率;clock.tick(FPS)把游戏帧率限制在每秒 60 帧,保证在不同电脑上运行速度一致;pygame.event.get()获取事件队列,处理关闭窗口事件;screen.fill((12, 12, 12))把背景填充成接近黑色。
运行这个文件后,你应该看到一个黑色窗口,右上角有关闭按钮。这证明环境正常。接下来所有代码都可以在这个骨架基础上扩展。
4. 核心玩法实战:配合 Deepseek 一步步完成
现在开始逐步实现完整游戏。我会按照“对象设计 → 核心代码 → 最终完整文件”的顺序讲解。读者可以一边看文章一边自己敲代码,也可以把每个模块的 prompt 复制给 Deepseek 让它生成,然后对照本文修正。
4.1 玩家对象:移动、射击与受伤
玩家是游戏交互的中心。需要具备几个能力:
- 键盘 WASD 或方向键控制移动;
- 鼠标左键向鼠标位置发射子弹;
- 碰到丧尸后受伤,并拥有短暂无敌帧;
- 屏幕边缘限制,防止角色跑出窗口。
类设计如下:
class Player: def __init__(self): self.x = SCREEN_WIDTH // 2 self.y = SCREEN_HEIGHT // 2 self.r = 14 self.speed = 4.0 self.max_hp = 100 self.hp = 100 self.cooldown = 0 # 射击冷却剩余帧数 self.max_cd = 20 # 射击冷却总帧数 self.damage = 25 self.lifesteal = 0 # 击杀回血 self.pierce = 1 # 子弹穿透数 self.invincible_timer = 0 # 受伤后的无敌帧 def update(self, keys): dx = dy = 0 if keys[pygame.K_w] or keys[pygame.K_UP]: dy -= 1 if keys[pygame.K_s] or keys[pygame.K_DOWN]: dy += 1 if keys[pygame.K_a] or keys[pygame.K_LEFT]: dx -= 1 if keys[pygame.K_d] or keys[pygame.K_RIGHT]: dx += 1 if dx != 0 and dy != 0: dx *= 0.7071 dy *= 0.7071 self.x += dx * self.speed self.y += dy * self.speed self.x = max(self.r, min(SCREEN_WIDTH - self.r, self.x)) self.y = max(self.r, min(SCREEN_HEIGHT - self.r, self.y)) if self.cooldown > 0: self.cooldown -= 1 if self.invincible_timer > 0: self.invincible_timer -= 1 def shoot(self, bullets, mx, my): if self.cooldown > 0: return angle = math.atan2(my - self.y, mx - self.x) bullets.append(Bullet(self.x, self.y, angle, self.damage)) self.cooldown = self.max_cd def hurt(self, dmg): if self.invincible_timer > 0: return self.hp -= dmg self.invincible_timer = 40 def heal(self, amount): self.hp = min(self.max_hp, self.hp + amount)几个设计细节值得说明:
- 对角线移动时,如果不处理,角色斜向速度会比水平和垂直方向更快。乘以
0.7071可以把合成速度归一化,这是游戏开发里常见的小细节。 invincible_timer控制无敌帧,玩家被丧尸碰到后不会因为同一次碰撞反复掉血,而是得到约 0.6 秒的喘息时间。cooldown用帧数表示。在 60 FPS 下,max_cd = 20表示每秒最多射击 3 次。后续升级“攻速”时,直接减少max_cd即可。
4.2 子弹:带碰撞检测的飞行体
子弹本身是一个“向外飞行、有限生命”的小球。需要记录位置、方向、伤害和剩余生命:
class Bullet: def __init__(self, x, y, angle, damage): self.x = x self.y = y self.angle = angle self.damage = damage self.speed = 8.0 self.r = 4 self.life = 90 self.hit_count = 0 def update(self): self.x += math.cos(self.angle) * self.speed self.y += math.sin(self.angle) * self.speed self.life -= 1 def draw(self): pygame.draw.circle(screen, YELLOW, (int(self.x), int(self.y)), self.r) def is_dead(self): return self.life <= 0 or self.x < -20 or self.x > SCREEN_WIDTH + 20 or self.y < -20 or self.y > SCREEN_HEIGHT + 20子弹的方向angle通过math.atan2(目标Y - 发射Y, 目标X - 发射X)计算,就是玩家指向鼠标的方向。每一帧沿着cos(angle), sin(angle)方向移动。life = 90表示子弹最多飞行 90 帧,超过后自动消失,避免子弹在屏幕上无限飞行消耗性能。
hit_count在设计上用于支持穿透升级。默认pierce = 1,所以子弹命中一个丧尸后就消失;如果通过升级让pierce = 2,子弹可以继续飞行并命中第二个丧尸。
子弹与丧尸的碰撞判定使用“圆心距离”方式。两圆中心距离小于半径之和,就判定为命中。这种判定方式虽然不精确,但足够支撑原型开发。
4.3 丧尸:从边缘涌向玩家
丧尸是本游戏最核心的敌人。它需要实现“从屏幕边缘随机生成 → 每帧追踪玩家 → 被子弹命中后扣血”的完整行为链:
class Zombie: def __init__(self, wave): self.r = 18 + random.randint(0, 6) self.speed = 1.5 + wave * 0.12 self.max_hp = 50 + wave * 10 self.hp = self.max_hp self.damage = 10 side = random.randint(0, 3) if side == 0: self.x = random.randint(0, SCREEN_WIDTH) self.y = -30 elif side == 1: self.x = random.randint(0, SCREEN_WIDTH) self.y = SCREEN_HEIGHT + 30 elif side == 2: self.x = -30 self.y = random.randint(0, SCREEN_HEIGHT) else: self.x = SCREEN_WIDTH + 30 self.y = random.randint(0, SCREEN_HEIGHT) if random.random() < 0.18: self.r += 5 self.hp *= 2 self.max_hp = self.hp self.damage = 20 def move_towards(self, px, py): angle = math.atan2(py - self.y, px - self.x) self.x += math.cos(angle) * self.speed self.y += math.sin(angle) * self.speed def hit(self, damage): self.hp -= damage def draw(self): color = RED if self.hp > self.max_hp * 0.5 else GREEN pygame.draw.circle(screen, color, (int(self.x), int(self.y)), self.r) pygame.draw.rect(screen, GREY, (int(self.x) - self.r, int(self.y) - self.r - 14, self.r * 2, 6)) ratio = max(0, self.hp / self.max_hp) pygame.draw.rect(screen, RED, (int(self.x) - self.r, int(self.y) - self.r - 14, int(self.r * 2 * ratio), 6))生成方式上,丧尸从屏幕四条边外随机出现,这样玩家需要不断移动和转身,而不是站在一个方向上无脑射击。wave参数决定丧尸的基础速度和血量,波次越高,丧尸越强。
draw方法里画了一个简单的血条。细节实现是先用灰色画底,再按hp / max_hp比例画红色叠加条。这样玩家可以很容易看出某只丧尸还剩下多少血。
我把大型丧尸设计成“出生时 18% 概率出现强化版”,它的体积、血量和伤害都会提升。这种设计不需要额外代码,只在__init__里做判断即可,却能让战斗节奏产生明显变化。
4.4 肉鸽核心:随机三选一升级与波次推进
肉鸽玩法最明显的特征就是“局内成长”。在这个原型里,玩家击杀一定数量丧尸后,游戏暂停,弹出一个三选一升级界面。选择完成后,波次加一,继续战斗。
升级池用字典列表定义,方便扩展:
UPGRADE_POOL = [ {"name": "Enhanced Bullet", "desc": "Damage +15", "effect": "damage"}, {"name": "Rapid Fire", "desc": "Cooldown -20%", "effect": "cooldown"}, {"name": "Swift Boots", "desc": "Speed +20%", "effect": "speed"}, {"name": "Vampiric", "desc": "Heal 3 per kill", "effect": "lifesteal"}, {"name": "Piercing Shot", "desc": "Bullet pierce +1", "effect": "pierce"}, {"name": "Vitality", "desc": "Max HP +20", "effect": "maxhp"}, ] def apply_upgrade(player, option): effect = option["effect"] if effect == "damage": player.damage += 15 elif effect == "cooldown": player.max_cd = max(8, int(player.max_cd * 0.8)) elif effect == "speed": player.speed += 0.8 elif effect == "lifesteal": player.lifesteal += 1 elif effect == "pierce": player.pierce += 1 elif effect == "maxhp": player.max_hp += 20 player.heal(20)为什么用effect字符串而不是直接在每个字典里写函数?因为字典结构方便让 AI 生成和扩展。比如想加一个“冰冻减速”效果,只需要新增一个字典项,再在apply_upgrade里加一个分支,同时修改Zombie类的移动逻辑。
波次推进的逻辑放在主循环里:每击杀一个丧尸,kills加一;当kills >= kill_to_next时,进入升级状态;玩家选择升级后,wave += 1,同时重新计算下一个升级门槛。
4.5 完整 game.py 代码
下面是把所有模块拼接起来的完整代码。我建议直接复制到一个新文件game.py中运行。
# game.py import math import random import sys import pygame pygame.init() SCREEN_WIDTH = 900 SCREEN_HEIGHT = 640 FPS = 60 WHITE = (255, 255, 255) BLACK = (12, 12, 12) RED = (205, 45, 45) GREEN = (60, 200, 90) BLUE = (70, 150, 220) YELLOW = (240, 210, 60) GREY = (90, 90, 90) screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Zombie Shooter Roguelike") clock = pygame.time.Clock() font = pygame.font.SysFont("Arial", 22, bold=True) font_big = pygame.font.SysFont("Arial", 40, bold=True) class Player: def __init__(self): self.x = SCREEN_WIDTH // 2 self.y = SCREEN_HEIGHT // 2 self.r = 14 self.speed = 4.0 self.max_hp = 100 self.hp = 100 self.cooldown = 0 self.max_cd = 20 self.damage = 25 self.lifesteal = 0 self.pierce = 1 self.invincible_timer = 0 def update(self, keys): dx = dy = 0 if keys[pygame.K_w] or keys[pygame.K_UP]: dy -= 1 if keys[pygame.K_s] or keys[pygame.K_DOWN]: dy += 1 if keys[pygame.K_a] or keys[pygame.K_LEFT]: dx -= 1 if keys[pygame.K_d] or keys[pygame.K_RIGHT]: dx += 1 if dx != 0 and dy != 0: dx *= 0.7071 dy *= 0.7071 self.x += dx * self.speed self.y += dy * self.speed self.x = max(self.r, min(SCREEN_WIDTH - self.r, self.x)) self.y = max(self.r, min(SCREEN_HEIGHT - self.r, self.y)) if self.cooldown > 0: self.cooldown -= 1 if self.invincible_timer > 0: self.invincible_timer -= 1 def shoot(self, bullets, mx, my): if self.cooldown > 0: return angle = math.atan2(my - self.y, mx - self.x) bullets.append(Bullet(self.x, self.y, angle, self.damage)) self.cooldown = self.max_cd def hurt(self, dmg): if self.invincible_timer > 0: return self.hp -= dmg self.invincible_timer = 40 def heal(self, amount): self.hp = min(self.max_hp, self.hp + amount) def draw(self): if self.invincible_timer % 6 < 3: pygame.draw.circle(screen, BLUE, (int(self.x), int(self.y)), self.r) else: pygame.draw.circle(screen, WHITE, (int(self.x), int(self.y)), self.r) class Bullet: def __init__(self, x, y, angle, damage): self.x = x self.y = y self.angle = angle self.damage = damage self.speed = 8.0 self.r = 4 self.life = 90 self.hit_count = 0 def update(self): self.x += math.cos(self.angle) * self.speed self.y += math.sin(self.angle) * self.speed self.life -= 1 def draw(self): pygame.draw.circle(screen, YELLOW, (int(self.x), int(self.y)), self.r) def is_dead(self): return self.life <= 0 or self.x < -20 or self.x > SCREEN_WIDTH + 20 or self.y < -20 or self.y > SCREEN_HEIGHT + 20 class Zombie: def __init__(self, wave): self.r = 18 + random.randint(0, 6) self.speed = 1.5 + wave * 0.12 self.max_hp = 50 + wave * 10 self.hp = self.max_hp self.damage = 10 side = random.randint(0, 3) if side == 0: self.x = random.randint(0, SCREEN_WIDTH) self.y = -30 elif side == 1: self.x = random.randint(0, SCREEN_WIDTH) self.y = SCREEN_HEIGHT + 30 elif side == 2: self.x = -30 self.y = random.randint(0, SCREEN_HEIGHT) else: self.x = SCREEN_WIDTH + 30 self.y = random.randint(0, SCREEN_HEIGHT) if random.random() < 0.18: self.r += 5 self.hp *= 2 self.max_hp = self.hp self.damage = 20 def move_towards(self, px, py): angle = math.atan2(py - self.y, px - self.x) self.x += math.cos(angle) * self.speed self.y += math.sin(angle) * self.speed def hit(self, damage): self.hp -= damage def draw(self): color = RED if self.hp > self.max_hp * 0.5 else GREEN pygame.draw.circle(screen, color, (int(self.x), int(self.y)), self.r) pygame.draw.rect(screen, GREY, (int(self.x) - self.r, int(self.y) - self.r - 14, self.r * 2, 6)) ratio = max(0, self.hp / self.max_hp) pygame.draw.rect(screen, RED, (int(self.x) - self.r, int(self.y) - self.r - 14, int(self.r * 2 * ratio), 6)) UPGRADE_POOL = [ {"name": "Enhanced Bullet", "desc": "Damage +15", "effect": "damage"}, {"name": "Rapid Fire", "desc": "Cooldown -20%", "effect": "cooldown"}, {"name": "Swift Boots", "desc": "Speed +20%", "effect": "speed"}, {"name": "Vampiric", "desc": "Heal 3 per kill", "effect": "lifesteal"}, {"name": "Piercing Shot", "desc": "Bullet pierce +1", "effect": "pierce"}, {"name": "Vitality", "desc": "Max HP +20", "effect": "maxhp"}, ] def apply_upgrade(player, option): effect = option["effect"] if effect == "damage": player.damage += 15 elif effect == "cooldown": player.max_cd = max(8, int(player.max_cd * 0.8)) elif effect == "speed": player.speed += 0.8 elif effect == "lifesteal": player.lifesteal += 1 elif effect == "pierce": player.pierce += 1 elif effect == "maxhp": player.max_hp += 20 player.heal(20) def reset_game(): player = Player() bullets = [] zombies = [] return player, bullets, zombies def run_game(): player, bullets, zombies = reset_game() wave = 1 kills = 0 kill_to_next = 6 + wave * 3 spawn_timer = 0 game_over = False waiting_upgrade = False upgrade_list = [] while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if game_over and event.key == pygame.K_r: player, bullets, zombies = reset_game() wave = 1 kills = 0 kill_to_next = 6 + wave * 3 spawn_timer = 0 game_over = False waiting_upgrade = False if game_over and event.key == pygame.K_q: pygame.quit() sys.exit() if waiting_upgrade: selected = None if event.key == pygame.K_1 and len(upgrade_list) >= 1: selected = upgrade_list[0] elif event.key == pygame.K_2 and len(upgrade_list) >= 2: selected = upgrade_list[1] elif event.key == pygame.K_3 and len(upgrade_list) >= 3: selected = upgrade_list[2] if selected: apply_upgrade(player, selected) waiting_upgrade = False wave += 1 kill_to_next = kills + 6 + wave * 3 if not game_over and not waiting_upgrade: keys = pygame.key.get_pressed() player.update(keys) mouse_buttons = pygame.mouse.get_pressed() if mouse_buttons[0]: mx, my = pygame.mouse.get_pos() player.shoot(bullets, mx, my) spawn_timer += 1 spawn_interval = max(18, 55 - wave * 5) if spawn_timer >= spawn_interval: spawn_timer = 0 zombies.append(Zombie(wave)) if wave >= 3: zombies.append(Zombie(wave)) if wave >= 6 and random.random() < 0.4: zombies.append(Zombie(wave)) for b in bullets: b.update() for z in zombies: z.move_towards(player.x, player.y) for b in bullets: for z in zombies: if z.hp > 0 and math.hypot(b.x - z.x, b.y - z.y) < b.r + z.r: z.hit(b.damage) b.hit_count += 1 if b.hit_count >= player.pierce: b.life = 0 break bullets = [b for b in bullets if not b.is_dead()] for z in zombies: if z.hp > 0 and math.hypot(z.x - player.x, z.y - player.y) < z.r + player.r: player.hurt(z.damage) for z in zombies[:]: if z.hp <= 0: zombies.remove(z) kills += 1 if player.lifesteal > 0: player.heal(player.lifesteal * 3) if kills >= kill_to_next: waiting_upgrade = True upgrade_list = random.sample(UPGRADE_POOL, 3) if player.hp <= 0: game_over = True screen.fill(BLACK) for b in bullets: b.draw() for z in zombies: z.draw() if not game_over: player.draw() hp_text = font.render(f"HP: {player.hp}/{player.max_hp}", True, WHITE) info_text = font.render(f"Kills: {kills} Wave: {wave} Next Upgrade: {max(0, kill_to_next - kills)}", True, WHITE) screen.blit(hp_text, (15, 10)) screen.blit(info_text, (15, 38)) if waiting_upgrade: overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 150)) screen.blit(overlay, (0, 0)) title_text = font_big.render("Choose Upgrade", True, YELLOW) screen.blit(title_text, (SCREEN_WIDTH // 2 - title_text.get_width() // 2, 130)) y = 220 for i, option in enumerate(upgrade_list, start=1): option_text = font.render(f"{i}. {option['name']} - {option['desc']}", True, WHITE) screen.blit(option_text, (SCREEN_WIDTH // 2 - 260, y)) y += 50 if game_over: overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) screen.blit(overlay, (0, 0)) over_text = font_big.render("GAME OVER", True, RED) sub_text = font.render("Press R to restart / Q to quit", True, WHITE) screen.blit(over_text, (SCREEN_WIDTH // 2 - over_text.get_width() // 2, 200)) screen.blit(sub_text, (SCREEN_WIDTH // 2 - sub_text.get_width() // 2, 270)) pygame.display.flip() clock.tick(FPS) if __name__ == "__main__": run_game()这段代码中的几个关键逻辑,再强调一下:
- 当
waiting_upgrade和game_over都为False时,才会更新玩家、子弹和丧尸,这是“暂停”效果的实现方式; - 丧尸碰到玩家并不会消失,玩家依靠无敌帧机制避免连续扣血;
- 升级选择完成后,
wave加一,并重新计算kill_to_next,从而形成“杀怪越多,怪越强,要求击杀数也在增加”的循环; - 如果玩家血量降到 0,
game_over变为True,游戏进入结束界面。
5. 运行效果与验证
5.1 如何运行
进入项目目录,执行:
python game.py如果代码没有问题,你会看到窗口打开,玩家角色出现在屏幕中央,丧尸从四周不断涌入。通过鼠标左键射击,击杀足够数量后屏幕弹出升级选择界面。
5.2 预期表现
从玩家视角看,这局游戏是这样的:
- 第一波开始,丧尸生成间隔较长,击杀 9 只后触发第一次升级;
- 三选一界面弹出,按数字键 1、2、3 选择升级;
- 选择完成后,波次进入第 2 波,丧尸血量、速度小幅提升;
- 随着波次提高,屏幕上的丧尸密度明显增加;
- 被丧尸碰到会扣血,角色闪白表示进入无敌帧;
- 血量归零后,游戏结束,按 R 重新开始。
5.3 升级项与手感调整
原型最有趣的部分是调整数值平衡。比如你会发现,如果不选“穿透子弹”,后期丧尸密集时子弹输出不够;如果不选“吸血”,被大丧尸群贴脸后很难恢复。这些都是可以继续迭代的方向。
如果觉得难度太低或太高,可以调整这几个参数:
spawn_interval:丧尸生成间隔,越小越难;kill_to_next:升级门槛,越大升级越慢;Zombie.__init__中的wave * 0.12和wave * 10:决定每波丧尸的速度和血量成长;player.max_cd:初始射击冷却,越小射击越频繁。
原型阶段不需要追求完美平衡,关键是先让玩法闭环跑起来。
6. Token 消耗与成本控制经验
回到标题里那 49 亿 Token。如果真的有人用 Deepseek 开发游戏烧掉这么多 Token,那大概率不是“AI 太笨”,而是流程里出现了大量无效对话和上下文累积。这一节就把 Token 消耗的主要来源和控制策略讲清楚。
6.1 Token 到底消耗在哪里
大模型 API 的计费单位是 Token。API 调用时,费用一般由两部分组成:输入 Token 和输出 Token。输入 Token 指你发给模型的系统提示词、历史消息、用户问题;输出 Token 指模型生成的内容。
用 Deepseek 辅助开发游戏时,Token 消耗主要来自四个场景:
第一,需求描述。每一次让 AI 生成代码,都需要用自然语言描述需求。描述越详细,输入 Token 越多。第二,代码产出。模型生成的代码长度就是输出 Token 数。生成 300 行代码,可能消耗几千到上万 Token。第三,上下文累积。很多人在同一个对话里连续提问,每次请求都会把之前所有内容再次发送给模型。对话越长,每次请求的输入 Token 就越贵。第四,错误尝试。AI 生成代码后跑出报错,你把报错贴回去,AI 改一版,又跑出新的报错。反复几轮之后,消耗就会快速膨胀。
6.2 49 亿 Token 背后的效率警示
49 亿 Token 即便是夸张表达,背后传递的信息也很有价值:大模型辅助开发不是免费的,也不是无限便宜的。如果你把整个项目都交给 AI,逐段生成、逐段纠错,却不清理对话上下文,Token 消耗会像滚雪球一样增长。
更常见的问题是“代际浪费”。比如 AI 半小时内生成了 5 个版本的 Player 类,但最终只用了其中 1 个版本。前面 4 个版本的生成与调试消耗,都变成了沉默成本。这个案例提醒我们:用 AI 写游戏代码,最大的成本不是“生成代码”,而是“围绕代码进行的无效对话”。
6.3 控制 Token 成本的七个策略
要控制费用,不是少用 AI,而是用得更聪明。下面这七个策略经过实际项目检验,值得直接套用:
- 任务拆分:把游戏的每个模块拆成独立小任务,每次只让 AI 生成一个类或一个函数。
- 新开对话:每完成一个模块,就新开一个对话。不要在同一个对话里既做玩家模块又做丧尸模块。
- 精简上下文:不要把无关代码反复贴给 AI。贴代码片段时,尽量只贴出错的部分。
- 先写注释再写代码:用中文注释描述函数行为,让 AI 基于注释生成实现,可以减少来回说明。
- 本地保存 AI 代码:AI 每次生成的代码都保存成文件,避免下次重新生成同样内容。
- 控制调试轮次:AI 给出的修复方案如果连续两轮仍无法运行,建议自己动手排查,而不是继续追问。
- 关注官方用量统计:Deepseek 等平台通常有 token 用量和费用统计,开发时定期查看。
如果把这些策略落实到位,开发一个类似本文规模的游戏原型,实际消耗通常会在可控范围内,而不是动辄“亿级 Token”。
6.4 Deepseek API 调用与常见 Token 报错
有些读者不只是用网页版 Deepseek,还想通过 API 调用把模型接入自己的工具。下面是一个调用 Deepseek API 的示例思路,具体参数以官方文档为准:
import os from openai import OpenAI client = OpenAI( api_key=os.environ.get("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com", ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是游戏开发助手"}, {"role": "user", "content": "用 pygame 实现一个 Player 类的移动逻辑"}, ] ) print(resp.choices[0].message.content)这段代码的核心要点:
- API Key 从环境变量读取,不要硬编码在源码里;
base_url指向 Deepseek API 的官方地址,具体以最新文档为准;- 返回结果通过
resp.choices[0].message.content获取; - 不同版本的 SDK 可能有细微差异,请先看官方示例。
使用 API 过程中,可能遇到各种与 Token 相关的报错。比如提示 “sign-in could not be completed token exchange failed”“token endpoint returned” 或 “failed to refresh token” 等。这类报错本质上都属于“认证/刷新令牌失败”类问题,排查时先按下面几个方向检查:
- API Key 是否有效,是否过期;
- 请求头里 Authorization 是否正确携带;
- refresh_token 是否为空或已失效;
- 当前地区、网络环境是否在服务商的支持范围内;
- SDK 版本是否过旧,或者 base_url 配置错误。
- 涉及地区限制时,请检查官方文档的服务范围说明,不要使用任何非官方工具绕过限制。
这些排查方向不仅适用于 Deepseek,也适用于大多数 OpenAI 兼容接口。重要的是先确认“Token 失效”到底出在哪个环节:是登录认证失败、API Key 失效,还是刷新令牌逻辑有漏洞。
7. 常见问题排查清单
开发这个原型的过程中,你可能会遇到下面这些问题。我整理成一张排查表,方便遇到报错时快速定位。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| pygame 安装失败 | Python 版本不匹配或网络问题 | 检查 Python 版本,升级 pip,使用镜像源重试 |
| 窗口打开后黑屏 | 主循环没有调用 display.flip | 检查主循环末尾是否有pygame.display.flip() |
| WASD 无法移动素材 | 角色被绘制但 update 未执行 | 确认主循环中调用了player.update(keys) |
| 鼠标点击没反应 | 事件处理早于射击逻辑 | 在每一帧通过pygame.mouse.get_pressed()检测射击 |
| 丧尸生成过快 | spawn_interval 太小 | 调大 spawn_interval,或降低 wave 增长速度 |
| 子弹穿不过丧尸 | 子弹命中后直接消失 | 检查b.hit_count >= player.pierce的逻辑 |
| 弹出升级界面时游戏还在运行 | 没有暂停更新逻辑 | 用if not waiting_upgrade:包裹更新代码 |
| GAME OVER 后按 R 无效 | 重置状态不完整 | 检查重置时是否恢复 wave、kills、kill_to_next 等变量 |
| API 报 token exchange failed | 认证 Token 无效或过期 | 重新获取 Token,检查 Authorization 请求头 |
| API 报 refresh_token 为空 | 刷新令牌逻辑未保存 | 检查令牌存储和刷新流程,确保传入非空 refresh_token |
| 调用 Deepseek API 报 403 | 地区/权限限制 | 查看官方文档确认服务范围,使用合法的 API 方式 |
7.1 为什么“游戏能跑但不能玩”
有一个非常常见的情况是:代码完全运行,但丧尸一多游戏就卡顿,或者玩家角色几乎瞬间被包围。这通常不是代码逻辑错误,而是数值平衡问题。
建议从两个方面调整:
- 降低
Zombie的初始速度和生成数量; - 提高玩家初始伤害或射击频率。
原型阶段的目标是“能玩”,不是“一步到位好玩”。把数值参数提取成常量后,多跑几局找感觉即可。
7.2 为什么 Python 运行时窗口突然无响应
这通常是因为主循环里的某个循环卡住。常见原因包括:在事件循环中写了while True死循环、碰撞检测中大量遍历导致帧率过低、或者在重置游戏时没有清空bullets和zombies。
排查思路:先在主循环里加打印日志,看卡在哪个阶段;再检查是否出现了 O(n^2) 的碰撞检测。原型阶段丧尸数量控制在几十只以内,性能不会有问题。
8. 最佳实践与扩展建议
8.1 让 AI 辅助开发保持可控
用 Deepseek 或类似模型写代码,最怕的不是生成质量差,而是“失控感”越来越强:代码不是自己写的,结构不熟悉,改不动。要让 AI 辅助开发真正可控,可以遵循几个工程原则。
第一,AI 生成代码后,自己先读一遍再运行。不要直接当成正确答案。第二,每个模块生成后,先做最小验证。比如 Player 类写完,先跑一下移动逻辑;Zombie 类写完,先看它会不会追踪玩家。第三,保持类与函数职责单一。AI 很容易把多个功能揉进一个函数,这时需要手动拆分。第四,把关键参数集中管理。比如丧尸速度、血量成长、射击冷却都放到常量或字典里,方便后续平衡调整。
从项目工程角度看,建议每个阶段用 Git 提交一次。AI 生成的结果如果“把代码改坏了”,可以快速回退,不需要浪费 Token 重新生成。
8.2 后续可以继续扩展的方向
这个原型只是起点。如果你想继续在丧尸射击肉鸽方向深入,可以按优先级选择以下扩展方向:
- 内容扩展:增加多种丧尸类型、枪械、技能、地图障碍物;
- 系统扩展:完成“死亡后永久解锁”的局外成长系统;
- 表现扩展:增加屏幕震动、特效粒子、音效和背景音乐;
- 数值扩展:把升级池从 6 个扩展到 30 个,引入稀有度分级;
- 工程扩展:把单文件拆成多文件,补充配置文件和数据表;
- 发布扩展:打包成可执行文件,发布到 itch.io 或 Steam 验证创意。
建议优先做“内容扩展”和“系统扩展”,因为它们对玩法的改变最直接。特效和音效放在玩法稳定之后再做,避免过早陷入视觉优化而忽略核心循环。
8.3 给想复刻这个项目的人一句话
如果看完这篇文章也想自己试一次,先别急着把三选一、波次、穿透和吸血都做进去。第一步只做玩家移动和一颗子弹;第二步让一只丧尸朝你走来;第三步让子弹能把它打死。只要把这个最小循环跑通,后面加任何肉鸽元素都快得多。
至于 Token,记得给每个阶段性任务单独开一个对话,不要让上一轮的长篇聊天记录把下一轮开发塞满。这个习惯,比任何“省 Token 技巧”都有效。