小程序游戏开发:使用Canvas实现简单游戏

小程序游戏开发:使用Canvas实现简单游戏

关键词:小程序开发、Canvas、2D游戏、游戏循环、碰撞检测

摘要:本文将带您从0到1用微信小程序的Canvas开发一个简单的2D游戏。我们会用”小球吃星星”作为实战案例,从核心概念讲解到代码实现,一步步拆解小程序游戏开发的关键技术点。即使您是刚接触游戏开发的新手,也能通过这篇文章掌握Canvas绘图、游戏循环控制、碰撞检测等核心技能。


背景介绍

目的和范围

随着微信小程序用户突破12亿,基于小程序的轻量级游戏成为开发者的新赛道。本文聚焦”如何用小程序的Canvas API开发简单2D游戏”,覆盖从环境搭建到完整游戏实现的全流程,适合想入门小程序游戏开发的开发者。

预期读者

有基础的小程序开发者(了解wxml/wxss/js基本语法)
对2D游戏开发感兴趣的前端工程师
想尝试轻量级游戏开发的独立开发者

文档结构概述

本文采用”概念讲解→原理分析→实战开发”的递进结构:先理解Canvas在小程序中的特性,再掌握游戏循环、碰撞检测等核心机制,最后通过”小球吃星星”案例完整实现一个可运行的游戏。

术语表

核心术语定义

Canvas:HTML5提供的2D绘图API,小程序中通过wx.createCanvasContext调用
游戏循环:游戏的”心跳”,包含”更新→绘制→等待”三个阶段的无限循环
碰撞检测:判断两个游戏对象是否接触的算法(本文用矩形包围盒检测)
精灵(Sprite):游戏中的可移动对象(如本文的小球和星星)

缩略词列表

API:Application Programming Interface(应用程序接口)
FPS:Frames Per Second(每秒帧数,衡量游戏流畅度)


核心概念与联系

故事引入:画家的”动态画卷”

想象你是一个古代画家,皇帝让你画一幅会动的画卷:里面有一颗会跑的小球,还有会随机出现的星星,小球碰到星星星星就会消失。你会怎么做?

首先需要一张能反复修改的”魔法画布”(对应Canvas)
然后需要一个”秒表”控制每秒钟画多少张新画(对应游戏循环)
最后需要一个”裁判”判断小球有没有碰到星星(对应碰撞检测)

这三个工具,就是我们开发小程序游戏的核心。

核心概念解释(像给小学生讲故事)

1. Canvas:会”擦除重画”的魔法画布
小程序的Canvas就像一块特殊的画布,你可以用它画圆、画矩形、写文字。但和普通画布不同的是,它有”自动擦除”功能——每次调用ctx.clearRect()就能清空之前的画,然后重新画新内容。就像你用白板笔在白板上画画,画完一页后用板擦一擦,又能画新的一页。

2. 游戏循环:控制”翻页速度”的秒表
我们的游戏画面不是一张静态图,而是由很多张图片快速切换形成的(就像电影的帧)。游戏循环就是控制每秒钟切换多少张图片的机制。小程序中可以用requestAnimationFrame实现,它会告诉浏览器:“下一次重绘前帮我执行这个函数”,这样就能保证画面流畅。

3. 碰撞检测:判断”有没有碰到”的裁判
游戏里经常需要判断两个物体是否接触(比如小球有没有吃到星星)。最简单的方法是”矩形包围盒检测”:给每个物体画一个虚拟的矩形框,只要两个矩形框有重叠,就认为它们碰撞了。就像判断两个盒子有没有叠在一起,只需要看它们的上下左右边是否交叉。

核心概念之间的关系(用小学生能理解的比喻)

Canvas和游戏循环的关系:Canvas是画家的画布,游戏循环是画家的秒表。秒表每滴答一次(比如每16ms),画家就擦干净画布(clearRect),然后在画布上画新的内容(小球的新位置、星星的新位置)。

游戏循环和碰撞检测的关系:游戏循环是”导演”,每次循环都会先让”裁判”(碰撞检测)检查有没有发生碰撞,然后根据结果调整游戏状态(比如星星消失、分数增加),最后再让画家(Canvas)画出新画面。

