Skip to content

游戏应用实战

游戏应用是移动端最受欢迎的应用类型之一,本文将介绍如何使用 uni-app 开发简单而有趣的游戏应用。

游戏应用概述

uni-app 虽然不是专门的游戏开发框架,但对于一些轻量级的休闲游戏,完全可以胜任。使用 uni-app 开发游戏有以下优势:

  • 跨平台:一次开发,多端运行
  • 开发效率高:利用 Vue 的响应式特性
  • 丰富的 API:可以调用设备的各种能力
  • 社区支持:有大量的插件和组件可供使用

技术选型

前端技术栈

  • uni-app:跨平台开发框架
  • Vue.js:响应式数据绑定
  • Canvas:游戏渲染
  • Animation:动画效果

后端技术栈(如需要)

  • 云函数:处理游戏逻辑
  • 云数据库:存储游戏数据和排行榜
  • WebSocket:实时多人游戏

案例:贪吃蛇游戏

下面我们将实现一个经典的贪吃蛇游戏,包括游戏界面、控制逻辑和得分系统。

项目结构

├── components            // 自定义组件
│   ├── game-controller   // 游戏控制器组件
│   └── score-board       // 得分板组件
├── pages                 // 页面文件夹
│   ├── index             // 首页(游戏主界面)
│   ├── rank              // 排行榜
│   └── settings          // 游戏设置
├── static                // 静态资源
│   ├── images            // 游戏图片资源
│   └── sounds            // 游戏音效
├── utils                 // 工具函数
│   ├── game.js           // 游戏核心逻辑
│   └── storage.js        // 本地存储工具
├── App.vue               // 应用入口
├── main.js               // 主入口
├── manifest.json         // 配置文件
└── pages.json            // 页面配置

核心功能实现

1. 游戏主界面

游戏主界面包含游戏画布、控制按钮和得分显示。

vue
<template>
  <view class="game-container">
    <!-- 游戏状态提示 -->
    <view class="game-status" v-if="!isPlaying">
      <view class="status-content">
        <text class="status-title">{{ gameOver ? '游戏结束' : '贪吃蛇' }}</text>
        <text class="status-score" v-if="gameOver">得分: {{ score }}</text>
        <button class="start-btn" @tap="startGame">{{ gameOver ? '再来一局' : '开始游戏' }}</button>
      </view>
    </view>
    
    <!-- 游戏画布 -->
    <canvas 
      canvas-id="gameCanvas" 
      id="gameCanvas"
      class="game-canvas"
      @touchstart="handleTouch"
      @touchmove="handleTouch"
    ></canvas>
    
    <!-- 游戏信息 -->
    <view class="game-info">
      <view class="score-box">
        <text class="score-label">得分</text>
        <text class="score-value">{{ score }}</text>
      </view>
      <view class="high-score-box">
        <text class="score-label">最高分</text>
        <text class="score-value">{{ highScore }}</text>
      </view>
    </view>
    
    <!-- 游戏控制器 -->
    <game-controller @direction-change="changeDirection"></game-controller>
    
    <!-- 功能按钮 -->
    <view class="function-btns">
      <view class="function-btn" @tap="pauseGame">
        <text class="iconfont" :class="isPaused ? 'icon-play' : 'icon-pause'"></text>
      </view>
      <view class="function-btn" @tap="goToRank">
        <text class="iconfont icon-rank"></text>
      </view>
      <view class="function-btn" @tap="goToSettings">
        <text class="iconfont icon-settings"></text>
      </view>
    </view>
  </view>
</template>

<script>
import gameController from '@/components/game-controller/game-controller.vue';
import Game from '@/utils/game.js';
import storage from '@/utils/storage.js';

