用 Vibe Coding 打造 React 飞机大战游戏 —— 我的实践与学习心得

用 Vibe Coding 打造 React 飞机大战游戏 ------ 我的实践与学习心得

一、项目背景:从小游戏入门前端"游戏开发"

一直想用前端做一个经典的「飞机大战」小游戏,一方面可以练习 React 的组件化和状态管理,另一方面也能体验一下"游戏循环 + 碰撞检测"这类偏游戏的开发模式。

这次我选择在 Vibe Coding 智能编码环境 里完成这个小项目,它相当于一个集成了 AI 搭档的 IDE,可以直接让 AI:

  • 初始化 React 项目(create-react-app
  • 自动检查/安装依赖
  • 帮我写核心逻辑代码(游戏循环、键盘控制、敌机生成等)
  • 启动开发服务器,打开浏览器实时预览

最后完成的是一个 基于 React 的飞机大战游戏,支持:

  • 方向键 ↑↓←→ 控制飞机移动(限制在屏幕范围内)
  • 空格键发射子弹
  • 3 种敌机(小 / 中 / 大),不同血量、速度、得分
  • 子弹与敌机碰撞检测、玩家与敌机/底部的碰撞检测
  • 实时计分 + 游戏开始 / 游戏结束界面

下面按步骤分享整个过程。


二、项目初始化:Vibe Coding

1. 使用 create-react-app 初始化项目

在 Vibe Coding 中,我让 AI 帮我执行项目初始化:

bash 复制代码
npx create-react-app plane-battle

这一步自动完成了:

  • 创建 plane-battle 目录
  • 安装 reactreact-domreact-scripts 等依赖
  • 初始化项目结构和基础配置(支持热更新、错误提示等)

项目结构的核心部分:

text 复制代码
plane-battle/
  src/
    App.js
    App.css
    index.js
    ...

2. 启动开发服务器

在项目根目录执行(或让 Vibe Coding 自动执行):

bash 复制代码
npm start

开发服务器启动后,可在浏览器中访问:

  • http://localhost:3000

热重载和报错提示也都是 create-react-app 原生自带,Vibe Coding 会在终端实时显示编译状态,非常适合快速迭代。


三、游戏整体设计:用 React 写一个"小引擎"

整个游戏放在 src/App.jssrc/App.css 中实现,核心思想是:

  • 用 React 管理游戏状态(开始 / 进行中 / 结束、得分、渲染用的实体列表)
  • requestAnimationFrame 实现游戏循环
  • 高频变化的数据(玩家位置、子弹列表、敌机列表)用 useRef 存储,避免每帧触发 React 重渲染造成卡顿
  • 通过键盘事件控制玩家移动与发射子弹

四、核心代码拆解:从键盘到画面的一帧

1. 游戏常量与敌机配置

src/App.js 中定义游戏画布尺寸、玩家、子弹和敌机的基础属性:

js 复制代码
const GAME_WIDTH = 500;
const GAME_HEIGHT = 800;
const PLAYER_WIDTH = 40;
const PLAYER_HEIGHT = 40;
const BULLET_WIDTH = 6;
const BULLET_HEIGHT = 15;
const BULLET_SPEED = 10;
const PLAYER_SPEED = 7;

const ENEMY_TYPES = {
  small: { width: 30, height: 30, speed: 4, hp: 1, score: 100, className: 'enemy-small' },
  medium: { width: 50, height: 50, speed: 2, hp: 3, score: 300, className: 'enemy-medium' },
  large: { width: 80, height: 80, speed: 1, hp: 10, score: 1000, className: 'enemy-large' },
};

这里将不同敌机的宽高、速度、血量和得分统一配置,便于后续随机生成和统一处理。

2. React 状态与 useRef:渲染 vs 逻辑

js 复制代码
const [gameState, setGameState] = useState('start'); // start, playing, gameover
const [score, setScore] = useState(0);

// 用于渲染的状态
const [playerPos, setPlayerPos] = useState({ x: GAME_WIDTH / 2 - PLAYER_WIDTH / 2, y: GAME_HEIGHT - 100 });
const [bullets, setBullets] = useState([]);
const [enemies, setEnemies] = useState([]);

// 用于游戏逻辑的 ref(避免每帧 setState)
const playerPosRef = useRef({ x: GAME_WIDTH / 2 - PLAYER_WIDTH / 2, y: GAME_HEIGHT - 100 });
const bulletsRef = useRef([]);
const enemiesRef = useRef([]);
const scoreRef = useRef(0);
const keysPressed = useRef({});
const gameTimeRef = useRef(0);
const lastShotTime = useRef(0);
const requestRef = useRef();

心得:

  • 高频更新(每帧)的数据用 useRef 存储非常合适,不会触发 React 重渲染。
  • 每一帧更新完逻辑后,再把 ref 中的数据同步到 state,用于渲染当前帧画面,是性能和 React 响应式之间的平衡。

3. 键盘事件:方向控制 + 发射子弹

js 复制代码
useEffect(() => {
  const handleKeyDown = (e) => {
    keysPressed.current[e.code] = true;
  };
  const handleKeyUp = (e) => {
    keysPressed.current[e.code] = false;
  };

  window.addEventListener('keydown', handleKeyDown);
  window.addEventListener('keyup', handleKeyUp);

  return () => {
    window.removeEventListener('keydown', handleKeyDown);
    window.removeEventListener('keyup', handleKeyUp);
  };
}, []);
  • e.code 区分按键,如 ArrowUpArrowDownArrowLeftArrowRightSpace
  • keysPressed.current 是一个键盘状态表,游戏循环每帧读取这个表,来决定玩家是否移动或发射子弹。

4. 游戏循环:requestAnimationFrame 驱动

核心循环使用 useCallback + requestAnimationFrame

js 复制代码
const gameLoop = useCallback(() => {
  if (gameState !== 'playing') return;

  const player = playerPosRef.current;

  // 玩家移动
  if (keysPressed.current['ArrowUp']) player.y = Math.max(0, player.y - PLAYER_SPEED);
  if (keysPressed.current['ArrowDown']) player.y = Math.min(GAME_HEIGHT - PLAYER_HEIGHT, player.y + PLAYER_SPEED);
  if (keysPressed.current['ArrowLeft']) player.x = Math.max(0, player.x - PLAYER_SPEED);
  if (keysPressed.current['ArrowRight']) player.x = Math.min(GAME_WIDTH - PLAYER_WIDTH, player.x + PLAYER_SPEED);

  // 发射子弹(带冷却)
  if (keysPressed.current['Space']) {
    const now = Date.now();
    if (now - lastShotTime.current > 200) {
      bulletsRef.current.push({
        id: now,
        x: player.x + PLAYER_WIDTH / 2 - BULLET_WIDTH / 2,
        y: player.y,
      });
      lastShotTime.current = now;
    }
  }

  // 子弹向上运动
  bulletsRef.current = bulletsRef.current
    .map(b => ({ ...b, y: b.y - BULLET_SPEED }))
    .filter(b => b.y + BULLET_HEIGHT > 0);

  // 难度随时间提升
  gameTimeRef.current += 1;
  const difficultyMultiplier = 1 + Math.floor(gameTimeRef.current / 600) * 0.1;

  // 随机生成敌机
  if (Math.random() < 0.02 * difficultyMultiplier) {
    const rand = Math.random();
    let type = 'small';
    if (rand > 0.7) type = 'medium';
    if (rand > 0.9) type = 'large';

    const enemyConfig = ENEMY_TYPES[type];
    enemiesRef.current.push({
      id: Date.now() + Math.random(),
      x: Math.random() * (GAME_WIDTH - enemyConfig.width),
      y: -enemyConfig.height,
      type,
      hp: enemyConfig.hp,
      ...enemyConfig,
    });
  }

  // 敌机下落
  enemiesRef.current = enemiesRef.current.map(e => ({
    ...e,
    y: e.y + e.speed * difficultyMultiplier,
  }));

  // 碰撞检测等(后面单独展开)...

  requestRef.current = requestAnimationFrame(gameLoop);
}, [gameState]);

配合 useEffect 启动 / 停止循环:

js 复制代码
useEffect(() => {
  if (gameState === 'playing') {
    requestRef.current = requestAnimationFrame(gameLoop);
  }
  return () => cancelAnimationFrame(requestRef.current);
}, [gameState, gameLoop]);

心得:

  • requestAnimationFrame 来做游戏循环,比 setInterval 更平滑,浏览器也会自动优化。
  • React 本身不是为"每帧渲染 60 次"设计的,所以用 ref+同步到 state 的方式是比较推荐的方案。

五、敌机系统与碰撞检测:简单 AABB 盒模型

1. 敌机随机生成与难度提升

敌机采用"随机 + 权重"的方式生成:

  • 每一帧都有一定概率生成新敌机
  • 随着时间推移,这个概率和敌机速度都增加(difficultyMultiplier

三种敌机的出现概率大致为:

  • 小型机:基础概率
  • 中型机:rand > 0.7
  • 大型机:rand > 0.9

这样游戏前期小怪多,后期慢慢会出现中型和大型敌机,难度自然增加。

2. 子弹与敌机碰撞检测

使用轴对齐矩形碰撞(AABB):

js 复制代码
bulletsRef.current.forEach(b => {
  enemiesRef.current.forEach(e => {
    if (
      b.x < e.x + e.width &&
      b.x + BULLET_WIDTH > e.x &&
      b.y < e.y + e.height &&
      b.y + BULLET_HEIGHT > e.y
    ) {
      e.hp -= 1;
      b.remove = true;
      if (e.hp <= 0) {
        e.remove = true;
        scoreRef.current += e.score;
      }
    }
  });
});

bulletsRef.current = bulletsRef.current.filter(b => !b.remove);
enemiesRef.current = enemiesRef.current.filter(e => !e.remove);

心得:

  • AABB 碰撞足够简单、性能好,非常适合这种 2D 平面小项目。
  • 不需要引入物理引擎,纯 JS 就可以快速实现。

3. 玩家与敌机/底部碰撞

js 复制代码
let gameOver = false;

enemiesRef.current.forEach(e => {
  // 玩家与敌机
  if (
    player.x < e.x + e.width &&
    player.x + PLAYER_WIDTH > e.x &&
    player.y < e.y + e.height &&
    player.y + PLAYER_HEIGHT > e.y
  ) {
    gameOver = true;
  }
  // 敌机落到底部
  if (e.y > GAME_HEIGHT) {
    gameOver = true;
  }
});

if (gameOver) {
  setGameState('gameover');
  setScore(scoreRef.current);
}

规则很简单:

  • 玩家一旦被敌机碰到 -> 游戏结束
  • 任何一架敌机落到底部 -> 游戏结束

六、UI 与 CSS 动效:简单但有"游戏味道"

1. 游戏主界面结构

App.js 中返回的 JSX:

jsx 复制代码
<div className="App">
  <div className="game-container">
    <div className="score-board">Score: {score}</div>

    {gameState === 'playing' && (
      <>
        <div className="player" style={{ left: playerPos.x, top: playerPos.y }} />
        {bullets.map(b => (
          <div key={b.id} className="bullet" style={{ left: b.x, top: b.y }} />
        ))}
        {enemies.map(e => (
          <div key={e.id} className={`enemy ${e.className}`} style={{ left: e.x, top: e.y }} />
        ))}
      </>
    )}

    {gameState === 'start' && (
      <div className="start-screen">
        <h1>Plane Battle</h1>
        <p>Use Arrow Keys to Move, Space to Shoot</p>
        <button onClick={startGame}>Start Game</button>
      </div>
    )}

    {gameState === 'gameover' && (
      <div className="game-over-screen">
        <h2>Game Over</h2>
        <p>Final Score: {score}</p>
        <button onClick={startGame}>Try Again</button>
      </div>
    )}
  </div>
</div>

2. 样式与动画(App.css

仅展示部分关键样式:

css 复制代码
.App {
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #282c34;
  overflow: hidden;
}

.game-container {
  position: relative;
  width: 500px;
  height: 800px;
  background-color: #000;
  overflow: hidden;
  border: 2px solid #444;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}

/* 玩家飞机 */
.player {
  position: absolute;
  width: 40px;
  height: 40px;
  background-color: #61dafb;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
  box-shadow: 0 0 10px #61dafb;
}

/* 子弹 */
.bullet {
  position: absolute;
  width: 6px;
  height: 15px;
  background-color: #ffeb3b;
  border-radius: 3px;
}

/* 敌机不同造型 */
.enemy-small {
  width: 30px;
  height: 30px;
  background-color: #ff5722;
  border-radius: 50%;
}

.enemy-medium {
  width: 50px;
  height: 50px;
  background-color: #9c27b0;
  clip-path: polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%);
}

.enemy-large {
  width: 80px;
  height: 80px;
  background-color: #f44336;
  clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}

利用 clip-path 和阴影等简单 CSS 技巧,就能做出看起来还不错的飞机、敌机造型,整体效果干净利落。


七、在 Vibe Coding 中的实际体验

对比传统"自己敲命令 + 写代码"的方式,这次用 Vibe Coding 的体验有几个明显感受:

  1. 项目初始化更省心

    直接让 AI 执行 npx create-react-app plane-battle,并自动安装依赖、初始化项目。

  2. 核心逻辑可以"对话式"设计

    比如描述需求:"玩家用方向键移动,不超出边界;空格键发射子弹;敌机随机生成并随时间加速;碰撞后加分或游戏结束",AI 会按这个思路写出完整的 App.js,然后我再细调。

  3. 实时运行 + 自动打开浏览器
    npm start 由环境帮我执行好后,浏览器直接打开 http://localhost:3000,修改代码即刻看到效果。

  4. 更专注在"游戏设计"和"逻辑理解"上

    大量样板代码(如 useEffect 注册监听、requestAnimationFrame 结构等)由 AI 生成,我主要精力放在:

    • 游戏规则怎么更好玩
    • 敌机速度和出现概率怎么调
    • 碰撞逻辑是否严谨

八、这次实践中学到的知识点

  1. React 与高频更新的协调方式

    • 高频变化用 useRef 存储逻辑状态
    • 每帧结束后再同步一次到 useState 做渲染
  2. 键盘控制的通用模式

    • keydown / keyup 分别设置 keysPressed[code] = true/false
    • 游戏循环只关心当前按键状态,不跟事件本身耦合
  3. 简单而实用的碰撞检测(AABB)

    js 复制代码
    rect1.x < rect2.x + rect2.width &&
    rect1.x + rect1.width > rect2.x &&
    rect1.y < rect2.y + rect2.height &&
    rect1.y + rect1.height > rect2.y
  4. 难度随时间变化的设计思路

    • gameTime 计帧数或时间
    • difficultyMultiplier 统一调节敌人的生成概率和速度
  5. 前端也可以做"小型游戏引擎"

    • React 虽不是专为游戏设计,但配合 requestAnimationFrame 和合理的数据结构,也能写出小型 2D 游戏。

九、踩坑与小总结

踩坑点

  • 一开始如果所有状态都用 useState,每一帧 setState 会触发大量重渲染,导致明显卡顿。
  • 键盘事件记得在组件卸载时移除监听,否则会出现"幽灵按键"问题。
  • 敌机和子弹的 ID 要确保唯一,否则 React 渲染列表时会有警告。

总结

这次在 Vibe Coding 中完成 React 飞机大战的实践,让我同时收获了:

  • 对 React 状态管理和 useRef 的更深理解
  • 对简单游戏循环、碰撞检测的实践体验
  • 对"AI 辅助编程"的全新感受:更多精力放在思路和规则设计上,而不是重复手敲样板代码
    我觉得想提高 React 水平、同时体验下前端小游戏开发,我非常推荐尝试用类似 Vibe Coding 这样的智能编码环境,从一个小小的飞机大战开始,会非常有成就感。

十、后续可以扩展的方向

这个项目还可以继续进化:

  • 增加敌机爆炸动画、音效
  • 增加道具系统(加速、护盾、激光等)
  • 增加关卡或 BOSS 模式
  • 用 Redux / Zustand 等管理更复杂的游戏状态
相关推荐
TheNextByte12 小时前
如何将 Safari 标签转移到新 iPhone 17?
前端·iphone·safari
m0_748245922 小时前
常见状态前缀
前端·css
用户90443816324602 小时前
拒绝 `setInterval`!手撕“死了么”生命倒计时,带你看看 60FPS 下的 Web Worker 优雅多线程
前端·javascript
ttod_qzstudio2 小时前
CSS 样式优先级原则详解:从一个 Vue 组件样式冲突案例说起
前端·css·vue.js
5967851542 小时前
css装饰
前端·css·css3
摘星编程2 小时前
React Native for OpenHarmony 实战:PanResponder 手势响应详解
javascript·react native·react.js
wearegogog12310 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars10 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习