Canvas和碰撞检测的关系:Canvas负责把物体画在正确的位置(记录每个物体的x/y坐标),碰撞检测则用这些坐标计算是否碰撞。就像你在纸上画了两个气球,然后用尺子量它们的位置,判断有没有碰到。

核心概念原理和架构的文本示意图

游戏主循环
├─ 更新阶段(Update)
│  ├─ 处理用户输入(如触摸移动)
│  ├─ 更新物体位置(小球移动、星星移动)
│  └─ 碰撞检测(判断是否吃到星星)
├─ 绘制阶段(Draw)
│  ├─ 清空画布(clearRect)
│  ├─ 绘制背景
│  ├─ 绘制小球
│  └─ 绘制星星
└─ 循环控制(requestAnimationFrame)

Mermaid 流程图

graph TD
    A[游戏开始] --> B[进入游戏循环]
    B --> C[更新阶段]
    C --> D[处理触摸输入]
    D --> E[更新物体位置]
    E --> F[碰撞检测]
    F --> G[更新游戏状态(如分数)]
    G --> H[绘制阶段]
    H --> I[清空画布]
    I --> J[绘制背景]
    J --> K[绘制小球]
    K --> L[绘制星星]
    L --> M[调用requestAnimationFrame]
    M --> B

核心算法原理 & 具体操作步骤

1. Canvas基础操作(小程序版)

小程序的Canvas分为”传统Canvas”(canvas组件)和”自定义组件Canvas”(wx.createCanvasContext),本文使用更灵活的后者。

关键API说明

wx.createCanvasContext('canvasId'):创建画布上下文(相当于拿到画笔)
ctx.beginPath():开始绘制路径
ctx.arc(x, y, r, sAngle, eAngle):画圆(x/y圆心坐标,r半径)
ctx.fillStyle:设置填充颜色
ctx.fill():填充路径
ctx.clearRect(x, y, width, height):清空指定区域(x/y起点,width/height尺寸)

示例:用Canvas画一个红色小球

// 在Page的onLoad中初始化
onLoad() {
            
  this.ctx = wx.createCanvasContext('gameCanvas'); // 获取画布上下文
  this.drawBall(100, 100); // 在(100,100)位置画小球
},

drawBall(x, y) {
            
  this.ctx.beginPath();
  this.ctx.arc(x, y, 20, 0, 2 * Math.PI); // 圆心(100,100),半径20
  this.ctx.fillStyle = '#FF4D4F'; // 红色
  this.ctx.fill();
  this.ctx.draw(); // 提交绘制命令
}

2. 游戏循环实现

游戏循环的核心是通过requestAnimationFrame实现”更新→绘制”的无限循环,目标是保持60FPS(每16ms执行一次)。

代码结构

startGameLoop() {
            
  // 更新阶段:处理逻辑
  this.update();
  
  // 绘制阶段:渲染画面
  this.draw();
  
  // 循环调用(告诉浏览器下一次重绘前执行)
  this.animationId = requestAnimationFrame(() => this.startGameLoop());
}

// 游戏停止时需要取消循环(避免内存泄漏)
stopGameLoop() {
            
  cancelAnimationFrame(this.animationId);
}

3. 碰撞检测(矩形包围盒算法)

假设小球和星星都是矩形(即使它们画成圆形,也可以用外接矩形检测),判断两个矩形是否相交的条件:

