boxmoe_header_banner_img

加载中

断网小恐龙游戏完全开发指南:用现代化前端技术复刻经典


从零开始,使用 React + TypeScript + Tailwind CSS + Framer Motion 打造属于你的 Chrome Dino Game


前言:为什么每个人都爱这只小恐龙?

还记得那个没有网络连接的午后,当 Chrome 浏览器告诉你”无法连接到互联网”时,一只可爱的小恐龙出现在屏幕前,按一下空格键,一场冒险就此开始。这个简单的游戏,却陪伴了全球数十亿用户度过了无数个断网时刻。

今天,我们将用最现代化的前端技术栈,从零开始复刻这个经典游戏。这不仅是一次技术练习,更是对游戏机制、动画原理和前端架构的深入探索。

本文亮点:

· ✅ 完整的游戏逻辑实现(碰撞检测、物理引擎、难度曲线)
· ✅ 现代化技术栈(React 18 + TypeScript + Tailwind CSS + Framer Motion)
· ✅ 像素级还原(包括奔跑、跳跃、蹲伏动画)
· ✅ 无任何第三方游戏引擎依赖
· ✅ 完整的代码示例,可直接运行


一、技术栈选型与项目初始化

1.1 核心技术栈

技术 版本 用途
React 18.2+ UI 框架,组件化开发
TypeScript 5.x 类型安全,减少运行时错误
Tailwind CSS 3.x 原子化样式,快速布局
Framer Motion 10.x 流畅动画和手势交互
Vite 5.x 极速构建工具

1.2 项目初始化

# 创建项目
npm create vite@latest dino-game -- --template react-ts

# 进入项目目录
cd dino-game

# 安装依赖
npm install

# 安装 Tailwind CSS 和 Framer Motion
npm install -D tailwindcss postcss autoprefixer
npm install framer-motion

# 初始化 Tailwind CSS
npx tailwindcss init -p

1.3 配置文件

tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        'dino-black': '#535353',
        'dino-bg': '#f7f7f7',
        'cactus': '#4caf50',
      },
      fontFamily: {
        'pixel': ['"Press Start 2P"', 'monospace'],
      },
      keyframes: {
        'walk': {
          '0%, 100%': { backgroundPosition: '0 0' },
          '50%': { backgroundPosition: '-48px 0' }
        },
        'ground-move': {
          '0%': { backgroundPosition: '0 0' },
          '100%': { backgroundPosition: '-48px 0' }
        },
        'cloud-move': {
          '0%': { transform: 'translateX(100%)' },
          '100%': { transform: 'translateX(-100%)' }
        }
      },
      animation: {
        'walk': 'walk 0.2s steps(2) infinite',
        'ground-move': 'ground-move 0.2s linear infinite',
        'cloud-move': 'cloud-move 20s linear infinite'
      }
    },
  },
  plugins: [],
}

src/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

@layer base {
  body {
    @apply bg-gradient-to-br from-gray-100 to-gray-200 min-h-screen flex items-center justify-center;
    font-family: 'Press Start 2P', monospace;
  }
}

@layer components {
  .game-container {
    @apply relative bg-white rounded-2xl shadow-2xl overflow-hidden;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  }
  
  .game-canvas {
    @apply block bg-dino-bg cursor-pointer;
    image-rendering: crisp-edges;
    image-rendering: pixelated;
  }
}

二、游戏核心逻辑设计

2.1 游戏状态与类型定义

src/types/game.ts

// 游戏状态枚举
export enum GameState {
  IDLE = 'idle',           // 待机状态
  RUNNING = 'running',     // 运行中
  GAME_OVER = 'game_over', // 游戏结束
  PAUSED = 'paused'        // 暂停状态
}

// 恐龙状态枚举
export enum DinoState {
  RUNNING = 'running',     // 奔跑
  JUMPING = 'jumping',     // 跳跃
  DUCKING = 'ducking'      // 蹲伏
}

// 障碍物类型枚举
export enum ObstacleType {
  SMALL_CACTUS = 'small',   // 小型仙人掌
  LARGE_CACTUS = 'large',   // 大型仙人掌
  BIRD = 'bird'             // 飞鸟
}

