最近心血来潮,想做个小游戏练练手。想起小时候玩的那些红白机射击游戏,简单但上瘾。于是决定用现在比较火的技术栈——Rust+React+Tauri,复刻一个复古风格的太空射击游戏。说实话,一开始心里也没底。游戏开发看起来挺复杂的,但真正动手后发现,只要理清核心逻辑,一步步来,并没有
最近心血来潮,想做个小游戏练练手。想起小时候玩的那些红白机射击游戏,简单但上瘾。
于是决定用现在比较火的技术栈 —— Rust + React + Tauri,复刻一个复古风格的太空射击游戏。
说实话,一开始心里也没底。游戏开发看起来挺复杂的,但真正动手后发现,只要理清核心逻辑,一步步来,并没有想象中那么难。
这篇文章就记录一下整个开发过程和踩过的坑,希望能给想入门的同学一些参考。
游戏长这样:

整个项目做下来,最大的感受是:
Rust 写游戏逻辑真的很爽,类型安全加上高性能,基本不用担心运行时出错。
一开始纠结过要不要用游戏引擎,像 Unity 或者 Godot。但考虑到我想做的是个 2D 小游戏,用引擎有点"杀鸡用牛刀"的感觉。而且我想试试用 Web 技术栈做游戏,部署起来方便。
最后选了这个组合:
| 技术 | 干嘛用的 | 为什么选它 |
|---|---|---|
| Rust | 游戏核心逻辑 | 内存安全、性能强、编译时就能发现大部分 bug |
| Tauri | 打包成桌面/手机应用 | 比 Electron 体积小很多,性能接近原生 |
| React | 做 UI 界面 | 熟悉,组件化开发方便 |
| TypeScript | 类型检查 | 和 Rust 搭配,前后端都有类型保障 |
架构大概是这么分的:
Rust 负责游戏逻辑运算,React 负责画面渲染和用户输入,两者通过 Tauri 的 IPC 通信。
这样 Rust 专心做它擅长的计算,React 专心做界面,各干各的,代码也清晰。
做游戏最关键的有三个东西:实体、状态、循环。
玩家飞船、敌人、子弹、道具、爆炸效果,这些都是实体。用 Rust 的 struct 和 enum 定义很清晰:
// 玩家
pub struct Player {
pub x: f64,
pub y: f64,
pub hp: i32,
pub fire_level: i32, // 火力等级,1-3级
pub rapid_fire_timer: i32, // 快速射击剩余时间
pub shield_timer: i32, // 护盾剩余时间
}
// 敌人类型
pub enum EnemyType {
Scout, // 小侦察机,速度快但脆皮
Fighter, // 战斗机,会发射子弹
Bomber, // 轰炸机,血厚
Boss, // Boss,出现频率低但威胁大
}
每一帧游戏的状态都存在一个大的结构体里:
pub struct GameState {
pub player: Player,
pub score: i32,
pub lives: i32,
pub level: i32,
pub enemies: Vec<Enemy>,
pub bullets: Vec<Bullet>,
pub powerups: Vec<PowerUp>,
pub explosions: Vec<Explosion>,
pub stars: Vec<Star>, // 背景星星,营造太空感
pub is_running: bool,
pub is_paused: bool,
}
游戏能跑起来全靠一个循环,每 16.67 毫秒(60帧)执行一次:
pub fn update_game(state: &mut GameState, input: &GameInput) {
// 1. 处理玩家输入
handle_player_input(state, input);
// 2. 更新所有东西的位置
update_positions(state);
// 3. 生成新的敌人和道具
spawn_entities(state);
// 4. 检测谁撞到了谁
check_collisions(state);
// 5. 清理已经销毁的东西
cleanup_entities(state);
// 6. 检查是不是过关了或者游戏结束了
check_game_state(state);
}
这个循环就是游戏的心跳,每一帧都走一遍这些步骤,游戏就动起来了。
射击游戏最重要的是打中敌人。碰撞检测用的是最简单的 AABB(轴对齐边界框),就是判断两个矩形有没有重叠:
pub fn check_collision(a: &BoundingBox, b: &BoundingBox) -> bool {
a.x < b.x + b.width
&& a.x + a.width > b.x
&& a.y < b.y + b.height
&& a.y + a.height > b.y
}
虽然简单,但对于这种 2D 游戏完全够用了。每一帧检查玩家子弹和敌人的碰撞、敌人子弹和玩家的碰撞、玩家和敌人的碰撞,还有玩家和道具的碰撞。
游戏不能太简单也不能太难,难度要慢慢上去。我设计的是每 10 关一个循环,每过一轮敌人速度变快,生成率变高:
pub fn get_level_config(level: i32) -> LevelConfig {
let cycle = (level - 1) / 10; // 第几轮
let sub_level = (level - 1) % 10 + 1; // 当前轮的第几关
LevelConfig {
enemy_spawn_rate: 0.02 + cycle as f64 * 0.005,
enemy_speed_multiplier: 1.0 + cycle as f64 * 0.2,
target_score: 1000 * sub_level + cycle * 5000,
}
}
这样玩家一开始能轻松上手,玩到后面就有挑战性了。
道具让游戏更有策略性。我设计了4种道具:
火力等级会影响发射子弹的数量:
match player.fire_level {
1 => spawn_bullet(state, player.x, player.y - 10.0, true),
2 => {
spawn_bullet(state, player.x - 5.0, player.y - 10.0, true);
spawn_bullet(state, player.x + 5.0, player.y - 10.0, true);
}
3 => {
spawn_bullet(state, player.x, player.y - 10.0, true);
spawn_bullet(state, player.x - 8.0, player.y - 8.0, true);
spawn_bullet(state, player.x + 8.0, player.y - 8.0, true);
}
_ => {}
}
1级单发,2级双发,3级扇形三发。吃到火力道具升级,被击中降级,这样玩家会更有动力去躲子弹、吃道具。
前端用 React 主要是处理 Canvas 渲染和用户输入:
export function GameCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const gameStateRef = useRef<GameState | null>(null);
const inputRef = useRef({ left: false, right: false, up: false, down: false, fire: false });
const gameLoop = useCallback(async () => {
if (!gameStateRef.current) return;
// 调用 Rust 更新游戏状态
const newState = await invoke<GameState>('update_game', {
state: gameStateRef.current,
input: inputRef.current,
});
gameStateRef.current = newState;
// 渲染画面
render(canvasRef.current!, newState);
requestAnimationFrame(gameLoop);
}, []);
// 键盘事件
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft' || e.key === 'a') inputRef.current.left = true;
if (e.key === 'ArrowRight' || e.key === 'd') inputRef.current.right = true;
if (e.key === ' ' || e.key === 'z') inputRef.current.fire = true;
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// 启动游戏
useEffect(() => {
initGame().then(state => {
gameStateRef.current = state;
gameLoop();
});
}, [gameLoop]);
return <canvas ref={canvasRef} width={640} height={480} />;
}
function render(canvas: HTMLCanvasElement, state: GameState) {
const ctx = canvas.getContext('2d')!;
// 清空画布
ctx.fillStyle = '#0f380f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制背景星星
state.stars.forEach(star => {
ctx.fillStyle = `rgba(155, 188, 15, ${star.brightness})`;
ctx.fillRect(star.x, star.y, 2, 2);
});
// 绘制玩家
drawSprite(ctx, state.player.x, state.player.y, PLAYER_SPRITE);
// 绘制敌人
state.enemies.forEach(enemy => {
const sprite = getEnemySprite(enemy.enemy_type);
drawSprite(ctx, enemy.x, enemy.y, sprite);
});
// 绘制子弹
ctx.fillStyle = '#9bbc0f';
state.bullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, 3, 8);
});
// 绘制UI(分数、生命值)
drawUI(ctx, state);
}
用 Web Audio API 做了几个简单的 8-bit 音效:
// 射击音效
playShoot() {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(880, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(110, this.ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
}
射击是高频率衰减的方波,爆炸是噪声,道具是上升音阶。虽然简单,但挺有复古游戏的味道。
一开始每帧都重新创建一堆对象,结果帧率掉到 30 帧。后来用了对象池,预分配好重复使用,性能立马就上来了。
最开始用圆形碰撞,结果敌人明明看着没碰到玩家,玩家却掉血了。改成矩形碰撞后准确多了。
手机屏幕小,按钮要做得足够大。还加了触摸控制,左右滑动移动,点击射击。一开始忽略了横竖屏切换,后来加了个监听事件,自动调整布局。
把不同功能分开,结构清晰很多:
src-tauri/src/game/
├── entities/ # 游戏实体
├── state/ # 状态管理
├── collision.rs # 碰撞检测
├── config.rs # 关卡配置
└── types.rs # 类型定义
整个项目做下来,收获还是挺大的:
最重要的是:完成比完美更重要。先做出能玩的版本,再慢慢优化。如果一开始就想着要做得多完美,可能永远都开不了头。
💡 学习建议:看完文章后,不妨自己动手试试。从最简单的开始,先让一个小方块能在屏幕上移动,慢慢加上射击、敌人、碰撞检测...一步步来,你会发现游戏开发其实挺有趣的。
如果这篇文章对你有帮助,欢迎点赞、在看、转发! 🚀
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!