{ 小球左边界 < 星星右边界 小球右边界 > 星星左边界 小球上边界 < 星星下边界 小球下边界 > 星星上边界 egin{cases} 小球左边界 < 星星右边界 \ 小球右边界 > 星星左边界 \ 小球上边界 < 星星下边界 \ 小球下边界 > 星星上边界 end{cases} ⎩

⎧​小球左边界<星星右边界小球右边界>星星左边界小球上边界<星星下边界小球下边界>星星上边界​

代码实现

// 定义物体的结构(x,y为中心点坐标,width/height为尺寸)
const ball = {
             x: 100, y: 100, width: 40, height: 40 };
const star = {
             x: 200, y: 200, width: 30, height: 30 };

// 计算边界
const ballLeft = ball.x - ball.width/2;
const ballRight = ball.x + ball.width/2;
const ballTop = ball.y - ball.height/2;
const ballBottom = ball.y + ball.height/2;

const starLeft = star.x - star.width/2;
const starRight = star.x + star.width/2;
const starTop = star.y - star.height/2;
const starBottom = star.y + star.height/2;

// 判断碰撞
const isCollide = (
  ballLeft < starRight &&
  ballRight > starLeft &&
  ballTop < starBottom &&
  ballBottom > starTop
);

数学模型和公式 & 详细讲解 & 举例说明

1. 物体移动的速度向量

小球的移动可以用速度向量(dx, dy)表示,每次更新位置时:
x = x + d x y = y + d y x = x + dx \ y = y + dy x=x+dxy=y+dy
例如:小球初始位置(100,100),速度(5,3),则1秒后(假设60FPS,执行60次更新)的位置是:
x = 100 + 5 ∗ 60 = 400 y = 100 + 3 ∗ 60 = 280 x = 100 + 5*60 = 400 \ y = 100 + 3*60 = 280 x=100+5∗60=400y=100+3∗60=280

2. 随机生成星星的位置

为了让星星在画布内随机出现,需要计算有效范围(假设画布宽375px,高667px,星星尺寸30px):
x m i n = 15 ( 星星左边界不超出画布左边缘 ) x m a x = 375 − 15 = 360 ( 星星右边界不超出画布右边缘 ) y m i n = 15 y m a x = 667 − 15 = 652 x_{min} = 15 quad (星星左边界不超出画布左边缘) \ x_{max} = 375 – 15 = 360 quad (星星右边界不超出画布右边缘) \ y_{min} = 15 \ y_{max} = 667 – 15 = 652 xmin​=15(星星左边界不超出画布左边缘)xmax​=375−15=360(星星右边界不超出画布右边缘)ymin​=15ymax​=667−15=652
Math.random()生成随机坐标:
x = x m i n + M a t h . r a n d o m ( ) ∗ ( x m a x − x m i n ) y = y m i n + M a t h . r a n d o m ( ) ∗ ( y m a x − y m i n ) x = x_{min} + Math.random()*(x_{max} – x_{min}) \ y = y_{min} + Math.random()*(y_{max} – y_{min}) x=xmin​+Math.random()∗(xmax​−xmin​)y=ymin​+Math.random()∗(ymax​−ymin​)


项目实战:代码实际案例和详细解释说明(小球吃星星游戏)

开发环境搭建

下载安装微信开发者工具
新建小程序项目(选择”不使用云服务”,后端服务暂时不需要)
pages目录下新建game页面(会自动生成game.wxmlgame.wxssgame.jsgame.json四个文件)

源代码详细实现和代码解读

1. wxml结构(游戏界面)
<!-- pages/game/game.wxml -->
<view class="container">
  <!-- 游戏画布(注意canvasId要和js中对应) -->
  <canvas canvas-id="gameCanvas" class="game-canvas" bindtouchmove="onTouchMove" />
  
  <!-- 分数显示 -->
  <view class="score">分数:{
           {score}}</view>
</view>
2. wxss样式(布局和样式)
/* pages/game/game.wxss */
.container {
            
  position: relative;
  width: 100%;
  height: 100vh;
}

.game-canvas {
            
  width: 100%;
  height: 100%;
  background: #F0F2F5; /* 浅灰色背景 */
}

.score {
            
  position: absolute;
  top: 20rpx;
  left: 20rpx;
  font-size: 32rpx;
  color: #333;
  font-weight: bold;
}
3. js逻辑(核心游戏代码)
// pages/game/game.js
Page({
            
  data: {
            
    score: 0, // 当前分数
    ball: {
             x: 187.5, y: 333.5, r: 20, color: '#FF4D4F' }, // 小球(圆心x/y,半径,颜色)
    stars: [], // 星星数组(每个元素包含x/y/r/color)
    canvasWidth: 375, // 画布宽度(根据手机屏幕适配)
    canvasHeight: 667, // 画布高度
    starSize: 15, // 星星半径
    starColor: '#FFD700', // 星星颜色
    isGameRunning: false // 游戏是否运行中
  },

  onLoad() {
            
    // 获取设备屏幕尺寸(适配不同手机)
    wx.getSystemInfo({
            
      success: (res) => {
            
        this.setData({
            
          canvasWidth: res.windowWidth,
          canvasHeight: res.windowHeight
        });
        // 初始化小球位置(屏幕中心)
        this.data.ball.x = res.windowWidth / 2;
        this.data.ball.y = res.windowHeight / 2;
      }
    });

    // 创建画布上下文
    this.ctx = wx.createCanvasContext('gameCanvas');
  },

  // 开始游戏(绑定到页面的"开始游戏"按钮,这里简化为onShow时自动开始)
  startGame() {
            
    this.setData({
             score: 0, stars: [] }); // 重置分数和星星
    this.data.isGameRunning = true;
    this.generateStar(); // 生成第一个星星
    this.startGameLoop(); // 启动游戏循环
  },

  // 生成随机星星
  generateStar() {
            
    const {
             canvasWidth, canvasHeight, starSize, starColor } = this.data;
    const star = {
            
      x: Math.random() * (canvasWidth - 2 * starSize) + starSize, // x范围:[starSize, canvasWidth - starSize]
      y: Math.random() * (canvasHeight - 2 * starSize) + starSize, // y范围:[starSize, canvasHeight - starSize]
      r: starSize,
      color: starColor
    };
    this.data.stars.push(star);
  },

  // 游戏循环(更新→绘制)
  startGameLoop() {
            
    if (!this.data.isGameRunning) return;

    // 更新阶段
    this.update();
    
    // 绘制阶段
    this.draw();
    
    // 递归调用(保持60FPS)
    this.animationId = requestAnimationFrame(() => this.startGameLoop());
  },

  // 更新游戏状态
  update() {
            
    // 碰撞检测:检查小球和所有星星
    const {
             ball, stars } = this.data;
    const newStars = stars.filter(star => {
            
      // 计算小球和星星的包围盒
      const ballLeft = ball.x - ball.r;
      const ballRight = ball.x + ball.r;
      const ballTop = ball.y - ball.r;
      const ballBottom = ball.y + ball.r;

      const starLeft = star.x - star.r;
      const starRight = star.x + star.r;
      const starTop = star.y - star.r;
      const starBottom = star.y + star.r;

      // 判断是否碰撞
      const isCollide = (
        ballLeft < starRight &&
        ballRight > starLeft &&
        ballTop < starBottom &&
        ballBottom > starTop
      );

      if (isCollide) {
            
        this.setData({
             score: this.data.score + 1 }); // 分数+1
        this.generateStar(); // 生成新星星
      }
      return !isCollide; // 保留未碰撞的星星
    });
    this.data.stars = newStars;
  },

  // 绘制所有物体
  draw() {
            
    const {
             ctx, ball, stars, canvasWidth, canvasHeight } = this.data;

    // 清空画布(覆盖整个屏幕)
    ctx.clearRect(0, 0, canvasWidth, canvasHeight);

    // 绘制小球
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.r, 0, 2 * Math.PI);
    ctx.fillStyle = ball.color;
    ctx.fill();

    // 绘制所有星星
    stars.forEach(star => {
            
      ctx.beginPath();
      ctx.arc(star.x, star.y, star.r, 0, 2 * Math.PI);
      ctx.fillStyle = star.color;
      ctx.fill();
    });

    // 提交绘制命令
    ctx.draw();
  },

  // 处理触摸移动事件(控制小球移动)
  onTouchMove(e) {
            
    const {
             touches } = e;
    if (touches.length > 0) {
            
      // 将触摸坐标赋值给小球(限制在画布内)
      const x = Math.max(this.data.ball.r, Math.min(touches[0].x, this.data.canvasWidth - this.data.ball.r));
      const y = Math.max(this.data.ball.r, Math.min(touches[0].y, this.data.canvasHeight - this.data.ball.r));
      this.data.ball.x = x;
      this.data.ball.y = y;
    }
  },

  onUnload() {
            
    // 页面卸载时停止循环(避免内存泄漏)
    this.data.isGameRunning = false;
    cancelAnimationFrame(this.animationId);
  }
});