export default {
  components: {
    gameController
  },
  data() {
    return {
      game: null,
      ctx: null,
      canvasWidth: 300,
      canvasHeight: 300,
      gridSize: 15,
      isPlaying: false,
      isPaused: false,
      gameOver: false,
      score: 0,
      highScore: 0,
      gameLoop: null,
      settings: {
        speed: 150,
        soundEnabled: true,
        vibrationEnabled: true
      }
    }
  },
  onLoad() {
    // 加载设置
    this.loadSettings();
    
    // 加载最高分
    this.highScore = storage.getHighScore() || 0;
  },
  onReady() {
    // 获取画布上下文
    this.initCanvas();
  },
  onUnload() {
    // 清除游戏循环
    this.stopGame();
  },
  methods: {
    // 初始化画布
    initCanvas() {
      const query = uni.createSelectorQuery().in(this);
      query.select('#gameCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node;
          const ctx = canvas.getContext('2d');
          
          // 设置画布大小
          const dpr = uni.getSystemInfoSync().pixelRatio;
          canvas.width = res[0].width * dpr;
          canvas.height = res[0].height * dpr;
          ctx.scale(dpr, dpr);
          
          this.ctx = ctx;
          this.canvasWidth = res[0].width;
          this.canvasHeight = res[0].height;
          
          // 初始化游戏
          this.game = new Game({
            ctx,
            width: this.canvasWidth,
            height: this.canvasHeight,
            gridSize: this.gridSize,
            speed: this.settings.speed,
            onScore: this.updateScore,
            onGameOver: this.handleGameOver
          });
          
          // 绘制初始界面
          this.game.drawBackground();
        });
    },
    
    // 开始游戏
    startGame() {
      if (this.game) {
        this.game.init();
        this.isPlaying = true;
        this.isPaused = false;
        this.gameOver = false;
        this.score = 0;
        
        // 启动游戏循环
        this.runGameLoop();
        
        // 播放开始音效
        if (this.settings.soundEnabled) {
          this.playSound('start');
        }
      }
    },
    
    // 暂停游戏
    pauseGame() {
      if (!this.isPlaying || this.gameOver) return;
      
      this.isPaused = !this.isPaused;
      
      if (this.isPaused) {
        clearInterval(this.gameLoop);
      } else {
        this.runGameLoop();
      }
    },
    
    // 停止游戏
    stopGame() {
      clearInterval(this.gameLoop);
      this.isPlaying = false;
    },
    
    // 运行游戏循环
    runGameLoop() {
      clearInterval(this.gameLoop);
      this.gameLoop = setInterval(() => {
        if (!this.isPaused && this.isPlaying) {
          this.game.update();
          this.game.draw();
        }
      }, this.settings.speed);
    },
    
    // 更新分数
    updateScore(newScore) {
      this.score = newScore;
      
      // 播放得分音效
      if (this.settings.soundEnabled) {
        this.playSound('score');
      }
      
      // 震动反馈
      if (this.settings.vibrationEnabled) {
        uni.vibrateShort();
      }
    },
    
    // 处理游戏结束
    handleGameOver() {
      this.gameOver = true;
      this.isPlaying = false;
      clearInterval(this.gameLoop);
      
      // 更新最高分
      if (this.score > this.highScore) {
        this.highScore = this.score;
        storage.setHighScore(this.highScore);
      }
      
      // 保存分数到排行榜
      storage.addScoreToRank({
        score: this.score,
        date: new Date().toISOString(),
        duration: this.game.getDuration()
      });
      
      // 播放结束音效
      if (this.settings.soundEnabled) {
        this.playSound('gameover');
      }
      
      // 震动反馈
      if (this.settings.vibrationEnabled) {
        uni.vibrateLong();
      }
    },
    
    // 改变蛇的方向
    changeDirection(direction) {
      if (this.game && this.isPlaying && !this.isPaused) {
        this.game.changeDirection(direction);
      }
    },
    
    // 处理触摸事件
    handleTouch(e) {
      if (!this.isPlaying || this.isPaused) return;
      
      const touch = e.touches[0];
      const lastTouch = this.lastTouch;
      
      if (lastTouch) {
        const deltaX = touch.clientX - lastTouch.clientX;
        const deltaY = touch.clientY - lastTouch.clientY;
        
        // 判断滑动方向
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
          // 水平滑动
          if (deltaX > 0) {
            this.changeDirection('right');
          } else {
            this.changeDirection('left');
          }
        } else {
          // 垂直滑动
          if (deltaY > 0) {
            this.changeDirection('down');
          } else {
            this.changeDirection('up');
          }
        }
      }
      
      this.lastTouch = touch;
    },
    
    // 播放音效
    playSound(type) {
      const soundMap = {
        start: '/static/sounds/start.mp3',
        score: '/static/sounds/score.mp3',
        gameover: '/static/sounds/gameover.mp3'
      };
      
      const soundUrl = soundMap[type];
      if (soundUrl) {
        const innerAudioContext = uni.createInnerAudioContext();
        innerAudioContext.src = soundUrl;
        innerAudioContext.play();
      }
    },
    
    // 加载设置
    loadSettings() {
      const settings = storage.getSettings();
      if (settings) {
        this.settings = { ...this.settings, ...settings };
      }
    },
    
    // 跳转到排行榜
    goToRank() {
      uni.navigateTo({
        url: '/pages/rank/rank'
      });
    },
    
    // 跳转到设置页面
    goToSettings() {
      uni.navigateTo({
        url: '/pages/settings/settings'
      });
    }
  }
}
</script>

