简介:飞机大战代码和对应的图片素材包.rar面向Python游戏开发初学者及需要教学案例的开发者,提供同一款空战小游戏的两套实现:面向过程版与面向对象简化版,便于对照理解函数组织、游戏主循环、事件响应、碰撞检测,以及类、对象、继承与多态在项目中的落地方式。压缩包共77个文件,以62张png和9个gif图片素材为主,另含2个py源码与少量jpg、db等辅助文件,整体约1.95MB;图片覆盖玩家与敌机造型、子弹、背景、爆炸及界面按钮等,可直接配合代码完成画面渲染。已有2577人学习,适合通过双版本源码与现成素材搭建可运行示例,并在对比中理解两种编程范式的差异与适用场景。
1. 从一份压缩包开始:飞机大战能当第一个“完整项目”练手的原因
带新人入门游戏开发,我一般不建议从贪吃蛇起步,而是丢一份飞机大战的代码和配套素材包过去,理由很实际:这个题材同时踩到了游戏开发里最核心的几件事——实时主循环、精灵与碰撞、逐帧动画、音效播放、状态切换、难度曲线,但每一项的量都不大,一个新人在一周内能全部摸一遍。这份资源把代码和图片素材打包在一起,最省事的地方在于不用自己去找美术资源,拿到就能跑出一个有背景滚动、有敌机、有子弹、有爆炸效果的完整画面,先看到结果,再回头拆实现。适合谁:刚学完 Python 语法想找个能落地的东西练手的、要给学生布置课程设计的、想拿一个现成骨架改出自己小游戏的。下面按“先跑通、再拆核心、最后改造”的顺序走一遍。
2. 资源结构与技术选型:解压后每个目录该放在哪
2.1 目录骨架与文件职责
解压后如果看到的是一堆平铺的文件,第一件事不是急着双击运行,而是先把结构认清楚。这类飞机大战项目常见的组织方式是按“引擎层 / 业务层 / 资源层 / 配置层”四分,main.py只负责启动,真正的逻辑拆到game/下,图片声音全塞进res/,所有可调参数集中放在conf/settings.py。结构大概长下面这样:
airplane-war/ ├── main.py # 入口,只做初始化和主循环 ├── game/ │ ├── __init__.py │ ├── scene.py # 场景与状态机(开始/游戏中/暂停/结束) │ ├── plane.py # 玩家飞机 │ ├── enemy.py # 敌机,含小/中/大三种 │ ├── bullet.py # 子弹 │ ├── explosion.py # 爆炸帧动画 │ └── background.py # 背景无缝滚动 ├── res/ │ ├── image/ # hero1.png、enemy1.png、bullet.png、background.png │ ├── sound/ # 射击、爆炸、背景音乐 │ └── font/ # 分数显示的字体 ├── conf/ │ └── settings.py # 窗口尺寸、速度、生成间隔全在这 └── requirements.txt这个分法不是强迫症,是有实际收益的:调手感的时候你只需要动settings.py,换素材的时候只动res/image,加玩法的时候只动game/,三条改动的路径互不干扰。我见过不少同学把所有东西写在一个 800 行的main.py里,跑到能玩没问题,但一旦要改“敌机多刷一点”就得在几百行里翻常量,这就属于给自己挖坑。如果解压出来的结构和我说的不一样,比如图片直接堆在根目录、文件名是1.png 2.png这种,那就先花十分钟重命名并挪进res/image,这个整理动作后面会反复回报你。
还有一类情况是素材包和代码包是分开的:图片放在images/,代码里却写死了res/image/hero1.png的路径。碰到路径对不上,别急着改代码里的每一处字符串,先把素材按代码期望的目录结构摆好,代码不用动一行。这是血泪经验——改代码路径容易漏改,改目录一次到位。
2.2 为什么这类项目几乎都用 pygame
飞机大战这种 2D 竖版射击,技术选型上有几个常见答案:pygame、pygame-zero、arcade、HTML5 Canvas、Unity 2D。这份资源既然是 Python 代码加图片素材,那基本就是 pygame 路线,这是最合理的选择,原因在于它的抽象层级刚好卡在这个题材需要的位置上。
pygame 提供了四样东西:Surface表示一张可绘制的图、Rect表示一个矩形区域、Sprite和Group表示“有图有位置的对象”和“一堆这样的对象”、Clock表示帧率控制。飞机大战需要的正是这四样,不多不少。你不需要理解渲染管线,也不需要写着色器,只要每帧把图片 blit 到屏幕上的指定坐标,画面就出来了。相比之下,直接用 OpenGL 或现代图形库属于杀鸡用牛刀,而用 Canvas 写 JS 版本也行,但素材加载、音频播放这些要自己处理浏览器兼容,新手容易在前置环节就卡住。
pygame 2 系列相比早期版本在高分屏和音频设备兼容上有明显改善,装的时候直接pip install pygame拉到的基本就是 2.x,Windows、macOS、常见 Linux 发行版都有预编译轮子,不需要本地编译,这对新手很关键。至于 pygame-zero,它把主循环都藏起来了,代码确实更短,但恰恰把“主循环怎么转”这个最该学的东西屏蔽掉了,练手价值打折。
有一点要提前说清楚:pygame 的坐标系是左上角为原点、y 轴向下为正,这和数学课本里的坐标系是反的。玩家飞机往上飞,rect.y是减小的。这个反转在写“子弹飞出屏幕就销毁”这类判断时特别容易写反,一开始就把它记住,能省掉不少调试时间。
2.3 素材规格:尺寸、透明通道与帧动画命名
素材包能不能直接用,取决于三件事:图片尺寸对不对、有没有透明通道、帧动画是否按序号命名。
先说尺寸。竖版飞机大战的窗口常见是 480×700 或 480×852 这类手机比例,玩家飞机一般在 60×80 到 100×120 像素之间,敌机按大中小分三档,小的 30×30 左右,大的能到 150×200。如果你的素材尺寸和代码里的窗口不匹配,画面会显得飞机巨大或者小得看不见,这时候要么改窗口尺寸,要么在加载时统一缩放。我一般倾向后者,因为缩放是运行时可调的,改窗口尺寸会连带影响背景滚动的循环点。
再说透明通道。飞机这种不规则形状的图,必须是带 alpha 通道的 PNG。如果素材包里混了 JPG,导入之后边缘会带一圈白底,飞机飞在深色星空背景上就像贴了张白纸,非常出戏。这种情况只能重新导出 PNG,没法靠代码补救。加载时统一调用convert_alpha(),这是把图片转成和屏幕一致的像素格式并保留透明通道,能显著提升 blit 性能,不做这一步在大分辨率下会明显掉帧。
帧动画的命名约定也很关键。玩家飞机被击中后的爆炸效果,通常是explode1.png到explode8.png这样一串,游戏里按时间顺序播放。代码如果按f"explode{i}.png"拼接路径,那文件名就必须严丝合缝,编号从 1 开始还是从 0 开始、有没有补零(explode01还是explode1),差一点就是FileNotFoundError。我通常写一个生成器函数一次性把整串图读进列表,而不是每帧去拼路径读文件——磁盘 IO 每帧做一次,卡顿是必然的。
import os import pygame def load_frames(folder, prefix, count, scale=None, start=1): """按前缀批量加载帧动画图片,返回 Surface 列表 folder: 图片所在目录 prefix: 文件名前缀,如 'explode' count: 总帧数 scale: 可选的 (w, h) 统一缩放尺寸 start: 起始编号,很多素材从 1 开始 """ frames = [] for i in range(start, start + count): path = os.path.join(folder, f"{prefix}{i}.png") img = pygame.image.load(path).convert_alpha() # 保留透明通道 if scale: img = pygame.transform.smoothscale(img, scale) # 平滑缩放,别用 scale frames.append(img) return frames # 用法:8 帧爆炸动画,统一缩放到 80x80 EXPLODE_FRAMES = load_frames("res/image", "explode", 8, scale=(80, 80))这段代码的关键在两点。convert_alpha()必须在pygame.display.set_mode()之后调用,否则会因为显示表面还没创建而报错,很多人第一次写就栽在这。smoothscale和scale的区别是前者做插值、边缘更柔和,代价是稍慢,用在加载阶段完全无所谓;后者是最近邻,缩小后锯齿明显。另外count参数最好别写死 8,而是在加载前用os.listdir数一下实际有多少帧,素材包换一套的时候就不用改代码。
3. 核心循环拆解:一帧之内到底发生了什么
3.1 主循环骨架:事件、更新、绘制三段式
整个游戏就是一个while循环,每秒转 60 圈,每圈做四件事:算时间、收输入、改状态、画画面。这个顺序不能乱,尤其是“更新”必须在“绘制”之前,否则你画出来的是上一帧的状态,画面会滞后一帧,手感上就是“按了键飞机愣一下才动”。
import pygame from conf.settings import SCREEN_W, SCREEN_H, FPS from game.scene import Scene def main(): pygame.init() pygame.mixer.init() # 音频单独初始化 screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) pygame.display.set_caption("飞机大战") clock = pygame.time.Clock() scene = Scene(screen) # 场景持有全部精灵 running = True while running: dt = clock.tick(FPS) / 1000.0 # 返回毫秒,转成秒 for event in pygame.event.get(): # 只采集,不写业务 if event.type == pygame.QUIT: running = False scene.handle_event(event) scene.update(dt) # 位置、计时、生成全在这 screen.fill((0, 0, 0)) # 先清屏,否则残影 scene.draw(screen) pygame.display.flip() # 双缓冲交换 pygame.quit() if __name__ == "__main__": main()参数上,FPS取 60 是标准答案,取 30 会明显顿挫,取 120 在普通显示器上没有收益还多耗电。clock.tick(FPS)的作用不只是限速,它返回的是“距上一次调用过了多少毫秒”,这个值就是dt。之所以要用dt而不是“每帧固定移动 5 像素”,是因为不同机器实际帧率会有波动,固定步长会导致快机器上飞机飞得飞快、慢机器上像在水里游。所有速度参数都应该写成“每秒多少像素”,乘dt之后才是这一帧的位移。
screen.fill()这句绝对不能省。pygame 的绘制是往同一个表面上叠,不清屏的话飞机后面会拖出一条残影长龙。有人觉得残影挺好看,但那是特效能做出来的东西,不是靠漏清屏。pygame.display.flip()是双缓冲的交换操作,没有它你什么都看不到。
3.2 精灵与分组:碰撞检测靠 Group 而不是手写矩形判断
新手最容易写出来的碰撞检测是一堆if:玩家和每颗子弹比一下、和每个敌机比一下。敌机 20 个、子弹 30 颗,那就是 600 次比较,代码又臭又长。pygame 的Sprite+Group就是为这事设计的:所有能碰撞的东西继承Sprite,装进Group,碰撞检测一行搞定。
import pygame class Hero(pygame.sprite.Sprite): def __init__(self, image, center): super().__init__() self.image = image self.rect = self.image.get_rect(center=center) self.mask = pygame.mask.from_surface(self.image) # 像素级碰撞掩码 self.speed = 320 # 像素/秒 self.hp = 3 self.invincible_until = 0 # 无敌帧到期时间戳 def update(self, dt, keys): dx = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed * dt dy = (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * self.speed * dt if dx and dy: # 斜向归一化,否则快 1.41 倍 dx *= 0.7071 dy *= 0.7071 self.rect.x = max(0, min(self.rect.x + dx, SCREEN_W - self.rect.width)) self.rect.y = max(0, min(self.rect.y + dy, SCREEN_H - self.rect.height)) def hit(self, now): if now < self.invincible_until: # 还在无敌时间,忽略 return False self.hp -= 1 self.invincible_until = now + 1000 # 受伤后 1 秒无敌 return Truemask那行是做像素级碰撞的基础,pygame.sprite.collide_mask会用它判断两张图真正重叠的像素,比矩形框精确得多——飞机机翼是尖的,矩形框碰撞会出现“明明没碰到却掉血”的玄学现象,玩家会很恼火。代价是 CPU 开销比矩形碰撞高,敌机数量上百的时候要权衡,一般这个量级完全够用。
移动那段里的 0.7071 是math.sqrt(2)/2,用来做斜向归一化。不写这个的话,同时按右和下,实际速度是单方向的 1.41 倍,玩家会发现斜着飞比直着飞快,手感很怪。边界限制用的是max/min夹取,比写if判断简洁,注意SCREEN_W - self.rect.width是因为rect.x是左上角坐标,不是中心。
碰撞检测本身只需要一行:
# 玩家与敌机群碰撞,dokill=False 因为我们还要播爆炸动画 hits = pygame.sprite.spritecollide(hero, enemy_group, False, pygame.sprite.collide_mask) for enemy in hits: if hero.hit(now): enemy.explode() # 触发敌机爆炸 if hero.hp <= 0: scene.set_state("game_over") # 子弹与敌机群碰撞,子弹撞到就消失,所以 dokill=True killed = pygame.sprite.groupcollide(bullet_group, enemy_group, True, False, pygame.sprite.collide_mask) for bullet, enemies in killed.items(): for e in enemies: e.hp -= 1 if e.hp <= 0: e.explode()spritecollide的第三个参数是“被撞的一方是否销毁”,groupcollide的前两个布尔参数分别是“第一组是否销毁”“第二组是否销毁”。这里子弹销毁、敌机不立即销毁(因为要播完爆炸动画再移除),所以是True, False。这个参数顺序搞反是很常见的翻车点,症状是敌机一碰就没、子弹还留在原地继续飞。
3.3 射击冷却:用时间戳而不是 sleep
“按住空格连续射击”如果写成time.sleep(0.1),整个游戏会卡住,因为主循环是单线程的,sleep 期间画面不刷新、输入不响应。正确做法是记录“下一次允许开火的时间点”,每帧比较当前时间。
import pygame class Weapon: def __init__(self, interval_ms=180, damage=1): self.interval = interval_ms # 射击间隔,越小射速越快 self.next_fire = 0 # 允许开火的下一时刻 self.damage = damage def try_fire(self, group, center): now = pygame.time.get_ticks() # 毫秒,从 pygame.init 起算 if now < self.next_fire: return self.next_fire = now + self.interval group.add(Bullet(center, self.damage)) # 主循环里 if keys[pygame.K_SPACE]: hero.weapon.try_fire(bullet_group, hero.rect.midtop)pygame.time.get_ticks()是自pygame.init()起经过的毫秒数,单调递增、不受系统时间调整影响,比time.time()适合做游戏计时。interval_ms是手感的核心参数之一,180ms 大约相当于每秒 5.5 发,是“能看清弹道又不过于稀疏”的区间,改成 100 就是弹幕流,改成 400 会显得很呆。这个值放在settings.py里,调手感的时候只改它一个数就行。
4. 敌机生成与难度曲线:随机刷怪背后的权重表
4.1 生成节奏:用权重表代替纯随机
纯random.choice挑敌机类型,玩十分钟你就会发现“怎么老是刷小飞机、大飞机几乎不来”。因为等概率分布下,稀有但强力的敌人出现频率太高或者太低都不符合设计预期,正确做法是给每种敌机一个权重,再按权重抽样。同时难度要随分数上升——生成间隔缩短、大飞机权重提高。
import random # 每种敌机的权重与属性,权重越大越容易出现 ENEMY_TABLE = [ {"name": "small", "weight": 60, "hp": 1, "score": 100, "speed": (120, 200)}, {"name": "medium", "weight": 30, "hp": 3, "score": 300, "speed": (80, 140)}, {"name": "large", "weight": 10, "hp": 8, "score": 800, "speed": (60, 90)}, ] def pick_enemy(score): """按分数动态调整权重:分数越高,大飞机占比越大""" boost = min(score // 3000, 30) # 每 3000 分加一档,封顶 30 weights = [e["weight"] for e in ENEMY_TABLE] weights[2] += boost # 只抬大飞机的权重 weights[0] = max(20, weights[0] - boost // 2) # 小飞机相应减少 return random.choices(ENEMY_TABLE, weights=weights, k=1)[0] def spawn_interval(score): """生成间隔随分数从 800ms 收缩到 260ms,有下限防止卡死""" return max(260, 800 - score // 20)random.choices的第三个参数k=1是取一个样本,返回的是列表,所以外面取[0]。boost用min(..., 30)封顶,是为了防止玩到几万分之后大飞机权重无限增长、变成满屏都是血厚的大飞机,节奏反而崩掉。spawn_interval加了max(260, ...)的下限,同理——没有下限的话,后期敌机一秒刷十架,玩家必死,游戏就变成“分数越高死得越快”的劝退机制。
生成位置的随机也有讲究。横坐标应该在一个安全区内随机,比如距离左右边缘各留 40 像素,否则敌机会贴边刷出来,玩家看到的时候已经快撞上了,这是纯粹的不可躲避。纵坐标固定为-rect.height(屏幕上方外侧),让它慢慢飞进来,而不是凭空出现在画面中间。
4.2 伤害结算与爆炸动画:别让对象在遍历中消失
爆炸动画是帧动画的典型应用:敌机死亡时,从精灵组里移除敌机本身,换成一个爆炸精灵,播放 8 帧后自己kill()掉自己。
class Explosion(pygame.sprite.Sprite): def __init__(self, center, frames, fps=24): super().__init__() self.frames = frames self.image = frames[0] self.rect = self.image.get_rect(center=center) self.frame_ms = 1000 // fps self.start = pygame.time.get_ticks() def update(self, dt): elapsed = pygame.time.get_ticks() - self.start idx = elapsed // self.frame_ms if idx >= len(self.frames): # 播完就自我销毁 self.kill() return self.image = self.frames[idx] def on_enemy_dead(enemy, explosion_group, explode_frames): explosion_group.add(Explosion(enemy.rect.center, explode_frames)) enemy.kill() # 一定要 kill,否则幽灵对象还在碰撞关键在最后那个enemy.kill()。kill()会把精灵从它所属的所有Group里移除,这样下一帧的碰撞检测就碰不到它了。如果只播放爆炸动画而忘记 kill,会出现非常诡异的现象:敌机已经看不见了,但玩家的子弹打到那个位置还是会掉血、还是能刷分——这就是所谓的“幽灵对象”。这个坑我踩过,排查了半天才意识到是对象没从组里摘掉。
爆炸帧率fps=24意味着每帧约 42ms,8 帧播完约 330ms,这个长度正好能让玩家感知到“打爆了”又不会拖慢节奏。如果素材帧数不一样,frames列表长度会自动适配,不用改代码。
4.3 状态机:把“开始画面、游戏中、暂停、结束”分开
初学写法常见的问题是所有状态揉在一个循环里,用一堆布尔标志位if is_start and not is_pause and not is_over,加第三个状态就开始组合爆炸。干净的做法是一个状态机,每个状态有自己的 update 和 draw。
class Scene: def __init__(self, screen): self.screen = screen self.state = "ready" # ready / playing / paused / game_over self.score = 0 def set_state(self, s): self.state = s def handle_event(self, event): if event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN and self.state in ("ready", "game_over"): self.reset() self.state = "playing" elif event.key == pygame.K_ESCAPE and self.state == "playing": self.state = "paused" elif event.key == pygame.K_ESCAPE and self.state == "paused": self.state = "playing" def update(self, dt): if self.state != "playing": # 非游戏中状态不推进行逻辑 return # ...状态机的价值在于“暂停”和“结束”不是靠跳过某几行代码实现的,而是整段逻辑不执行。用布尔标志位的写法,暂停之后计时器还在跑、敌机还在生成,恢复的瞬间会涌出一堆,这个 bug 的表现是“暂停十分钟回来一秒就死了”。
5. 避坑与排查:跑不起来和跑起来不对的几类现场
5.1 环境与启动阶段
现象一:ModuleNotFoundError: No module named 'pygame',但明明装过。原因十有八九是装了不止一个 Python——系统自带一个、IDE 里配的虚拟环境一个、命令行pip又指向第三个。解决方式是先python -c "import sys; print(sys.executable)"确认当前解释器路径,再用python -m pip install pygame保证 pip 装在同一个解释器上。绝对不要用裸的pip install,它可能装到别的地方去了。
现象二:FileNotFoundError: res/image/hero1.png,但文件明明在那。原因是相对路径以“当前工作目录”为准,而不是脚本所在目录。在 IDE 里点运行,工作目录是项目根目录,能跑;换到命令行cd到别的目录再执行,就找不到图了。解决办法是在入口最前面加一句os.chdir(os.path.dirname(os.path.abspath(__file__))),把工作目录锁定到脚本所在位置,一劳永逸。
现象三:窗口一闪而过,什么都没看到。这是主循环里抛了异常,但pygame.quit()之前就崩了,窗口开了又关。排查方法是在main外面套一层try/except,捕获后打印完整 traceback 并input()等一下,或者直接看终端的红字。新手常犯的是把pygame.display.set_mode写在pygame.init()之前,或者图片加载用了中文路径在部分环境下编码出错。
5.2 跑起来之后的手感与异常
现象四:没有声音,或者启动时报 mixer 初始化失败。常见原因是运行环境没有可用的音频设备(比如远程桌面、容器里),或者音频格式不被支持。稳妥写法是把pygame.mixer.init()包在try/except里,失败就设一个全局SOUND_ENABLED = False,后续所有播放调用前先判断这个标志,让游戏在无声状态下也能正常玩,而不是直接崩掉。另外音频文件建议统一转成 ogg 或 wav,mp3 在不同平台的解码支持并不一致。
现象五:玩两三分钟后越来越卡,最后掉到十几帧。几乎可以断定是精灵组只增不减:子弹飞出屏幕后没有kill(),敌机从底部飞出去也没有kill(),几百个看不见的对象还在参与碰撞检测和绘制。解决方式是在每个精灵的update末尾加一句边界判断,if self.rect.bottom < 0 or self.rect.top > SCREEN_H: self.kill()。这个坑的隐蔽之处在于前三十秒完全正常,等到对象攒够了几百个才开始卡,很容易误判成“是不是我的碰撞检测写得太慢”。
现象六:换了个分辨率的显示器,游戏变得特别小或者特别大。高分屏上系统会做缩放,set_mode拿到的逻辑分辨率不变,但实际像素密度变了,图会糊。可以开启pygame.display.set_mode(size, pygame.SCALED)让 pygame 自己处理缩放,或者在settings.py里把窗口尺寸和素材尺寸绑成比例关系。
6. 二次开发:换素材、调手感、加玩法的三个切入点
改这个项目,我建议按“先调参数、再换皮、最后加逻辑”的顺序,每一步都能独立验证,出了问题好回滚。
第一步是调手感,全部集中在conf/settings.py,不改任何逻辑代码。常用的几个参数和我的经验值:
| 参数 | 含义 | 建议范围 | 调大的效果 |
|---|---|---|---|
HERO_SPEED | 玩家速度(像素/秒) | 280 ~ 420 | 更跟手,但容易撞 |
FIRE_INTERVAL | 射击间隔(毫秒) | 120 ~ 250 | 弹幕密,伤害高 |
ENEMY_BASE_INTERVAL | 初始刷怪间隔 | 700 ~ 1000 | 前 30 秒更轻松 |
INVINCIBLE_MS | 受伤无敌时长 | 800 ~ 1500 | 容错高,难度降 |
第二步是换皮。把res/image里的图换成自己的一套,注意三件事:尺寸统一、必须是带 alpha 的 PNG、帧动画编号连续。换完先只换玩家飞机跑一次,确认没错位再换敌机,一次全换出问题很难定位是哪张图尺寸不对。
第三步是加玩法。最常见的是加一种新敌机,比如会左右摆动的“蛇形机”。这时候只要在ENEMY_TABLE里加一条,再写一个子类覆盖update:
class ZigzagEnemy(Enemy): """横向正弦摆动的敌机,增加躲避难度""" def __init__(self, image, center, amp=60, period=2.0): super().__init__(image, center) self.base_x = center[0] self.amp = amp # 摆幅,像素 self.period = period # 摆动周期,秒 self.t = 0.0 def update(self, dt): self.t += dt import math # 纵向正常下落,横向叠加正弦偏移 self.rect.y += self.speed * dt self.rect.x = self.base_x + self.amp * math.sin(2 * math.pi * self.t / self.period)这里amp控制摆动的横向幅度,period控制摆动快慢,两个参数决定了这个敌人的“讨厌程度”。注意base_x要存住初始横坐标,否则每帧在上一帧的结果上再叠加偏移,会越摆越远直接飞出去——这是我第一次写摆动敌人时犯的错,飞机飞着飞着就出屏了,查了十分钟才反应过来。
验证改造成果有个笨但有效的办法:在屏幕上把hp、score、当前敌机数量用font.render打到左上角,边玩边看数值变化是否合理。数值异常往往比画面异常更早暴露问题。
最后说个习惯。我早期改这类项目,最常犯的毛病是“一口气改五处再运行”,一旦崩了根本不知道是哪处引起的。后来强制自己每改一个参数或者一个类,就完整跑一局,确认没崩、手感没跑偏、分数正常结算,再动下一处。这个规矩看着慢,实际上比反复回滚快得多。希望帮到你。
本文还有配套的精品资源,点击获取