// 障碍物接口
export interface Obstacle {
  id: number;
  type: ObstacleType;
  x: number;                // X轴坐标
  width: number;            // 宽度
  height: number;           // 高度
  y: number;                // Y轴坐标
}

// 游戏配置接口
export interface GameConfig {
  gravity: number;          // 重力加速度
  jumpForce: number;        // 跳跃初速度
  groundY: number;          // 地面Y坐标
  gameSpeed: number;        // 游戏速度
  speedIncrement: number;   // 速度增量
  maxSpeed: number;         // 最大速度
  score: number;            // 当前分数
  highScore: number;        // 最高分
}

// 游戏状态接口
export interface GameStateType {
  status: GameState;
  dino: {
    state: DinoState;
    y: number;
    velocityY: number;
    isOnGround: boolean;
  };
  obstacles: Obstacle[];
  config: GameConfig;
  frame: number;
}

2.2 游戏主组件实现

src/App.tsx

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GameState, DinoState, ObstacleType, GameStateType } from './types/game';

// 游戏常量定义
const GROUND_Y = 280;
const DINO_WIDTH = 48;
const DINO_HEIGHT = 48;
const DINO_DUCK_HEIGHT = 32;
const GRAVITY = 0.8;
const JUMP_FORCE = -12;
const INITIAL_SPEED = 5;
const MAX_SPEED = 15;
const SPEED_INCREMENT = 0.002;
const OBSTACLE_SPAWN_INTERVAL = 2000;
const MIN_OBSTACLE_INTERVAL = 800;

// 恐龙精灵帧(像素风格的8-bit恐龙)
const DINO_SPRITES = {
  running: [
    '🐱‍🏍', // 奔跑帧1
    '🐱‍👤'  // 奔跑帧2
  ],
  jumping: '🦖',
  ducking: '🐱‍👓',
  idle: '🦕'
};
function App() {
  // 游戏状态
  const [gameState, setGameState] = useState<GameStateType['status']>(GameState.IDLE);
  const [dinoState, setDinoState] = useState<DinoState>(DinoState.RUNNING);
  const [dinoY, setDinoY] = useState(GROUND_Y - DINO_HEIGHT);
  const [velocityY, setVelocityY] = useState(0);
  const [isOnGround, setIsOnGround] = useState(true);
  const [obstacles, setObstacles] = useState<GameStateType['obstacles']>([]);
  const [score, setScore] = useState(0);
  const [highScore, setHighScore] = useState(() => {
    const saved = localStorage.getItem('dino_highscore');
    return saved ? parseInt(saved, 10) : 0;
  });
  const [gameSpeed, setGameSpeed] = useState(INITIAL_SPEED);
  const [frame, setFrame] = useState(0);
  const [isDucking, setIsDucking] = useState(false);

  // Refs
  const gameLoopRef = useRef<number>();
  const lastTimestampRef = useRef<number>(0);
  const spawnTimerRef = useRef<NodeJS.Timeout>();

  // 重置游戏
  const resetGame = useCallback(() => {
    setGameState(GameState.RUNNING);
    setDinoState(DinoState.RUNNING);
    setDinoY(GROUND_Y - DINO_HEIGHT);
    setVelocityY(0);
    setIsOnGround(true);
    setObstacles([]);
    setScore(0);
    setGameSpeed(INITIAL_SPEED);
    setIsDucking(false);
  }, []);

  // 跳跃逻辑
  const jump = useCallback(() => {
    if (!isOnGround) return;
    if (gameState !== GameState.RUNNING) return;
    
    setVelocityY(JUMP_FORCE);
    setIsOnGround(false);
    setDinoState(DinoState.JUMPING);
    
    // 播放跳跃音效(使用Web Audio API)
    const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
    const oscillator = audioContext.createOscillator();
    const gainNode = audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);
    
    oscillator.frequency.value = 880;
    gainNode.gain.value = 0.1;
    
    oscillator.start();
    gainNode.gain.exponentialRampToValueAtTime(0.00001, audioContext.currentTime + 0.3);
    oscillator.stop(audioContext.currentTime + 0.3);
  }, [isOnGround, gameState]);

  // 蹲伏逻辑
  const duck = useCallback(() => {
    if (!isOnGround) return;
    if (gameState !== GameState.RUNNING) return;
    
    setIsDucking(true);
    setDinoState(DinoState.DUCKING);
    setDinoY(GROUND_Y - DINO_DUCK_HEIGHT);
  }, [isOnGround, gameState]);

  // 站立逻辑
  const stand = useCallback(() => {
    if (!isDucking) return;
    
    setIsDucking(false);
    setDinoState(DinoState.RUNNING);
    setDinoY(GROUND_Y - DINO_HEIGHT);
  }, [isDucking]);

  // 碰撞检测
  const checkCollision = useCallback(() => {
    const dinoHitbox = {
      x: 80, // 恐龙固定X位置
      y: dinoY,
      width: DINO_WIDTH,
      height: isDucking ? DINO_DUCK_HEIGHT : DINO_HEIGHT
    };

    for (const obstacle of obstacles) {
      const obstacleHitbox = {
        x: obstacle.x,
        y: obstacle.y,
        width: obstacle.width,
        height: obstacle.height
      };

      // AABB碰撞检测
      if (dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.width &&
          dinoHitbox.x + dinoHitbox.width > obstacleHitbox.x &&
          dinoHitbox.y < obstacleHitbox.y + obstacleHitbox.height &&
          dinoHitbox.y + dinoHitbox.height > obstacleHitbox.y) {
        return true; // 发生碰撞
      }
    }
    return false;
  }, [dinoY, isDucking, obstacles]);