代码解读与分析

初始化:在onLoad中获取屏幕尺寸,初始化小球位置到屏幕中心
游戏循环:通过requestAnimationFrame实现60FPS的更新绘制循环
碰撞检测:使用矩形包围盒算法过滤被吃掉的星星,同时增加分数并生成新星星
触摸控制:通过bindtouchmove事件监听手指移动,实时更新小球位置(限制在画布内防止越界)
绘制逻辑:每次循环先清空画布,再重新绘制小球和所有未被吃掉的星星


实际应用场景

轻量级游戏:如”跳一跳”、”飞机大战”等,适合用Canvas快速实现2D玩法
互动营销活动:品牌可以开发H5小游戏嵌入小程序,通过Canvas实现抽奖、拼图等互动效果
教育类应用:用Canvas开发数学/物理可视化工具(如小球碰撞模拟、几何图形绘制)
数据可视化:虽然不是游戏,但Canvas的高性能绘图能力可以用于实时数据图表(如股票走势)


工具和资源推荐

官方文档:微信小程序Canvas文档(必看!)
学习网站:MDN Canvas教程(理解HTML5 Canvas的通用特性)
性能优化:小程序性能优化指南(避免Canvas绘制卡顿)
扩展框架:LayaAir(支持小程序的游戏引擎,适合复杂游戏开发)


