
简介这是一份面向初学者的j2ME手机射击游戏完整代码以飞机为主角适合想入门移动游戏开发、理解游戏编程基础流程的读者。资源包共200个文件约4.27MB包含11个java源码、22个class编译文件、3个jad与2个jar打包文件以及138个png图片素材、20个mid音频和eclipseme、classpath等工程配置覆盖从源码到可运行程序的完整结构。代码围绕游戏循环、精灵对象、碰撞检测、用户输入处理、图形渲染、音频播放与状态管理等核心知识点展开飞机与敌机、子弹的交互逻辑清晰便于对照学习。压缩包内还附有textPlan文件可能记录设计计划与关键逻辑描述可作为理解代码工作原理的线索。目前已有1950人学习适合作为j2ME游戏开发的实践参考。1. 从零手搓一款简单射击类游戏为什么我不建议你直接抄开源项目很多人第一次想写射击游戏第一反应是去搜一个开源仓库clone 下来改改贴图就当成自己的作品。我早年也这么干过结果就是打开源码一看几千行耦合在一起的逻辑连子弹从哪发射的都找不到改一个参数整个游戏就崩。射击游戏看着简单——玩家移动、敌人出现、子弹飞出去、打中扣血——但它其实是游戏开发里最典型的麻雀虽小五脏俱全输入系统、物理碰撞、对象生命周期管理、渲染循环、状态机一个都跑不掉。你真正需要的不是一份能跑的代码而是一套自己能掌控的最小架构。这篇笔记就围绕一款简单射击类游戏代码这个目标把从窗口创建到子弹命中判定的完整链路拆开讲清楚用 Python Pygame 作为落地技术栈因为它依赖少、反馈快、适合把每一个机制都摊开来看。读完你能得到一个 300 行以内、结构清晰、可以自己往上加武器和关卡的射击游戏骨架而不是一团看不懂的玄学代码。适合有基础编程能力、想真正理解游戏循环而不是调包的新手也适合想找个干净模板做原型验证的老手。2. 射击游戏的最小骨架游戏循环与坐标系2.1 为什么游戏循环是所有机制的宿主任何实时游戏的核心都是一个 while 循环它每秒钟执行几十次每次做三件事处理输入、更新状态、重绘画面。这个循环的频率就是帧率60 帧意味着每 16.6 毫秒跑一轮。射击游戏里子弹的移动、敌人的生成、碰撞的判定全都挂在这个循环的更新状态阶段。如果你不理解这一点就会写出按下按键子弹就瞬移这种脱离时间的概念的错误代码。我一般会把循环写成固定时间步长的形式让物理更新和渲染解耦这样即使某一帧卡顿子弹速度也不会忽快忽慢。坐标系是第二个必须先立住的根基。Pygame 的坐标原点在窗口左上角x 向右增大y 向下增大。这和数学课本里的笛卡尔坐标系 y 轴方向相反是新手最容易翻车的地方——你以为向上是 y 加结果角色往下跑。射击游戏里玩家在底部、敌人从顶部来所以敌人的 y 值是从 0 往窗口高度增长玩家子弹的 y 值是从玩家位置往 0 递减。import pygame import sys pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Simple Shooter) clock pygame.time.Clock() # 玩家初始位置底部居中 player_x, player_y WIDTH // 2, HEIGHT - 60 PLAYER_SPEED 5 while True: dt clock.tick(60) / 1000.0 # 秒为单位的时间步长 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_x - PLAYER_SPEED if keys[pygame.K_RIGHT]: player_x PLAYER_SPEED # 边界钳制防止跑出窗口 player_x max(0, min(WIDTH - 40, player_x)) screen.fill((10, 10, 30)) pygame.draw.rect(screen, (0, 200, 255), (player_x, player_y, 40, 40)) pygame.display.flip()这段代码里clock.tick(60)既限制帧率又返回上一帧耗时dt后续可以用来做基于时间的移动。player_x的钳制用max/min一行搞定比写 if 判断更不容易漏边界。pygame.draw.rect的第三个参数是 (x, y, 宽, 高)注意它是从左上角开始画的不是中心点这个细节在后续做碰撞时会反复用到。2.2 用精灵组管理对象而不是一堆散变量上面那种player_x、player_y的写法在只有一个角色时能用一旦有了子弹和敌人就会失控。正确做法是用 Pygame 的Sprite和Group。每个游戏对象继承Sprite拥有image和rect两个属性rect就是它的碰撞盒。Group负责批量更新和绘制还能用groupcollide一次性检测两组对象之间的碰撞。这是射击游戏代码能不能扩展的分水岭。class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((40, 40)) self.image.fill((0, 200, 255)) self.rect self.image.get_rect(center(WIDTH // 2, HEIGHT - 60)) self.speed 5 def update(self, keys): if keys[pygame.K_LEFT]: self.rect.x - self.speed if keys[pygame.K_RIGHT]: self.rect.x self.speed self.rect.clamp_ip(pygame.Rect(0, 0, WIDTH, HEIGHT))get_rect(center...)让矩形以给定点为中心比手动算左上角省事。clamp_ip是原地钳制直接把矩形限制在窗口内比手写边界判断更稳。把玩家做成类之后子弹和敌人都照这个模板来代码结构立刻清晰。3. 子弹发射与碰撞判定射击手感的核心3.1 子弹的生成、移动与回收子弹的本质是一个每帧向上移动的小矩形飞出屏幕后必须销毁否则内存里会堆积成千上万个已经看不见的对象帧率断崖式下跌。这是射击游戏最常见的性能坑。我一般用Group管理所有子弹在update里检查rect.bottom 0就调用kill()把自己从组里移除。发射逻辑要处理两个问题一是按键持续按住时不能每帧都发射否则一秒出 60 发子弹二是要有发射冷却。用一个时间戳记录上次发射时刻和当前时间比较超过冷却间隔才允许再发射。import time class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((6, 16)) self.image.fill((255, 230, 0)) self.rect self.image.get_rect(center(x, y)) self.speed 10 def update(self): self.rect.y - self.speed if self.rect.bottom 0: self.kill() # 飞出屏幕即销毁 # 发射控制 last_shot 0 SHOT_COOLDOWN 0.2 # 秒 now time.time() if keys[pygame.K_SPACE] and now - last_shot SHOT_COOLDOWN: bullets.add(Bullet(player.rect.centerx, player.rect.top)) last_shot nowSHOT_COOLDOWN是手感的关键参数0.2 秒约等于每秒 5 发太快像泼水太慢像卡壳一般 0.15 到 0.3 之间调。Bullet的初始位置用player.rect.centerx和player.rect.top保证子弹从玩家枪口位置出来而不是从中心冒出来。kill()是 Sprite 自带方法调用后该对象会在下一次组操作时被移除不用手动 del。3.2 碰撞检测矩形判定够不够用Pygame 提供两种碰撞rect矩形碰撞和mask像素级碰撞。射击游戏里 99% 的情况矩形碰撞就够了因为子弹和敌人都是规则形状矩形判定快得多。pygame.sprite.groupcollide(bullets, enemies, True, True)一行就能检测两组之间所有碰撞两个 True 分别表示碰撞后子弹和敌人是否销毁。返回的是一个字典键是子弹值是它撞到的敌人列表需要计分时可以从这里取。# 子弹击中敌人双方都销毁 hits pygame.sprite.groupcollide(bullets, enemies, True, True) score len(hits) * 10 # 敌人撞到玩家只销毁敌人玩家扣血 if pygame.sprite.spritecollide(player, enemies, True): hp - 1 if hp 0: running Falsegroupcollide的第三个参数dokill控制第一组是否销毁第四个控制第二组。计分用len(hits)是因为一次可能有多发子弹同时命中。玩家碰撞用spritecollide它检测单个精灵和一组精灵第三个参数 True 表示撞到的敌人销毁。这里有个容易忽略的点如果敌人和玩家重叠面积很大一帧内可能触发多次扣血实际项目里通常给玩家加一个受击无敌时间。3.3 敌人刷新与难度曲线敌人不能一次性全放出来要有节奏地生成。常见做法是用一个计时器每隔固定间隔在顶部随机 x 位置生成一个敌人。难度曲线通过缩短生成间隔或提高敌人下落速度来实现。我一般把生成间隔做成随时间递减的变量让游戏越玩越紧张。import random class Enemy(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((36, 36)) self.image.fill((255, 80, 80)) x random.randint(20, WIDTH - 20) self.rect self.image.get_rect(center(x, -20)) self.speed random.uniform(2, 4) def update(self): self.rect.y self.speed if self.rect.top HEIGHT: self.kill() spawn_timer 0 SPAWN_INTERVAL 1.0 # 初始每秒一个 elapsed 0 # 在主循环中 elapsed dt spawn_timer dt if spawn_timer SPAWN_INTERVAL: enemies.add(Enemy()) spawn_timer 0 SPAWN_INTERVAL max(0.3, 1.0 - elapsed * 0.02) # 逐渐加快random.uniform(2, 4)让每个敌人速度略有差异视觉上更自然。SPAWN_INTERVAL用max(0.3, ...)设下限防止后期敌人密到无法游玩。elapsed累计游戏时间乘以 0.02 是难度增长系数这个值需要根据实际手感调太大游戏会突然变难。4. 避坑与排查射击游戏代码里最容易翻车的五件事4.1 现象子弹速度在不同电脑上不一样原因用了每帧固定位移比如rect.y - 10而没有乘以时间步长。帧率高的机器子弹飞得快帧率低的飞得慢。解决把速度定义成每秒多少像素更新时写成rect.y - self.speed * dt其中dt是clock.tick(60) / 1000.0。这样无论帧率如何子弹每秒移动的距离一致。4.2 现象游戏玩几分钟后越来越卡原因子弹或敌人飞出屏幕后没有销毁对象越积越多每帧的碰撞检测和绘制开销线性增长。解决在每个update里检查对象是否超出边界超出就kill()。另外确认groupcollide的dokill参数设对了否则撞到的对象也不会消失。4.3 现象按住空格子弹像机关枪一样喷原因pygame.key.get_pressed()每帧都返回 True没有冷却控制。解决引入last_shot时间戳和SHOT_COOLDOWN间隔只有距上次发射超过冷却时间才允许再发射。注意用time.time()而不是帧计数后者在帧率波动时不可靠。4.4 现象敌人和玩家碰撞时一帧扣好几滴血原因敌人和玩家矩形重叠期间每一帧spritecollide都返回 True导致连续扣血。解决给玩家加一个受击无敌计时器被撞后设置invincible_until time.time() 1.0在无敌期内跳过碰撞检测。或者碰撞后立即把敌人销毁并给玩家一个短暂的无敌闪烁效果。4.5 现象窗口关闭后程序卡死或报错原因没有正确处理pygame.QUIT事件或者退出时没有调用pygame.quit()。解决在事件循环里检测QUIT后先pygame.quit()再sys.exit()。如果用了音乐和字体也要在退出前释放。另外注意sys.exit()会抛SystemExit异常确保它不在 try 块里被吞掉。5. 从能跑到好玩三个提升射击手感的进阶技巧第一个技巧是屏幕震动。子弹命中或玩家受击时把整个画面偏移几个像素再恢复持续两三帧能极大增强打击感。实现方式是在绘制前给所有对象的绘制坐标加一个随机偏移量或者用screen.blit时传入偏移后的目标矩形。震动幅度一般 3 到 6 像素时间不超过 0.1 秒太强会让人晕。第二个技巧是对象池。当子弹和敌人数量很大时频繁创建和销毁对象会触发垃圾回收造成帧率抖动。对象池的思路是预先创建一批对象不用时标记为未激活而不是销毁需要时直接取出来重置状态。Pygame 的Sprite配合一个自定义的active标志就能实现。对于简单射击游戏对象池不是必须的但如果你打算把子弹数量提到几百发它能明显稳住帧率。第三个技巧是用状态机管理游戏流程。把游戏分成开始菜单游玩中暂停结束几个状态用一个变量记录当前状态主循环里根据状态决定更新和绘制哪些内容。这样加暂停功能和重开功能会非常干净不会把逻辑全堆在 while 循环里。技巧适用场景关键参数代价屏幕震动命中反馈幅度 3-6px时长 0.05-0.1s几乎为零对象池子弹/敌人超过 200 个池大小 峰值数量 × 1.2代码复杂度上升状态机需要菜单/暂停/重开状态枚举 转移条件前期设计成本我自己的习惯是先把游戏循环和碰撞跑通确认子弹能打中敌人、敌人能扣血再去加震动和状态机。很多新手一上来就想做完整的菜单和音效结果核心机制还没调稳就陷进 UI 细节里。先把最丑但能玩的版本做出来手感对了再美化这个顺序我踩过坑之后就没再改过。希望帮到你。本文还有配套的精品资源点击获取