<style lang="scss">
.game-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 30rpx;
  
  .game-status {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.7);
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    
    .status-content {
      background-color: #fff;
      border-radius: 20rpx;
      padding: 40rpx;
      display: flex;
      flex-direction: column;
      align-items: center;
      
      .status-title {
        font-size: 40rpx;
        font-weight: bold;
        margin-bottom: 20rpx;
      }
      
      .status-score {
        font-size: 32rpx;
        margin-bottom: 30rpx;
      }
      
      .start-btn {
        background-color: #1aad19;
        color: #fff;
        border-radius: 40rpx;
        padding: 0 60rpx;
      }
    }
  }
  
  .game-canvas {
    width: 100%;
    height: 750rpx;
    background-color: #f5f5f5;
    border: 2rpx solid #ddd;
    border-radius: 10rpx;
    margin-bottom: 30rpx;
  }
  
  .game-info {
    display: flex;
    width: 100%;
    margin-bottom: 30rpx;
    
    .score-box, .high-score-box {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 20rpx;
      background-color: #f9f9f9;
      border-radius: 10rpx;
      
      &:first-child {
        margin-right: 20rpx;
      }
      
      .score-label {
        font-size: 28rpx;
        color: #666;
        margin-bottom: 10rpx;
      }
      
      .score-value {
        font-size: 40rpx;
        font-weight: bold;
        color: #333;
      }
    }
  }
  
  .function-btns {
    display: flex;
    justify-content: center;
    margin-top: 30rpx;
    
    .function-btn {
      width: 80rpx;
      height: 80rpx;
      border-radius: 50%;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 0 20rpx;
      
      .iconfont {
        font-size: 40rpx;
        color: #333;
      }
      
      &:active {
        background-color: #e0e0e0;
      }
    }
  }
}
</style>

2. 游戏控制器组件

游戏控制器组件提供方向控制按钮,让用户可以控制蛇的移动方向。

vue
<template>
  <view class="game-controller">
    <view class="direction-pad">
      <view class="direction-btn up" @tap="changeDirection('up')">
        <text class="iconfont icon-up"></text>
      </view>
      <view class="direction-row">
        <view class="direction-btn left" @tap="changeDirection('left')">
          <text class="iconfont icon-left"></text>
        </view>
        <view class="direction-btn center"></view>
        <view class="direction-btn right" @tap="changeDirection('right')">
          <text class="iconfont icon-right"></text>
        </view>
      </view>
      <view class="direction-btn down" @tap="changeDirection('down')">
        <text class="iconfont icon-down"></text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  methods: {
    changeDirection(direction) {
      this.$emit('direction-change', direction);
    }
  }
}
</script>

<style lang="scss">
.game-controller {
  width: 100%;
  
  .direction-pad {
    display: flex;
    flex-direction: column;
    align-items: center;
    
    .direction-row {
      display: flex;
      align-items: center;
    }
    
    .direction-btn {
      width: 100rpx;
      height: 100rpx;
      border-radius: 50%;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 10rpx;
      
      .iconfont {
        font-size: 40rpx;
        color: #333;
      }
      
      &:active {
        background-color: #e0e0e0;
      }
      
      &.center {
        background-color: transparent;
      }
    }
  }
}
</style>

3. 游戏核心逻辑

游戏核心逻辑封装在 game.js 中,包括蛇的移动、食物生成、碰撞检测等功能。