未来发展趋势与挑战

趋势

WebGL支持:小程序已逐步开放WebGL能力(如wx.createWebGLContext),未来可以开发3D游戏
跨端复用:基于uni-app等框架,Canvas代码可以同时运行在微信/支付宝/抖音等多端小程序
AI互动:结合小程序的AI能力(如手势识别、图像识别),开发更智能的游戏交互

挑战

性能限制:小程序的JS引擎和渲染线程分离,频繁的Canvas绘制可能导致卡顿(需优化绘制频率)
跨设备适配:不同手机的屏幕尺寸、GPU性能差异大,需要做好坐标适配和性能降级
用户留存:轻量级游戏用户流失快,需要设计更吸引人的玩法和社交分享机制


总结:学到了什么?

核心概念回顾

Canvas:小程序的2D绘图工具,通过createCanvasContext操作
游戏循环:通过requestAnimationFrame实现”更新→绘制”的无限循环,保证画面流畅
碰撞检测:用矩形包围盒算法判断物体是否接触,是游戏逻辑的关键

概念关系回顾

Canvas负责”画”,游戏循环负责”控制画的频率”,碰撞检测负责”判断画中的物体是否互动”。三者就像电影的”摄影师”(Canvas)、“导演”(游戏循环)和”动作指导”(碰撞检测),共同完成游戏的呈现。


思考题:动动小脑筋

现在的游戏中,星星只会被生成一次(吃到一个生成一个)。如果想让星星每隔2秒自动生成一个(不管是否被吃到),应该怎么修改代码?(提示:使用setInterval

小球现在只能通过触摸移动,如果想增加”自动移动+重力感应控制”的混合模式,需要用到小程序的哪些API?(提示:wx.onAccelerometerChange

游戏的分数现在只是显示数字,如果想添加”吃到星星时弹出+1动画”,应该如何用Canvas实现?(提示:记录动画的位置和透明度,在绘制阶段额外绘制)


附录:常见问题与解答

Q:Canvas绘制卡顿怎么办?
A:1. 减少绘制次数(保持60FPS即可,不要过度绘制);2. 合并绘制操作(比如先绘制所有星星再绘制小球);3. 避免在绘制循环中执行复杂计算(移到更新阶段)。

Q:为什么触摸移动时小球会抖动?
A:可能是触摸事件的坐标更新频率和游戏循环不同步。可以尝试在onTouchMove中直接更新小球位置,而不是在更新阶段处理。

Q:如何让星星有动画效果(比如闪烁)?
A:在星星对象中添加alpha属性(透明度),在更新阶段修改alpha值(0→1→0循环),绘制时设置ctx.globalAlpha = star.alpha


扩展阅读 & 参考资料

《HTML5 Canvas核心技术》(出版书籍,深入理解Canvas绘图原理)
微信小程序游戏开发官方示例(官方提供的游戏Demo)
游戏循环的三种实现方式(深入理解游戏循环的时间控制)

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容