// 生成障碍物
  const spawnObstacle = useCallback(() => {
    if (gameState !== GameState.RUNNING) return;

    const random = Math.random();
    let type: ObstacleType;
    let width: number;
    let height: number;
    let y: number;

    if (random < 0.7) {
      // 70%概率生成仙人掌
      const isLarge = Math.random() < 0.3;
      if (isLarge) {
        type = ObstacleType.LARGE_CACTUS;
        width = 32;
        height = 56;
        y = GROUND_Y - height;
      } else {
        type = ObstacleType.SMALL_CACTUS;
        width = 24;
        height = 40;
        y = GROUND_Y - height;
      }
    } else {
      // 30%概率生成飞鸟
      type = ObstacleType.BIRD;
      width = 40;
      height = 32;
      y = GROUND_Y - 80; // 飞鸟在空中
    }

    setObstacles(prev => [...prev, {
      id: Date.now(),
      type,
      x: window.innerWidth,
      width,
      height,
      y
    }]);
  }, [gameState]);

  // 更新障碍物位置
  const updateObstacles = useCallback(() => {
    setObstacles(prev => {
      const updated = prev
        .map(obs => ({
          ...obs,
          x: obs.x - gameSpeed
        }))
        .filter(obs => obs.x + obs.width > 0); // 移除超出屏幕的障碍物
      
      // 当障碍物通过恐龙时增加分数
      const passedCount = prev.filter(obs => obs.x + obs.width < 80 && obs.x > 0).length;
      if (passedCount > 0) {
        setScore(s => s + passedCount * 10);
      }
      
      return updated;
    });
  }, [gameSpeed]);

  // 更新恐龙物理
  const updateDinoPhysics = useCallback(() => {
    if (!isOnGround) {
      const newVelocityY = velocityY + GRAVITY;
      const newY = dinoY + newVelocityY;
      
      if (newY >= GROUND_Y - (isDucking ? DINO_DUCK_HEIGHT : DINO_HEIGHT)) {
        setDinoY(GROUND_Y - (isDucking ? DINO_DUCK_HEIGHT : DINO_HEIGHT));
        setVelocityY(0);
        setIsOnGround(true);
        setDinoState(isDucking ? DinoState.DUCKING : DinoState.RUNNING);
      } else {
        setDinoY(newY);
        setVelocityY(newVelocityY);
      }
    }
  }, [dinoY, isOnGround, velocityY, isDucking]);