javascript
// utils/game.js
export default class Game {
  constructor(options) {
    this.ctx = options.ctx;
    this.width = options.width;
    this.height = options.height;
    this.gridSize = options.gridSize || 15;
    this.speed = options.speed || 150;
    this.onScore = options.onScore || function() {};
    this.onGameOver = options.onGameOver || function() {};
    
    this.snake = [];
    this.food = null;
    this.direction = 'right';
    this.nextDirection = 'right';
    this.score = 0;
    this.startTime = 0;
    
    // 计算网格数量
    this.gridWidth = Math.floor(this.width / this.gridSize);
    this.gridHeight = Math.floor(this.height / this.gridSize);
  }
  
  // 初始化游戏
  init() {
    // 初始化蛇
    this.snake = [
      { x: 3, y: 1 },
      { x: 2, y: 1 },
      { x: 1, y: 1 }
    ];
    
    // 初始化方向
    this.direction = 'right';
    this.nextDirection = 'right';
    
    // 初始化分数
    this.score = 0;
    
    // 生成食物
    this.generateFood();
    
    // 记录开始时间
    this.startTime = Date.now();
  }
  
  // 更新游戏状态
  update() {
    // 更新方向
    this.direction = this.nextDirection;
    
    // 获取蛇头
    const head = { ...this.snake[0] };
    
    // 根据方向移动蛇头
    switch (this.direction) {
      case 'up':
        head.y -= 1;
        break;
      case 'down':
        head.y += 1;
        break;
      case 'left':
        head.x -= 1;
        break;
      case 'right':
        head.x += 1;
        break;
    }
    
    // 检查是否撞墙
    if (head.x < 0 || head.x >= this.gridWidth || head.y < 0 || head.y >= this.gridHeight) {
      this.onGameOver();
      return;
    }
    
    // 检查是否撞到自己
    for (let i = 0; i < this.snake.length; i++) {
      if (this.snake[i].x === head.x && this.snake[i].y === head.y) {
        this.onGameOver();
        return;
      }
    }
    
    // 将新蛇头添加到蛇身前面
    this.snake.unshift(head);
    
    // 检查是否吃到食物
    if (head.x === this.food.x && head.y === this.food.y) {
      // 增加分数
      this.score += 10;
      this.onScore(this.score);
      
      // 生成新食物
      this.generateFood();
    } else {
      // 如果没有吃到食物,移除蛇尾
      this.snake.pop();
    }
  }
  
