简介这份资源是一套基于J2ME平台的飞机射击类手机游戏源码面向刚接触移动游戏开发的初学者用来理解小游戏从框架搭建到逻辑落地的完整过程。代码整体架构清晰适合通过实际项目学习游戏循环、精灵对象、碰撞检测、键盘响应和屏幕绘制等核心概念同时掌握MIDP与CLDC移动开发API的基本用法。压缩包共200个文件大小4.27MB内含138张PNG图片素材、多个class编译文件、20段mid音频以及Java源文件、JAD/JAR配置和工程文件。PNG用于飞机和敌机等精灵绘制mid音效配合射击与爆炸反馈便于对照源码与产物检查打包流程。当前已有1950人学习下载。项目中的textPlan设计文档可以帮助快速梳理游戏状态、敌机生成、射击逻辑等模块爆炸、敌机、得分等典型实现均可作为课堂练习或毕业设计的参考起点也可作为移植到Android等平台的入门示例。1. 简单射击类游戏代码难点不在开枪而在主循环搜“简单射击类游戏代码”的人大多数不是想做商业大作而是想要一个能跑、能看懂、能改的最小版本。这就和“小恐龙游戏网址代码大全可复制”是同一类需求但小恐龙只需要跳跃和碰到障碍射击类多了一个“发射子弹并判断命中”等于把事件处理、对象管理、碰撞检测三件事一次打通。最容易翻车的地方恰恰不是开枪逻辑而是背后的主循环窗口要不要刷新、事件要不要排队、速度跟着帧率还是跟着时间走这些没理顺游戏不是黑屏就是手感别扭。下面直接给方案。我用 Python Pygame 写一个可复现的最小版本从技术选型、核心机制、手感调参一路落到避坑和打包。想第一次把游戏完整跑起来的人、准备拿它当课程设计的人都能照着敲。2. 技术选型为什么我用 Python Pygame而不是网页 Canvas 或 Godot2.1 三种方案怎么选小恐龙游戏能放网页射击类却能收进单文件看到“简单射击类游戏代码”第一反应可能是去抄“小恐龙游戏网址代码大全”这种网页版毕竟浏览器打开就能玩。网页版确实方便分享但代码会拆成 HTML、CSS、JavaScript 三份Canvas 画布、requestAnimationFrame 循环、键盘事件监听各放一处。对“我想看懂每一行”的人来说这反而成了负担。我更推荐 Python Pygame。理由有三个第一是单文件整款游戏逻辑可以收在一个.py里阅读顺序就是代码顺序第二是 Pygame 内置精灵Sprite和碰撞检测不需要自己写矩形相交公式第三是 Python 的报错信息相对直白遇到问题把报错贴到搜索引擎能查到大量“示例代码讲解”风格的帖子。Godot 和 Unity 当然也能做而且上限更高但那是引擎思维节点、场景、信号这些概念学完已经过了一下午。单纯为了跑通一个“简单射击类游戏代码”引擎是杀鸡用牛刀。真要做手感、做关卡后续确实可以迁过去但第一版用 Pygame 把逻辑吃透是性价比最高的路径。哪怕你最终目标是用 Unity 做商业项目先在 Pygame 里理解游戏循环和碰撞模型迁移时也能少走弯路。2.2 最小窗口骨架先让一个窗口稳定跑起来先把环境装上Python 3.8 以上都能用Pygame 一条命令装好pip install pygame然后是最小骨架。很多教程上来就贴几百行完整代码读者反而看不出“游戏循环”长什么样。我们先做一个只有窗口的版本确认事件和刷新两条链路都通import pygame import sys # 初始化窗口800x600 是常见分辨率 pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(简单射击游戏) # 主循环里唯一的时间源 clock pygame.time.Clock() running True while running: # 事件队列必须每帧取空否则窗口会“无响应” for event in pygame.event.get(): if event.type pygame.QUIT: running False # 用深灰色填充整个画布掩盖上一帧的残留 screen.fill((30, 30, 30)) # flip() 把绘制内容真正送到屏幕 pygame.display.flip() # 锁帧率到 60也就是每秒最多执行 60 次循环 clock.tick(60) pygame.quit() sys.exit()这段代码里最容易漏掉的是pygame.event.get()。Windows 系统会在窗口消息得不到响应时把窗口标成“未响应”所以即使你现在还没写游戏逻辑也要先把事件循环摆进主循环最顶部。screen.fill和pygame.display.flip()是正常画面与黑屏之间唯一的差别。clock.tick(60)控制了循环速度后面所有跟“速度”有关的数值都会依赖它。这个骨架里还没出现任何图片资源。用Surface填充纯色当贴图是为了把“资源管理”和“逻辑开发”拆开先跑通机制再慢慢用 png 替换。换成图片时记得在image上调用convert_alpha()否则透明背景会变成黑块这是很多人到第二个项目才发现的问题。2.3 主循环为什么值得慢点写游戏的主循环本质上是个“死循环”每次循环做三件事取事件、更新状态、重绘画布。一个简单游戏里几乎所有 bug 都来自这三件事的顺序和遗漏。比如把screen.fill放在事件循环前面画面会抖把clock.tick放在最底部当帧耗时不稳定时速度就会忽快忽慢。把这段骨架跑上十分钟别急着加玩法后面每加一个功能都往这个循环里塞心里就有底。你甚至可以故意删掉一行看看会发生什么删掉pygame.event.get()窗口立刻假死删掉flip()画面一片黑。这种“破坏性实验”比看十篇文档都管用。真要做全屏把set_mode的参数改为(0, 0), pygame.FULLSCREEN即可但注意事件处理和分辨率适配会变复杂第一版先固定窗口就好。如果主循环里某个函数卡了很久帧率黏在个位数优先检查那个函数里有没有pygame.time.delay或者耗时的文件读取别急着怀疑硬件。3. 把射击机制写进代码玩家、子弹、敌人与计分的实现3.1 用 Sprite 组织玩家和敌人每个单位都是一个“矩形 贴图”Pygame 里的精灵Sprite不神秘它就是一个带self.image和self.rect的普通类。image是长什么样rect是它占多大、在哪。用 Sprite 的好处是后面调用pygame.sprite.Group.add()和碰撞检测时Pygame 会自动用 rect 做矩形相交判断不用自己维护一堆坐标。先定义三个最基本的类import random import pygame class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((40, 50)) self.image.fill((0, 180, 255)) self.rect self.image.get_rect() self.rect.midbottom (400, 550) self.speed 5 # 像素/帧60fps 下约 300 像素/秒 def update(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right 800: self.rect.x self.speed class Enemy(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((30, 30)) self.image.fill((220, 40, 40)) self.rect self.image.get_rect() self.rect.x random.randint(30, 770) # 不贴着边缘生成 self.rect.y -30 # 从画布外进场 self.speed 3 def update(self): self.rect.y self.speed if self.rect.y 600: self.kill() # 超出屏幕就销毁防止内存膨胀 class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((6, 16)) self.image.fill((255, 220, 0)) self.rect self.image.get_rect() self.rect.centerx x self.rect.bottom y # 子弹从枪口位置生成 self.speed 10 def update(self): self.rect.y - self.speed if self.rect.y -20: self.kill()参数说明这三个speed是“像素/帧”不是“像素/秒”。在 60 帧锁定的前提下5 像素/帧就是每秒 300 像素玩家从左侧走到右侧大约 2.7 秒这个手感偏稳。敌人 3 像素/帧每秒 180 像素从顶部到进入屏幕约 0.17 秒留足了反应时间。子弹 10 像素/帧每秒 600 像素是后面碰撞穿模的关键数值等下我会专门讲。这里有个容易踩的细节Bullet的rect.bottom y而生成时传入的是player.rect.top。如果你写的是rect.y y子弹会从玩家头顶偏下的位置冒出来视觉上像从胸口开枪。用 bottom 对齐子弹才真正顶在枪口。3.2 空格开枪与子弹生命周期每按一次生成一发别按一次生成一串玩家操作只有左右移动和空格开枪。先初始化精灵组再在主循环里监听按键事件player Player() enemies pygame.sprite.Group() bullets pygame.sprite.Group() all_sprites pygame.sprite.Group(player) last_spawn 0 spawn_interval 1200 # 毫秒也就是每秒生成不到一个敌人 while running: # 读取当前帧按下的所有键适合做持续移动判断 keys pygame.key.get_pressed() player.update(keys) for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN and event.key pygame.K_SPACE: bullet Bullet(player.rect.centerx, player.rect.top) bullets.add(bullet) all_sprites.add(bullet) # 定时生成敌人get_ticks 返回开机到现在的毫秒数 now pygame.time.get_ticks() if now - last_spawn spawn_interval: enemy Enemy() enemies.add(enemy) all_sprites.add(enemy) last_spawn now # 统一调用所有精灵的 update() all_sprites.update() # 绘制前先填充背景再画所有精灵 screen.fill((30, 30, 30)) all_sprites.draw(screen) pygame.display.flip() clock.tick(60)这里的事件写法有个细节空格监听放在KEYDOWN里而不是用keys[pygame.K_SPACE]持续判断因为我不想按住空格就连发而是每按一次开一枪。如果你想要连发模式可以加一个“按下标志”再配合帧计数节流。bullet在y -20时kill()是在清理离屏对象这条必须写否则运行十分钟后内存里会挂着几千个越界精灵。有个常见误用是把all_sprites.update()写在事件循环之前。这样写不会报错但同类精灵的更新顺序会乱比如子弹先更新、玩家后更新碰到特殊玩法时逻辑很难查。我一般固定顺序先读取输入再处理事件产生新对象最后统一update()和draw()。3.3 碰撞检测与计分groupcollide 一行完成命中判定写碰撞时不用遍历每个子弹、每个敌人去手算矩形相交Pygame 提供了一行方案# 这行同时处理两件事子弹碰到敌人子弹和敌人一起消失 for bullet_hit in pygame.sprite.groupcollide(bullets, enemies, True, True): score 1groupcollide是组碰撞函数两个True分别表示“子弹被撞到后销毁”和“敌人被撞到后销毁”。它返回一个字典key 是撞到东西的子弹对象循环用不到 key 本身只把它当作“这一帧有碰撞发生”的信号。因为碰撞后对象就销毁了不会出现一个敌人被两发子弹同时命中、一次击杀加两分的情况。加分之后要把分数画到屏幕上否则玩家不知道自己在玩什么font pygame.font.SysFont(microsoftyahei, 32) score_text font.render(Score: str(score), True, (255, 255, 255)) screen.blit(score_text, (12, 12))这里用SysFont指定微软雅黑是为了后面显示中文提示不出现方块。如果用默认字体中文一定乱码。分数渲染每帧都做也没关系数据量很小等以后分数多了可以只在分数变化时才重建surface。如果你想给碰撞加个爆炸效果别把粒子生成代码直接塞进groupcollide的循环里因为碰撞发生和绘制刷新不在同一时刻很容易出现粒子闪烁一帧就消失。更好的做法是把碰撞事件记录到一个队列下一帧统一生成粒子这样主循环结构不会被破坏。4. 手感和难度曲线这三个参数决定游戏好不好玩4.1 先固定帧率再按秒换算速度调参才有参照系很多新手的调参体验是“改了一个数字不知道变快还是变慢”。因为直接写speed 5会让人误以为这是个绝对值其实在 60fps 环境下 5 像素/帧就是 300 像素/秒。如果你的电脑跑不满 60 帧比如实际只有 30 帧游戏就整体变慢一半。所以第一件事永远是clock.tick(60)锁帧或者用dt两次循环的时间差去更新逻辑。简单游戏用前者足够。下面是推荐的一个平稳手感参数表对象像素/帧换算60fps说明玩家5300 px/s横穿 800px 屏约 2.7s子弹10600 px/s已接近逐帧碰撞上限敌人3180 px/s初期敌人留反应时间生成间隔1200ms每秒约 0.83 个初始压力窗口800x60016:9 削边适合小屏这套数值是人肉试出来比较稳妥的起点。别直接抄抄完也要在你自己机器上跑两分钟玩家速度超过 8 像素/帧会感觉发飘子弹低于 6 像素/帧会觉得“子弹追不上敌人”生成间隔低于 700ms 时屏幕上会密密麻麻体验直线下降。用秒做单位想再换回像素/帧写进代码调参就不会靠瞎猜。4.2 难度曲线生成间隔和敌人速度不要拍脑袋写死简单游戏也不该一成不变越往后越要有压力。但难度曲线不能一下子拉满常见做法是用击杀数驱动两个变量# 敌人速度随击杀数缓慢上升最高 8 像素/帧 enemy_speed min(8, 3 score * 0.02) # 生成间隔随击杀数缩短最低 300ms spawn_interval max(300, 1200 - score * 10)这两条公式把难度变化写成了代码开局 3 像素/帧、1200ms第 20 个击杀时敌人速度到 3.4、间隔 1000ms第 60 个击杀时速度 4.2、间隔 600ms第 90 个击杀后间隔触底。这样玩家在前期有成长空间后期压力稳定不下来。想调整节奏优先动间隔的1200和速度的3两个初始值把它们看作一条直线的截距。注意max和min的钳制很重要。不钳制的话score高了生成间隔会变成负数主循环里now - last_spawn spawn_interval立刻永久成立游戏会在一瞬间刷满屏敌人直接卡死。这个“没做夹紧导致指数级刷怪”是我见过很多次的小翻车现场。每个动态参数都要带上限和下限不只是这条公式以后写敌人血量和攻击力也一样。4.3 不要用 time.sleep 做暂停事件会跟着一起睡有人为了让游戏“节奏慢下来”在主循环里加了一个time.sleep(0.1)结果是按键有明显延迟、窗口像卡住。原因很简单time.sleep会让整个 Python 进程暂停事件队列里堆积的键盘消息不会被及时处理玩家按一下方向键可能 200ms 后才生效。Pygame 提供的替代方案是pygame.time.delay(ms)它会暂停但一边释放 CPU 一边处理事件不会把事件队列饿死而clock.tick(60)本身就是按帧睡眠绝大多数情况够用了。如果你真的需要“暂停游戏”功能更好的做法是维护一个paused标志if paused: # 暂停时只处理事件和锁帧不更新任何游戏对象 for event in pygame.event.get(): if event.type pygame.KEYDOWN and event.key pygame.K_p: paused False clock.tick(30) continue这段代码的妙处在于暂停期间玩家依然可以按 P 恢复窗口不会显示“未响应”背景保持最后一帧画面。如果直接把paused做成一个无限循环事件处理会被卡死窗口照样假死。暂停是一个很典型的“看起来简单、做起来容易卡”的功能值得单独写清楚。5. 五个常见问题和避坑清单从穿模到打包后打不开5.1 子弹穿模逐帧碰撞的必然结果不是玄学现象子弹看起来明明穿过敌人身体却没有触发计分偶尔又正常。原因碰撞检测是每帧做一次矩形相交判断子弹速度太快时这一帧还在敌人左边下一帧已经到了右边两帧之间敌人没有“被碰到”过。简单场景里这不是玄学而是采样率不够刚好在两帧之间跳过了目标。解决把子弹速度限制在“目标尺寸 / 单帧时长”以内。60fps 下单帧约 16.7ms敌人宽 30px子弹 10 像素/帧正好在安全线上沿再往上调就危险。如果你的子弹必须更快就把碰撞检测从“点是否落在矩形内”升级为“线段与矩形相交”或者把一步移动拆成两小步分别碰撞。实战里我一般不加复杂度直接约束速度上限效果最稳定。5.2 窗口黑屏、鼠标变沙漏事件循环和刷新循环没有解耦现象窗口能打开但全黑点击关闭按钮没反应操作系统提示“程序未响应”。原因最常见的是主循环里漏了pygame.event.get()。Windows 把“不处理消息的窗口”判定为无响应Pygame 也一样。另一个常见原因是调用了screen.fill()却忘记pygame.display.flip()画布内容永远不提交。解决先检查主循环顶部有没有事件循环再看是否调用了flip()。我习惯把事件处理、逻辑更新、绘制三块各写成一个独立函数哪怕暂时很大后面接功能也不会打乱顺序。还有个小坑pygame.display.update()和flip()不要混用整屏刷新统一用flip()最稳。5.3 中文路径引发的图片加载失败现象代码在自己电脑上用英文路径跑得好好的把整个项目复制到E:/射击游戏/目录下pygame.image.load()就报错找不到文件。原因Pygame 底层依赖 SDL而 SDL 在某些 Windows 环境下对非 ASCII 路径支持不稳定。不是图片坏了也不是代码错了是路径里带中文。解决项目目录和引用路径全部使用英文字母、数字、下划线比如E:/shoot_game/。如果你的用户必须用中文目录可以在程序启动时把当前工作目录切到脚本所在目录import os os.chdir(os.path.dirname(os.path.abspath(__file__)))再用相对路径./assets/player.png能躲过一部分坑。但最省心的方案还是英文目录发布游戏时也建议提醒用户别把压缩包解压到桌面中文文件夹里。5.4 关闭窗口后进程不退出多个主循环打架现象点掉游戏窗口游戏界面没了任务管理器里 Python 进程还活着风扇还在转。原因很多人在中途为了“重开一局”又写了一个while True循环结果pygame.QUIT只退出了内层循环外层主循环还在空转或者pygame.quit()被sys.exit()挡住进程挂在那里。解决整款游戏只保留一个主循环用running标志控制退出重开一局的逻辑用“重置所有对象”代替“重新进入新循环”。在pygame.QUIT事件里先pygame.quit()再sys.exit()保证资源释放。写代码时顺手把pygame.quit()放在主循环外面作为正常退出的唯一出口。5.5 打包后提示 msvcp140.dll 缺失先装 VC 运行库不是代码问题现象把游戏用 PyInstaller 打包成 exe在自己电脑上能打开发给别人后双击报“由于找不到 msvcp140.dll 无法继续执行代码”。原因PyInstaller 打包的是 Python 解释器和依赖但 C 扩展库运行时需要系统的 Visual C Redistributable 运行库。目标机器没装过 VC 运行库就会报这个错。解决别在打包阶段硬塞 dll最正规的做法是发布时附一个vc_redist.x64.exe安装包让用户先安装再运行。如果只是自己玩装一次就好了。这个坑不影响程序逻辑但很容易让新手误以为打包命令写错了实际命令没问题。打包前先确认目标机器有没有装 VC 运行库能省下一整天的“为什么换个电脑就跑不了”的困惑。6. 扩展一步粒子爆炸、音效与把游戏打包给别人玩6.1 用 20 行粒子类做出爆炸反馈没有反馈的射击游戏会显得很“干”给敌人加一个消失时的粒子爆炸能显著提升手感。粒子本质上是一堆快速飞散的小矩形生命周期很短class Particle(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((4, 4)) self.image.fill((240, 180, 60)) self.rect self.image.get_rect(center(x, y)) self.vx random.uniform(-4, 4) self.vy random.uniform(-4, 4) self.life 20 def update(self): self.rect.x self.vx self.rect.y self.vy self.life - 1 if self.life 0: self.kill()在碰撞计分的地方顺手生成 8 个粒子加入粒子组然后照常particles.draw(screen)即可。敌人消失的瞬间会有短暂的碎片感实现成本极低效果却很明显。粒子速度建议控制在 4 像素/帧以内太快会满屏闪烁反而刺眼。6.2 打包命令、资源路径与帧率验证打包 Pygame 游戏最常用的一行命令是pyinstaller -F -w shoot_game.py-F把程序打成一个单独 exe-w不显示控制台窗口。注意如果游戏用到了图片和音效资源-F单文件模式会在运行时解压到一个临时目录直接写相对路径容易找不到资源。常见做法是把资源路径统一到一个函数里代码开头用getattr(sys, frozen, False)判断是否处于打包状态再基于sys._MEIPASS计算资源目录这样开发环境和打包环境都能读到资源。验证游戏是否健康我习惯把帧率直接打在窗口标题上每半秒更新一次。如果稳定 60说明逻辑负载不高如果掉到 30优先看粒子组数量和子弹生命周期清理有没有丢。跑上十分钟再看一眼内存增长能确认kill()清理有没有正常工作。我有一次把子弹速度调到 20 像素/帧想追求爽快感结果满屏子弹穿敌不中弹了半小时才发现是采样率问题。后来老老实实把碰撞和速度的边界写进注释避免后人再踩。这种小游戏的乐趣不在华丽而在于每个机制都能被自己掌控希望这些避坑经验能帮你把最简单的版本跑通、跑稳然后大胆改出你自己的手感。本文还有配套的精品资源点击获取