// 更新分数和速度
  const updateScoreAndSpeed = useCallback(() => {
    setScore(prev => prev + 0.1);
    setGameSpeed(prev => {
      const newSpeed = prev + SPEED_INCREMENT;
      return Math.min(newSpeed, MAX_SPEED);
    });
  }, []);

  // 主游戏循环
  const gameLoop = useCallback(() => {
    if (gameState !== GameState.RUNNING) return;

    updateDinoPhysics();
    updateObstacles();
    updateScoreAndSpeed();
    setFrame(prev => prev + 1);

    // 碰撞检测
    if (checkCollision()) {
      setGameState(GameState.GAME_OVER);
      if (Math.floor(score) > highScore) {
        setHighScore(Math.floor(score));
        localStorage.setItem('dino_highscore', Math.floor(score).toString());
      }
    }
  }, [gameState, updateDinoPhysics, updateObstacles, updateScoreAndSpeed, checkCollision, score, highScore]);

  // 启动游戏循环
  useEffect(() => {
    if (gameState === GameState.RUNNING) {
      const interval = setInterval(gameLoop, 1000 / 60); // 60 FPS
      return () => clearInterval(interval);
    }
  }, [gameState, gameLoop]);

  // 障碍物生成定时器
  useEffect(() => {
    if (gameState === GameState.RUNNING) {
      const spawnInterval = Math.max(
        MIN_OBSTACLE_INTERVAL,
        OBSTACLE_SPAWN_INTERVAL - (gameSpeed - INITIAL_SPEED) * 50
      );
      
      spawnTimerRef.current = setInterval(() => {
        spawnObstacle();
      }, spawnInterval);
      
      return () => {
        if (spawnTimerRef.current) {
          clearInterval(spawnTimerRef.current);
        }
      };
    }
  }, [gameState, gameSpeed, spawnObstacle]);
// 键盘事件监听
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.code === 'Space' || e.code === 'ArrowUp') {
        e.preventDefault();
        
        if (gameState === GameState.IDLE) {
          resetGame();
        } else if (gameState === GameState.GAME_OVER) {
          resetGame();
        } else if (gameState === GameState.RUNNING) {
          jump();
        }
      } else if (e.code === 'ArrowDown') {
        e.preventDefault();
        if (gameState === GameState.RUNNING) {
          duck();
        }
      }
    };
    
    const handleKeyUp = (e: KeyboardEvent) => {
      if (e.code === 'ArrowDown') {
        e.preventDefault();
        stand();
      }
    };
    
    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, [gameState, jump, duck, stand, resetGame]);