  // 绘制游戏
  draw() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.width, this.height);
    
    // 绘制背景
    this.drawBackground();
    
    // 绘制食物
    this.drawFood();
    
    // 绘制蛇
    this.drawSnake();
  }
  
  // 绘制背景
  drawBackground() {
    this.ctx.fillStyle = '#f5f5f5';
    this.ctx.fillRect(0, 0, this.width, this.height);
    
    // 绘制网格
    this.ctx.strokeStyle = '#e0e0e0';
    this.ctx.lineWidth = 0.5;
    
    // 绘制垂直线
    for (let x = 0; x <= this.width; x += this.gridSize) {
      this.ctx.beginPath();
      this.ctx.moveTo(x, 0);
      this.ctx.lineTo(x, this.height);
      this.ctx.stroke();
    }
    
    // 绘制水平线
    for (let y = 0; y <= this.height; y += this.gridSize) {
      this.ctx.beginPath();
      this.ctx.moveTo(0, y);
      this.ctx.lineTo(this.width, y);
      this.ctx.stroke();
    }
  }
  
  // 绘制食物
  drawFood() {
    if (!this.food) return;
    
    this.ctx.fillStyle = '#ff0000';
    this.ctx.beginPath();
    const centerX = this.food.x * this.gridSize + this.gridSize / 2;
    const centerY = this.food.y * this.gridSize + this.gridSize / 2;
    const radius = this.gridSize / 2 * 0.8;
    this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
    this.ctx.fill();
  }
  
  // 绘制蛇
  drawSnake() {
    // 绘制蛇身
    for (let i = 1; i < this.snake.length; i++) {
      const segment = this.snake[i];
      this.ctx.fillStyle = '#4caf50';
      this.ctx.fillRect(
        segment.x * this.gridSize,
        segment.y * this.gridSize,
        this.gridSize,
        this.gridSize
      );
    }
    
    // 绘制蛇头
    const head = this.snake[0];
    this.ctx.fillStyle = '#388e3c';
    this.ctx.fillRect(
      head.x * this.gridSize,
      head.y * this.gridSize,
      this.gridSize,
      this.gridSize
    );
    
    // 绘制蛇眼
    this.ctx.fillStyle = '#ffffff';
    
    const eyeSize = this.gridSize / 5;
    let leftEyeX, leftEyeY, rightEyeX, rightEyeY;
    
    switch (this.direction) {
      case 'up':
        leftEyeX = head.x * this.gridSize + this.gridSize / 4;
        leftEyeY = head.y * this.gridSize + this.gridSize / 4;
        rightEyeX = head.x * this.gridSize + this.gridSize * 3 / 4;
        rightEyeY = head.y * this.gridSize + this.gridSize / 4;
        break;
      case 'down':
        leftEyeX = head.x * this.gridSize + this.gridSize / 4;
        leftEyeY = head.y * this.gridSize + this.gridSize * 3 / 4;
        rightEyeX = head.x * this.gridSize + this.gridSize * 3 / 4;
        rightEyeY = head.y * this.gridSize + this.gridSize * 3 / 4;
        break;
      case 'left':
        leftEyeX = head.x * this.gridSize + this.gridSize / 4;
        leftEyeY = head.y * this.gridSize + this.gridSize / 4;
        rightEyeX = head.x * this.gridSize + this.gridSize / 4;
        rightEyeY = head.y * this.gridSize + this.gridSize * 3 / 4;
        break;
      case 'right':
        leftEyeX = head.x * this.gridSize + this.gridSize * 3 / 4;
        leftEyeY = head.y * this.gridSize + this.gridSize / 4;
        rightEyeX = head.x * this.gridSize + this.gridSize * 3 / 4;
        rightEyeY = head.y * this.gridSize + this.gridSize * 3 / 4;
        break;
    }
    
    this.ctx.beginPath();
    this.ctx.arc(leftEyeX, leftEyeY, eyeSize, 0, Math.PI * 2);
    this.ctx.fill();
    
    this.ctx.beginPath();
    this.ctx.arc(rightEyeX, rightEyeY, eyeSize, 0, Math.PI * 2);
    this.ctx.fill();
  }
  
  // 生成食物
  generateFood() {
    // 创建一个包含所有可能位置的数组
    const availablePositions = [];
    
    for (let x = 0; x < this.gridWidth; x++) {
      for (let y = 0; y < this.gridHeight; y++) {
        // 检查该位置是否被蛇占用
        let isOccupied = false;
        for (let i = 0; i < this.snake.length; i++) {
          if (this.snake[i].x === x && this.snake[i].y === y) {
            isOccupied = true;
            break;
          }
        }
        
        if (!isOccupied) {
          availablePositions.push({ x, y });
        }
      }
    }
    
    // 随机选择一个可用位置
    if (availablePositions.length > 0) {
      const randomIndex = Math.floor(Math.random() * availablePositions.length);
      this.food = availablePositions[randomIndex];
    }
  }
  
  // 改变方向
  changeDirection(newDirection) {
    // 防止180度转弯
    if (
      (this.direction === 'up' && newDirection === 'down') ||
      (this.direction === 'down' && newDirection === 'up') ||
      (this.direction === 'left' && newDirection === 'right') ||
      (this.direction === 'right' && newDirection === 'left')
    ) {
      return;
    }
    
    this.nextDirection = newDirection;
  }
  
  // 获取游戏持续时间(秒)
  getDuration() {
    return Math.floor((Date.now() - this.startTime) / 1000);
  }
}

4. 排行榜页面

排行榜页面展示玩家的历史最高分。

vue
<template>
  <view class="rank-page">
    <view class="header">
      <text class="title">排行榜</text>
    </view>
    
    <view class="empty-tip" v-if="rankList.length === 0">
      <text class="iconfont icon-empty"></text>
      <text>暂无排行数据</text>
    </view>
    
    <scroll-view scroll-y class="rank-list" v-else>
      <view class="rank-header">
        <text class="rank-column rank">排名</text>
        <text class="rank-column score">分数</text>
        <text class="rank-column date">日期</text>
        <text class="rank-column duration">用时</text>
      </view>
      
      <view 
        class="rank-item" 
        v-for="(item, index) in rankList" 
        :key="index"
      >
        <text class="rank-column rank">{{ index + 1 }}</text>
        <text class="rank-column score">{{ item.score }}</text>
        <text class="rank-column date">{{ formatDate(item.date) }}</text>
        <text class="rank-column duration">{{ formatDuration(item.duration) }}</text>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import storage from '@/utils/storage.js';

