【飞行棋】微信小程序Canvas绘图与多人游戏状态同步实战

1. 飞行棋小游戏开发概述

小时候玩过的飞行棋游戏,是很多人的童年回忆。现在通过微信小程序,我们可以轻松实现一个支持2-4人同时在线对战的飞行棋游戏。这种游戏不仅能够重温儿时乐趣,还能通过微信的社交属性,让好友之间随时随地进行互动。

开发一个完整的飞行棋小程序,主要涉及以下几个核心模块:

  • Canvas绘制棋盘和棋子
  • 游戏逻辑实现(投骰子、移动棋子等)
  • 多人游戏状态同步
  • 用户界面交互

相比传统APP开发,微信小程序具有无需安装、即用即走的优势,特别适合这类轻量级的休闲游戏。而且小程序提供了丰富的API和组件,能够大大降低开发难度。

2. 项目创建与基础配置

2.1 初始化小程序项目

首先打开微信开发者工具,选择"新建项目",填写项目名称(如flying-chess),选择不使用云服务,模板选择"JavaScript-基础模板"。

创建完成后,会自动生成一个基础项目结构:

/pages /index index.js index.json index.wxml index.wxss

2.2 开始页面开发

在index.wxml中,我们需要设计一个简单的开始界面,让玩家选择游戏人数:

<view class="container"> <view class="title">飞行棋</view> <view class="subtitle">选择游戏人数</view> <picker mode="selector" range="{{playerCounts}}" bindchange="onPlayerCountChange"> <view class="picker">当前选择:{{playerCounts[playerIndex]}}人</view> </picker> <button type="primary" bindtap="startGame">开始游戏</button> </view>

对应的index.js逻辑:

Page({ data: { playerCounts: ['2', '3', '4'], playerIndex: 0, selectedCount: 2 }, onPlayerCountChange(e) { this.setData({ playerIndex: e.detail.value, selectedCount: parseInt(this.data.playerCounts[e.detail.value]) }) }, startGame() { wx.navigateTo({ url: `/pages/game/game?count=${this.data.selectedCount}` }) } })

3. 游戏页面开发

3.1 页面结构与Canvas配置

在game.wxml中,我们主要使用Canvas来绘制游戏界面:

<view class="game-container"> <canvas canvas-id="gameCanvas" style="width: 100%; height: 100%;" bindtouchstart="handleTouchStart" ></canvas> <view class="control-panel"> <button bindtap="rollDice" disabled="{{isRolling}}"> {{isRolling ? '骰子滚动中...' : '投骰子'}} </button> <view class="dice-result" wx:if="{{diceValue > 0}}"> 骰子点数:{{diceValue}} </view> </view> </view>

在game.js中初始化Canvas:

Page({ data: { diceValue: 0, isRolling: false, playerCount: 2 }, onLoad(options) { this.setData({ playerCount: parseInt(options.count) || 2 }) this.initCanvas() }, initCanvas() { const query = wx.createSelectorQuery() query.select('#gameCanvas') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node const ctx = canvas.getContext('2d') const dpr = wx.getSystemInfoSync().pixelRatio canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr ctx.scale(dpr, dpr) this.canvas = canvas this.ctx = ctx this.drawBoard() }) } })

3.2 棋盘绘制实现

棋盘绘制是游戏的核心视觉部分,我们需要在utils/game-map.js中实现:

class GameMap { constructor(canvasInfo, playerCount) { this.canvas = canvasInfo.canvas this.ctx = canvasInfo.context this.playerCount = playerCount this.gridSize = 40 this.colors = ['#E60116', '#FFC700', '#0277A8', '#08983F'] this.initGrids() } initGrids() { // 初始化棋盘格子坐标 this.grids = [] // 这里省略具体坐标计算逻辑 // 每个格子需要记录坐标、类型(普通格、基地、终点等) } drawBoard() { const { ctx, grids, gridSize } = this ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) // 绘制背景 ctx.fillStyle = '#55A3FF' ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) // 绘制格子 grids.forEach(grid => { ctx.beginPath() ctx.rect(grid.x, grid.y, gridSize, gridSize) ctx.fillStyle = grid.color || '#FFFFFF' ctx.fill() ctx.strokeStyle = '#999999' ctx.stroke() // 特殊格子标记 if(grid.type === 'base') { // 绘制玩家基地 } else if(grid.type === 'end') { // 绘制终点区域 } }) // 绘制连接线(飞行路线) this.drawConnections() } drawConnections() { // 绘制格子间的连接线 } } module.exports = GameMap

4. 游戏核心逻辑实现

4.1 棋子绘制与管理

在GameMap类中添加棋子绘制方法:

class GameMap { // ...其他代码 drawPieces(players) { const { ctx, gridSize } = this const pieceRadius = gridSize / 2 - 2 players.forEach((player, playerIndex) => { player.pieces.forEach((piece, pieceIndex) => { if(piece.position === 'base') { // 绘制基地中的棋子 const base = this.getPlayerBase(playerIndex) this.drawPiece(base.x, base.y, playerIndex, pieceIndex) } else if(piece.position === 'track') { // 绘制轨道上的棋子 const grid = this.grids[piece.gridIndex] this.drawPiece(grid.x, grid.y, playerIndex, pieceIndex) } }) }) } drawPiece(x, y, playerIndex, pieceIndex) { const { ctx, gridSize, colors } = this const radius = gridSize / 2 - 2 ctx.beginPath() ctx.arc(x + gridSize/2, y + gridSize/2, radius, 0, Math.PI * 2) ctx.fillStyle = colors[playerIndex] ctx.fill() ctx.strokeStyle = '#000000' ctx.stroke() // 绘制棋子编号 ctx.fillStyle = '#FFFFFF' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(pieceIndex + 1, x + gridSize/2, y + gridSize/2) } }

4.2 投骰子逻辑

在game.js中添加投骰子方法:

Page({ // ...其他代码 rollDice() { if(this.data.isRolling) return this.setData({ isRolling: true }) // 模拟骰子动画 let count = 0 const interval = setInterval(() => { count++ const randomValue = Math.floor(Math.random() * 6) + 1 this.setData({ diceValue: randomValue }) if(count >= 10) { clearInterval(interval) this.setData({ isRolling: false }) this.handleDiceResult(randomValue) } }, 100) }, handleDiceResult(value) { // 根据骰子结果处理游戏逻辑 const currentPlayer = this.getCurrentPlayer() if(value === 6) { // 投到6点可以派出新棋子或移动已有棋子 if(this.canLaunchNewPiece(currentPlayer)) { wx.showActionSheet({ itemList: ['派出新棋子', '移动已有棋子'], success: (res) => { if(res.tapIndex === 0) { this.launchNewPiece(currentPlayer) } else { this.movePiece(currentPlayer, value) } } }) return } } this.movePiece(currentPlayer, value) }, movePiece(player, steps) { // 棋子移动逻辑 // 包括动画效果、碰撞检测等 } })

4.3 游戏规则实现

飞行棋的核心规则包括:

  1. 棋子顺时针移动
  2. 投到6点可以派出新棋子或再投一次
  3. 棋子可以叠放
  4. 遇到对手棋子可以将其打回基地
  5. 到达终点区域获胜

在game.js中实现这些规则:

Page({ // ...其他代码 checkCollision(gridIndex, currentPlayerIndex) { const targetGrid = this.gameData.grids[gridIndex] const otherPlayers = this.gameData.players.filter((_, i) => i !== currentPlayerIndex) // 检查目标格子是否有其他玩家的棋子 otherPlayers.forEach(player => { player.pieces.forEach(piece => { if(piece.gridIndex === gridIndex) { // 将对手棋子打回基地 piece.position = 'base' piece.gridIndex = -1 this.showToast(`${currentPlayerIndex + 1}号玩家击退了${player.index + 1}号玩家的棋子`) } }) }) }, checkWinCondition(player) { // 检查玩家是否所有棋子都到达终点 const allInEnd = player.pieces.every(piece => piece.position === 'end') if(allInEnd) { wx.showModal({ title: '游戏结束', content: `${player.index + 1}号玩家获胜!`, showCancel: false, success: () => { wx.navigateBack() } }) } } })

5. 多人游戏状态同步

5.1 使用Socket实现实时通信

微信小程序提供了WebSocket API,可以用来实现多人实时对战:

// 在game.js中 initSocket() { this.socket = wx.connectSocket({ url: 'wss://your-server-url', success: () => { console.log('Socket连接成功') } }) this.socket.onMessage((res) => { const data = JSON.parse(res.data) switch(data.type) { case 'player_join': this.handlePlayerJoin(data) break case 'dice_roll': this.handleRemoteDiceRoll(data) break case 'piece_move': this.handleRemotePieceMove(data) break } }) }, sendSocketMessage(type, payload) { if(this.socket && this.socket.readyState === this.socket.OPEN) { this.socket.send({ data: JSON.stringify({ type, ...payload }) }) } }, // 修改rollDice方法 rollDice() { if(this.data.isRolling) return this.setData({ isRolling: true }) this.sendSocketMessage('dice_roll', { player: this.currentPlayer }) }

5.2 状态同步策略

为了保证所有玩家的游戏状态一致,我们需要设计合理的同步策略:

  1. 权威服务器模式:所有关键操作(投骰子、移动棋子)都由服务器验证后广播
  2. 乐观预测:本地先展示操作结果,等服务器确认后再修正
  3. 状态同步:定期同步完整游戏状态,解决不一致问题

服务器端需要维护完整的游戏状态,并在每次操作后广播给所有客户端:

// 伪代码示例 socket.on('dice_roll', (playerId) => { const value = Math.floor(Math.random() * 6) + 1 gameState.currentPlayer = playerId gameState.diceValue = value // 广播给所有玩家 broadcast({ type: 'dice_result', player: playerId, value: value }) })

5.3 断线重连处理

网络不稳定时需要考虑断线重连:

Page({ onShow() { if(this.socket && this.socket.readyState !== this.socket.OPEN) { this.initSocket() this.requestFullState() // 请求完整游戏状态 } }, requestFullState() { this.sendSocketMessage('request_state', { player: this.currentPlayer }) }, handleFullState(state) { // 用服务器状态更新本地状态 this.gameData = state this.redraw() } })

6. 性能优化与调试

6.1 Canvas绘制优化

频繁的Canvas绘制会影响性能,可以采取以下优化措施:

  1. 分层绘制:将静态元素(棋盘)和动态元素(棋子)分开绘制
  2. 局部重绘:只重绘发生变化的部分,而不是整个画布
  3. 使用离屏Canvas:预渲染静态内容
// 创建离屏Canvas const offScreenCanvas = wx.createOffscreenCanvas({ type: '2d', width: 500, height: 500 }) const offScreenCtx = offScreenCanvas.getContext('2d') // 预渲染棋盘 this.drawStaticBoard(offScreenCtx) // 主绘制函数中 redraw() { // 先绘制预渲染的静态内容 this.ctx.drawImage(offScreenCanvas, 0, 0) // 再绘制动态内容 this.drawPieces() }

6.2 动画优化

棋子移动动画要流畅,可以使用CSS3动画或requestAnimationFrame:

animatePieceMove(piece, fromGrid, toGrid, callback) { const startPos = { x: fromGrid.x, y: fromGrid.y } const endPos = { x: toGrid.x, y: toGrid.y } const duration = 500 // 动画持续时间 const startTime = Date.now() const animate = () => { const elapsed = Date.now() - startTime const progress = Math.min(elapsed / duration, 1) const currentPos = { x: startPos.x + (endPos.x - startPos.x) * progress, y: startPos.y + (endPos.y - startPos.y) * progress } this.redraw() // 先重绘所有内容 this.drawPieceAt(currentPos.x, currentPos.y, piece) // 再绘制移动中的棋子 if(progress < 1) { requestAnimationFrame(animate) } else { callback && callback() } } animate() }

6.3 调试技巧

微信开发者工具提供了强大的调试功能:

  1. Canvas调试:开启"调试"模式可以显示Canvas绘制命令
  2. 性能面板:监控CPU、内存使用情况
  3. 远程调试:在真机上调试时使用

对于多人游戏,可以添加调试命令快速测试各种情况:

// 在game.js中添加调试方法 debugCommand(cmd) { switch(cmd) { case 'win': this.forceWin() break case 'dice6': this.setData({ diceValue: 6 }) break case 'reset': this.resetGame() break } }

7. 项目扩展与优化方向

7.1 游戏功能扩展

基础版本完成后,可以考虑添加更多有趣的功能:

  1. 道具系统:添加各种道具卡(多投一次、后退几步等)
  2. 成就系统:记录玩家的游戏成就
  3. AI对手:单人模式时添加电脑玩家
  4. 自定义皮肤:允许玩家自定义棋子和棋盘外观

7.2 社交功能增强

利用微信的社交能力增强游戏互动:

  1. 排行榜:显示好友间的胜负记录
  2. 观战模式:允许好友观战正在进行中的游戏
  3. 表情互动:游戏中发送表情互动
  4. 战绩分享:将游戏结果分享到朋友圈

7.3 性能进一步优化

对于更复杂的游戏场景,可以进一步优化:

  1. WebGL渲染:对于复杂图形可以考虑使用WebGL
  2. 资源预加载:提前加载所有游戏资源
  3. 代码分包:将游戏代码按需加载
  4. 数据压缩:优化网络传输的数据量

8. 实际开发中的经验分享

在开发飞行棋小程序的过程中,我遇到过几个典型的"坑",这里分享给大家避免重复踩坑:

  1. Canvas尺寸问题:小程序中的Canvas需要显式设置width/height属性,否则会出现绘制模糊。正确的做法是通过wx.createSelectorQuery获取Canvas的实际尺寸,然后考虑设备像素比(dpr)进行缩放。

  2. 触摸事件精度:在小屏幕上精确点击棋子比较困难。我们最终增加了点击区域的检测范围,并在棋子周围添加了半透明的点击热区,大大提升了操作体验。

  3. 网络延迟处理:在弱网环境下,游戏状态同步会出现延迟。我们采用了指令缓冲和状态校验机制,当网络恢复时自动同步最新状态,避免游戏出现不一致。

  4. 动画卡顿问题:同时播放多个动画时会出现卡顿。解决方案是将动画队列化,确保同一时间只有一个主要动画在执行,其他动画采用简化效果。

  5. 微信版本兼容:不同微信版本对Canvas和Socket的支持有差异。我们添加了特性检测和降级方案,确保在旧版本上也能正常运行基础功能。