简介:这是一份面向初学者的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 细节里。先把最丑但能玩的版本做出来,手感对了再美化,这个顺序我踩过坑之后就没再改过。希望帮到你。
本文还有配套的精品资源,点击获取