// 触摸事件(移动端支持)
  useEffect(() => {
    const handleTouchStart = (e: TouchEvent) => {
      e.preventDefault();
      const touch = e.touches[0];
      const screenHeight = window.innerHeight;
      const isBottomHalf = touch.clientY > screenHeight / 2;
      
      if (isBottomHalf) {
        duck();
      } else {
        if (gameState === GameState.IDLE || gameState === GameState.GAME_OVER) {
          resetGame();
        } else {
          jump();
        }
      }
    };
    
    const handleTouchEnd = (e: TouchEvent) => {
      e.preventDefault();
      stand();
    };
    
    window.addEventListener('touchstart', handleTouchStart);
    window.addEventListener('touchend', handleTouchEnd);
    
    return () => {
      window.removeEventListener('touchstart', handleTouchStart);
      window.removeEventListener('touchend', handleTouchEnd);
    };
  }, [gameState, jump, duck, stand, resetGame]);

  // 获取恐龙显示字符
  const getDinoChar = () => {
    if (gameState === GameState.IDLE) return DINO_SPRITES.idle;
    if (dinoState === DinoState.JUMPING) return DINO_SPRITES.jumping;
    if (dinoState === DinoState.DUCKING) return DINO_SPRITES.ducking;
    return DINO_SPRITES.running[Math.floor(frame / 10) % 2];
  };

  return (
    <div className="relative w-full max-w-4xl">
      {/* 游戏容器 */}
      <div className="game-container relative">
        {/* 游戏画布 */}
        <div 
          className="game-canvas relative w-full h-[400px] overflow-hidden bg-gradient-to-b from-gray-50 to-gray-100"
          style={{ cursor: 'pointer' }}
          onClick={() => {
            if (gameState === GameState.IDLE || gameState === GameState.GAME_OVER) {
              resetGame();
            } else if (gameState === GameState.RUNNING) {
              jump();
            }
          }}
        >
{/* 天空背景 */}
          <div className="absolute inset-0 pointer-events-none">
            <div className="absolute top-8 left-1/2 text-xs text-gray-400 animate-cloud-move whitespace-nowrap">
              ☁️ ☁️ ☁️
            </div>
          </div>
          
          {/* 分数显示 */}
          <div className="absolute top-4 right-6 text-right">
            <div className="text-sm text-gray-600">
              <span className="font-pixel">SCORE</span>
              <span className="ml-2 font-pixel text-lg">{Math.floor(score)}</span>
            </div>
            <div className="text-xs text-gray-400 mt-1">
              <span className="font-pixel">BEST</span>
              <span className="ml-2 font-pixel">{highScore}</span>
            </div>
          </div>
          
          {/* 速度指示器(难度显示) */}
          <div className="absolute top-4 left-6 text-xs text-gray-400 font-pixel">
            SPEED: {(gameSpeed / INITIAL_SPEED).toFixed(1)}x
          </div>
          
          {/* 恐龙 */}
          <motion.div
            className="absolute text-6xl select-none"
            style={{
              left: '80px',
              top: `${dinoY}px`,
              transform: dinoState === DinoState.JUMPING ? 'scale(1.1)' : 'scale(1)',
              filter: dinoState === DinoState.JUMPING ? 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' : 'none'
            }}
            animate={{
              y: dinoState === DinoState.JUMPING ? [0, -5, 0] : 0,
              rotate: dinoState === DinoState.JUMPING ? [-5, 5, -5] : 0
            }}
            transition={{ duration: 0.1, repeat: dinoState === DinoState.JUMPING ? Infinity : 0 }}
          >
            {getDinoChar()}
          </motion.div>
          
          {/* 障碍物 */}
          <AnimatePresence>
            {obstacles.map(obstacle => (
              <motion.div
                key={obstacle.id}
                className="absolute text-4xl select-none"
                style={{
                  left: `${obstacle.x}px`,
                  top: `${obstacle.y}px`,
                }}
                initial={{ x: 100, opacity: 0 }}
                animate={{ x: 0, opacity: 1 }}
                exit={{ x: -100, opacity: 0 }}
                transition={{ duration: 0.1 }}
              >
                {obstacle.type === ObstacleType.SMALL_CACTUS && '🌵'}
                {obstacle.type === ObstacleType.LARGE_CACTUS && '🌵🌵'}
                {obstacle.type === ObstacleType.BIRD && '🐦'}
              </motion.div>
            ))}
          </AnimatePresence>
          
          {/* 地面 */}
          <div 
            className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-r from-gray-700 to-gray-600"
            style={{ top: `${GROUND_Y}px` }}
          >
            <div className="absolute inset-0 bg-repeat-x animate-ground-move" 
                 style={{ backgroundImage: 'repeating-linear-gradient(90deg, #4a4a4a 0px, #4a4a4a 12px, #6b6b6b 12px, #6b6b6b 24px)' }} />
          </div>
          
          {/* 游戏状态覆盖层 */}
          <AnimatePresence>
            {gameState === GameState.IDLE && (
              <motion.div 
                className="absolute inset-0 bg-black/50 flex items-center justify-center backdrop-blur-sm"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
              >
                <div className="text-center bg-white rounded-2xl p-8 shadow-2xl">
                  <div className="text-7xl mb-4">🦖</div>
                  <h2 className="text-2xl font-pixel text-gray-800 mb-2">DINO GAME</h2>
                  <p className="text-sm text-gray-500 mb-4">Press SPACE or Click to Start</p>
                  <div className="flex gap-4 text-xs text-gray-400">
                    <span>⬆️ Jump</span>
                    <span>⬇️ Duck</span>
                  </div>
                </div>
              </motion.div>
            )}
{gameState === GameState.GAME_OVER && (
              <motion.div 
                className="absolute inset-0 bg-black/50 flex items-center justify-center backdrop-blur-sm"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
              >
                <div className="text-center bg-white rounded-2xl p-8 shadow-2xl">
                  <div className="text-7xl mb-4">💀</div>
                  <h2 className="text-2xl font-pixel text-gray-800 mb-2">GAME OVER</h2>
                  <p className="text-sm text-gray-600 mb-2">Score: {Math.floor(score)}</p>
                  <p className="text-sm text-gray-500 mb-4">Best: {highScore}</p>
                  <button 
                    onClick={resetGame}
                    className="px-6 py-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-lg font-pixel text-sm hover:shadow-lg transition-all"
                  >
                    TRY AGAIN
                  </button>
                </div>
              </motion.div>
            )}
            
            {gameState === GameState.PAUSED && (
              <motion.div 
                className="absolute inset-0 bg-black/50 flex items-center justify-center backdrop-blur-sm"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
              >
                <div className="text-center bg-white rounded-2xl p-8 shadow-2xl">
                  <div className="text-6xl mb-4">⏸️</div>
                  <h2 className="text-2xl font-pixel text-gray-800">PAUSED</h2>
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      </div>
      
      {/* 游戏说明 */}
      <div className="mt-6 text-center text-sm text-gray-500 font-pixel">
        <div className="flex justify-center gap-8 mb-2">
          <div className="flex items-center gap-2">
            <kbd className="px-2 py-1 bg-gray-800 text-white rounded text-xs">SPACE</kbd>
            <span>or</span>
            <kbd className="px-2 py-1 bg-gray-800 text-white rounded text-xs">↑</kbd>
            <span>Jump</span>
          </div>
          <div className="flex items-center gap-2">
            <kbd className="px-2 py-1 bg-gray-800 text-white rounded text-xs">↓</kbd>
            <span>Duck</span>
          </div>
        </div>
        <p className="text-xs">Mobile: Tap top half to jump • Tap bottom half to duck</p>
      </div>
    </div>
  );
}

export default App;

三、高级功能扩展

3.1 添加白天/黑夜模式切换

// 在 App 组件中添加
const [isDarkMode, setIsDarkMode] = useState(false);

// 添加切换函数
const toggleTheme = useCallback(() => {
  setIsDarkMode(prev => !prev);
}, []);

// 修改游戏容器背景
<div className={`game-canvas relative w-full h-[400px] overflow-hidden transition-colors duration-300 ${
  isDarkMode ? 'bg-gradient-to-b from-gray-800 to-gray-900' : 'bg-gradient-to-b from-gray-50 to-gray-100'
}`}>
  {/* 添加主题切换按钮 */}
  <button
    onClick={toggleTheme}
    className="absolute bottom-4 right-4 z-10 p-2 rounded-full bg-white/20 backdrop-blur-sm text-sm"
  >
    {isDarkMode ? '☀️' : '🌙'}
  </button>
</div>

3.2 添加音效系统

// 创建音效管理器
class SoundManager {
  private audioContext: AudioContext;
  
  constructor() {
    this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
  }
  
  playJump() {
    const oscillator = this.audioContext.createOscillator();
    const gainNode = this.audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(this.audioContext.destination);
    
    oscillator.frequency.value = 880;
    gainNode.gain.value = 0.1;
    
    oscillator.start();
    gainNode.gain.exponentialRampToValueAtTime(0.00001, this.audioContext.currentTime + 0.3);
    oscillator.stop(this.audioContext.currentTime + 0.3);
  }
  
  playGameOver() {
    const oscillator = this.audioContext.createOscillator();
    const gainNode = this.audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(this.audioContext.destination);
    
    oscillator.frequency.value = 440;
    gainNode.gain.value = 0.15;
    
    oscillator.start();
    gainNode.gain.exponentialRampToValueAtTime(0.00001, this.audioContext.currentTime + 0.5);
    oscillator.stop(this.audioContext.currentTime + 0.5);
  }
  
  playScore() {
    const oscillator = this.audioContext.createOscillator();
    const gainNode = this.audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(this.audioContext.destination);
    
    oscillator.frequency.value = 660;
    gainNode.gain.value = 0.05;
    
    oscillator.start();
    gainNode.gain.exponentialRampToValueAtTime(0.00001, this.audioContext.currentTime + 0.1);
    oscillator.stop(this.audioContext.currentTime + 0.1);
  }
}
// 在组件中使用
const soundManager = useRef(new SoundManager());

// 在跳跃时播放音效
const jump = useCallback(() => {
  if (!isOnGround) return;
  if (gameState !== GameState.RUNNING) return;
  
  soundManager.current.playJump();
  setVelocityY(JUMP_FORCE);
  setIsOnGround(false);
  setDinoState(DinoState.JUMPING);
}, [isOnGround, gameState]);

// 在游戏结束时播放
if (checkCollision()) {
  soundManager.current.playGameOver();
  setGameState(GameState.GAME_OVER);
}

3.3 添加粒子效果系统

// 创建粒子组件
const ParticleEffect: React.FC<{ x: number; y: number; onComplete: () => void }> = ({ x, y, onComplete }) => {
  const particles = Array.from({ length: 12 }, (_, i) => ({
    id: i,
    angle: (i / 12) * Math.PI * 2,
    distance: Math.random() * 30 + 10,
    size: Math.random() * 6 + 2
  }));
  
  return (
    <AnimatePresence>
      {particles.map(particle => (
        <motion.div
          key={particle.id}
          className="absolute w-2 h-2 bg-yellow-500 rounded-full"
          style={{ left: x, top: y }}
          initial={{ scale: 1, opacity: 1 }}
          animate={{
            x: Math.cos(particle.angle) * particle.distance,
            y: Math.sin(particle.angle) * particle.distance,
            scale: 0,
            opacity: 0
          }}
          transition={{ duration: 0.5 }}
          onAnimationComplete={onComplete}
        />
      ))}
    </AnimatePresence>
  );
};

// 在恐龙跳跃时添加粒子效果
const [showParticles, setShowParticles] = useState(false);
const [particlePos, setParticlePos] = useState({ x: 0, y: 0 });

const jump = useCallback(() => {
  if (!isOnGround) return;
  if (gameState !== GameState.RUNNING) return;
  
  setParticlePos({ x: 100, y: dinoY + DINO_HEIGHT });
  setShowParticles(true);
  setTimeout(() => setShowParticles(false), 500);
  
  // ... 原有跳跃逻辑
}, [isOnGround, gameState, dinoY]);

四、性能优化与最佳实践

4.1 使用 useCallback 和 useMemo 优化渲染

// 缓存碰撞检测函数
const checkCollision = useCallback(() => {
  // ... 碰撞检测逻辑
}, [dinoY, isDucking, obstacles]);

// 缓存分数显示
const displayScore = useMemo(() => Math.floor(score), [score]);

// 缓存速度显示
const speedMultiplier = useMemo(() => (gameSpeed / INITIAL_SPEED).toFixed(1), [gameSpeed]);

4.2 使用 requestAnimationFrame 替代 setInterval

useEffect(() => {
  if (gameState !== GameState.RUNNING) return;
  
  let animationId: number;
  let lastTime = performance.now();
  
  const animate = (currentTime: number) => {
    const deltaTime = Math.min(100, currentTime - lastTime);
    
    if (deltaTime >= 16) { // 约60 FPS
      gameLoop();
      lastTime = currentTime;
    }
    
    animationId = requestAnimationFrame(animate);
  };
  
  animationId = requestAnimationFrame(animate);
  
  return () => {
    if (animationId) {
      cancelAnimationFrame(animationId);
    }
  };
}, [gameState, gameLoop]);

4.3 虚拟化障碍物列表

// 使用 useMemo 优化障碍物渲染
const visibleObstacles = useMemo(() => {
  return obstacles.filter(obs => obs.x + obs.width > 0 && obs.x < window.innerWidth);
}, [obstacles]);

五、完整代码清单

5.1 项目结构

dino-game/
├── src/
│   ├── App.tsx              # 主游戏组件
│   ├── main.tsx             # 入口文件
│   ├── index.css            # 全局样式
│   ├── types/
│   │   └── game.ts          # TypeScript类型定义
│   ├── hooks/
│   │   ├── useGameLoop.ts   # 游戏循环Hook
│   │   └── useKeyboard.ts   # 键盘事件Hook
│   └── utils/
│       ├── collision.ts     # 碰撞检测工具
│       └── sound.ts         # 音效管理
├── public/
│   └── favicon.ico
├── index.html
├── package.json
├── vite.config.ts
├── tailwind.config.js
└── tsconfig.json

5.2 运行项目

# 安装依赖
npm install

# 启动开发服务器
npm run dev

# 构建生产版本
npm run build

# 预览生产版本
npm run preview

六、常见问题与解决方案

Q1: 跳跃手感不流畅怎么办?

解决方案: 调整重力常数和跳跃初速度

const GRAVITY = 0.8;      // 重力加速度
const JUMP_FORCE = -12;    // 跳跃初速度

Q2: 障碍物生成间隔不均匀?

解决方案: 使用动态间隔,根据游戏速度调整

const spawnInterval = Math.max(
  MIN_OBSTACLE_INTERVAL,
  OBSTACLE_SPAWN_INTERVAL - (gameSpeed - INITIAL_SPEED) * 50
);

Q3: 移动端触摸不灵敏?

解决方案: 使用 touchstart 和 touchend 事件,并区分点击区域

const handleTouchStart = (e: TouchEvent) => {
  const touch = e.touches[0];
  const isBottomHalf = touch.clientY > window.innerHeight / 2;
  
  if (isBottomHalf) {
    duck();
  } else {
    jump();
  }
};

七、总结

通过本教程,我们使用现代化的前端技术栈(React + TypeScript + Tailwind CSS + Framer Motion)完整复刻了经典的 Chrome Dino Game。项目实现了:

✅ 完整的游戏机制

· 重力物理系统
· 精确的碰撞检测
· 动态难度曲线
· 分数和最高分记录

✅ 流畅的动画效果

· 恐龙奔跑、跳跃、蹲伏动画
· 障碍物移动动画
· 游戏状态切换动画

✅ 良好的用户体验

· 键盘和触摸双支持
· 响应式设计
· 音效反馈(可选)
· 粒子特效(可选)

✅ 优秀的代码质量

· TypeScript 类型安全
· 组件化架构
· 性能优化
· 可扩展性设计

这个项目不仅是一个游戏,更是一个完整的前端架构示例。你可以在此基础上继续扩展,添加更多的障碍物类型、道具系统、关卡系统,甚至将它集成到你的个人网站中。

扩展建议:

· 添加更多恐龙皮肤
· 实现无尽模式排行榜
· 添加成就系统
· 集成 WebSocket 实现多人对战

现在,打开浏览器,按下空格键,和你的小恐龙一起奔跑吧!🦖💨


文章纯原创 请不要侵权 by yuyu的博客 可能有部署语法错误 还请包容。

上一次更新已经跑远了✨ 计算中...
(‾◡◝) 本内容里的一些消息,可能已经跟不上时间啦~
感谢您的支持
微信赞赏

微信扫一扫

支付宝赞赏

支付宝扫一扫



评论(0)

查看评论列表

暂无评论


发表评论

北京时间 (Asia/Shanghai)

定位中...
🌤️
--°C
加载中...
体感: --°C
湿度: --%
2026 年 3 月
 12
3456789
10111213141516
17181920212223
2425262728  

已阻挡的垃圾评论

后退
前进
刷新
复制
粘贴
全选
删除
返回首页

💿 音乐控制窗口

🎼 歌词

🪗 歌曲信息

封面

🎚️ 播放控制

🎶 播放进度

00:00 00:00

🔊 音量控制

100%

📋 歌单

0%
目录
顶部
底部
📖 文章导读