export default {
  data() {
    return {
      rankList: []
    }
  },
  onLoad() {
    this.loadRankList();
  },
  methods: {
    // 加载排行榜数据
    loadRankList() {
      const rankList = storage.getRankList() || [];
      // 按分数降序排序
      this.rankList = rankList.sort((a, b) => b.score - a.score);
    },
    
    // 格式化日期
    formatDate(dateString) {
      const date = new Date(dateString);
      return `${date.getMonth() + 1}/${date.getDate()}`;
    },
    
    // 格式化时长
    formatDuration(seconds) {
      const minutes = Math.floor(seconds / 60);
      const remainingSeconds = seconds % 60;
      return `${minutes}:${String(remainingSeconds).padStart(2, '0')}`;
    }
  }
}
</script>

<style lang="scss">
.rank-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  
  .header {
    padding: 30rpx;
    background-color: #f5f5f5;
    border-bottom: 1rpx solid #eee;
    
    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333;
    }
  }
  
  .empty-tip {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: #999;
    
    .iconfont {
      font-size: 80rpx;
      margin-bottom: 20rpx;
    }
  }
  
  .rank-list {
    flex: 1;
    
    .rank-header {
      display: flex;
      padding: 20rpx;
      background-color: #f9f9f9;
      border-bottom: 1rpx solid #eee;
      font-weight: bold;
    }
    
    .rank-item {
      display: flex;
      padding: 20rpx;
      border-bottom: 1rpx solid #f5f5f5;
      
      &:nth-child(odd) {
        background-color: #fafafa;
      }
    }
    
    .rank-column {
      &.rank {
        width: 15%;
        text-align: center;
      }
      
      &.score {
        width: 25%;
        text-align: center;
        font-weight: bold;
      }
      
      &.date {
        width: 30%;
        text-align: center;
      }
      
      &.duration {
        width: 30%;
        text-align: center;
      }
    }
  }
}
</style>

4. 应用优化与最佳实践

4.1 性能优化

游戏应用对性能要求较高,以下是一些优化建议:

Canvas 渲染优化

javascript
// 优化 Canvas 渲染
function optimizeCanvasRendering(canvas, ctx) {
  // 使用 requestAnimationFrame 代替 setInterval
  let animationId;
  
  function gameLoop(timestamp) {
    // 更新游戏状态
    game.update();
    
    // 渲染游戏
    game.draw();
    
    // 继续循环
    animationId = requestAnimationFrame(gameLoop);
  }
  
  // 启动游戏循环
  function startGameLoop() {
    animationId = requestAnimationFrame(gameLoop);
  }
  
  // 停止游戏循环
  function stopGameLoop() {
    cancelAnimationFrame(animationId);
  }
  
  // 减少重绘区域
  function drawPartial(x, y, width, height) {
    // 只重绘需要更新的部分
    ctx.clearRect(x, y, width, height);
    // 绘制更新的内容
  }
  
  return {
    startGameLoop,
    stopGameLoop,
    drawPartial
  };
}

内存管理

javascript
// 内存管理优化
function optimizeMemoryUsage() {
  // 对象池模式,减少对象创建和垃圾回收
  const particlePool = [];
  const maxParticles = 100;
  
  // 初始化对象池
  for (let i = 0; i < maxParticles; i++) {
    particlePool.push({
      x: 0,
      y: 0,
      active: false,
      // 其他属性
    });
  }
  
  // 获取一个可用的粒子对象
  function getParticle() {
    for (let i = 0; i < maxParticles; i++) {
      if (!particlePool[i].active) {
        particlePool[i].active = true;
        return particlePool[i];
      }
    }
    return null; // 池已满
  }
  
  // 释放粒子对象
  function releaseParticle(particle) {
    particle.active = false;
    // 重置其他属性
  }
  
  return {
    getParticle,
    releaseParticle
  };
}

4.2 游戏音效与震动

游戏音效和震动可以提升游戏体验,以下是实现方式:

javascript
// 音效管理器
const SoundManager = {
  sounds: {},
  
  // 预加载音效
  preload(soundMap) {
    Object.keys(soundMap).forEach(key => {
      const sound = uni.createInnerAudioContext();
      sound.src = soundMap[key];
      sound.autoplay = false;
      this.sounds[key] = sound;
    });
  },
  
  // 播放音效
  play(name) {
    if (this.sounds[name]) {
      // 重新播放前先停止并重置
      this.sounds[name].stop();
      this.sounds[name].seek(0);
      this.sounds[name].play();
    }
  },
  
  // 停止音效
  stop(name) {
    if (this.sounds[name]) {
      this.sounds[name].stop();
    }
  },
  
  // 释放资源
  release() {
    Object.values(this.sounds).forEach(sound => {
      sound.destroy();
    });
    this.sounds = {};
  }
};

// 震动管理器
const VibrationManager = {
  // 短震动
  short() {
    uni.vibrateShort({
      success: () => {
        console.log('短震动成功');
      },
      fail: (err) => {
        console.error('短震动失败', err);
      }
    });
  },
  
  // 长震动
  long() {
    uni.vibrateLong({
      success: () => {
        console.log('长震动成功');
      },
      fail: (err) => {
        console.error('长震动失败', err);
      }
    });
  }
};

4.3 本地存储工具

本地存储工具用于保存游戏设置、最高分和排行榜数据。

javascript
// utils/storage.js
export default {
  // 保存最高分
  setHighScore(score) {
    uni.setStorageSync('snake_high_score', score);
  },
  
  // 获取最高分
  getHighScore() {
    return uni.getStorageSync('snake_high_score');
  },
  
  // 保存游戏设置
  setSettings(settings) {
    uni.setStorageSync('snake_settings', settings);
  },
  
  // 获取游戏设置
  getSettings() {
    return uni.getStorageSync('snake_settings');
  },
  
  // 添加分数到排行榜
  addScoreToRank(scoreData) {
    let rankList = this.getRankList() || [];
    
    // 添加新分数
    rankList.push(scoreData);
    
    // 按分数降序排序
    rankList.sort((a, b) => b.score - a.score);
    
    // 只保留前20名
    if (rankList.length > 20) {
      rankList = rankList.slice(0, 20);
    }
    
    uni.setStorageSync('snake_rank_list', rankList);
  },
  
  // 获取排行榜
  getRankList() {
    return uni.getStorageSync('snake_rank_list');
  },
  
  // 清除排行榜
  clearRankList() {
    uni.removeStorageSync('snake_rank_list');
  }
};

5. 总结与拓展

5.1 开发要点总结

  1. Canvas 性能优化:游戏开发中,Canvas 渲染性能至关重要,应当合理控制重绘区域,使用 requestAnimationFrame 代替 setInterval。

  2. 状态管理:游戏状态管理需要清晰,包括游戏初始化、运行、暂停、结束等状态的切换。

  3. 用户体验:游戏控制需要简单直观,音效和震动反馈可以提升游戏体验。

  4. 数据持久化:使用本地存储保存游戏设置、最高分和排行榜数据,提升用户粘性。

  5. 跨端适配:使用 uni-app 开发游戏时,需要考虑不同平台的差异,如触控方式、屏幕尺寸等。

5.2 功能拓展方向

  1. 多人对战:通过 WebSocket 实现实时多人对战功能,增加游戏的社交性和竞争性。

  2. 关卡设计:设计不同难度的关卡,增加游戏的挑战性和可玩性。

  3. 成就系统:设计游戏成就系统,激励用户完成各种挑战。

  4. 皮肤系统:提供多种游戏皮肤,增加游戏的个性化和收益点。

  5. 社交分享:集成社交分享功能,让用户可以分享游戏成绩,扩大游戏影响力。

5.3 商业化思路

  1. 内购道具:提供游戏内购买,如特殊能力、皮肤、关卡等。

  2. 广告变现:在游戏中适当位置插入广告,如游戏结束页面、暂停页面等。

  3. 会员订阅:提供会员订阅服务,会员可以获得更多游戏特权。

  4. 游戏联盟:与其他游戏开发者合作,形成游戏联盟,互相推广。

  5. 赛事活动:举办游戏比赛,吸引更多用户参与,提高用户活跃度。

参考资源

一次开发,多端部署 - 让跨平台开发更简单