微信小程序的2048小游戏--【小程序千寻】

微信目录集链接在此:

详细解析黑马微信小程序视频–【思维导图知识范围】难度★✰✰✰✰

不会导入/打开小程序的看这里:参考

让别人的小程序长成自己的样子-更换window上下颜色–【浅入深出系列001】

本系列校训

用免费公开视频,卷飞培训班哈人!打死不报班,赚钱靠狠干!
只要自己有电脑,前后项目都能搞!N年苦学无人问,一朝成名天下知!

啥是2048

很多人都玩过2048,我就比较老套,因为我一向看不上这类单机游戏。但是就在某一天泡脚的无聊时光,拿了媳妇儿的手机,左看看右点点,莫名打开了2048。嗯… 这真是一款打发无聊时光的 “good game”。通过滑动来使得每行或每列相邻并且相同的数字相加而得到一个最大的数字,最后的数字越大,得分越高!于是,我在想,是否能像魔方一样,有一定的套路来帮助我们决定每一步该往哪个方向滑动最佳,以便获得最好的成绩呢?

通过滑动来使得每行或每列相邻并且相同的数字相加
在这里插入图片描述这个游戏怎么玩好象就跟本文没有啥关系了。知道怎么玩就行了。这一次用微信小程序来实现,这种无聊小游戏真的是说不定怎么回事就能火。不信你看看“羊了个羊”,好了,本文章没有那么高级。先看效果图吧。

微信小程序里的效果

在这里插入图片描述
题外话:
其实我本来是想写一个简单一些的从后台取列表的小程序的。但是,找到了身边的一本书。沈顺天的《微信小程序项目开发实战》然后,去GIT上下载了这本书的代码:
https://github.com/ssthouse/mini-program-development-code
不推荐!!严重的不推荐
书上的第五章的例子运行不出来,然后,我把各章的代码都运行一下。就感觉这个小游戏里还有一个移动的知识点。就写了本文了。
在这里插入图片描述
第五章的代码应该是抓这个头条的新闻里的页面的内容,但是代码有问题。而且页面又丑。完全就不想下手了。
在这里插入图片描述

在这里插入图片描述
就看到这个小游戏还能运行,就拿出来写写了。

项目里的理论知识

老生常谈的,先说说目录结构,不会导入项目的,打不开项目的。先看前面的文章
在这里插入图片描述

目录结构

app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
注意:

  1. json配置中键名、键值必须使用双引号,不能使用单引号。
  2. 以下配置中除了page字段是必需设置,其它项目为可选项。

只有两段,一个是pages,另一个是window 可以说是最简的小程序的app.json文件了

  {
  "pages": [
    "pages/index/index",
    "pages/canvas-demo/index"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle": "black"
  },
  "sitemapLocation": "sitemap.json"
}

页面文件wxml

<view class="container">
  <view class="game-info-panel">
    <view class="logo-cell">2048</view>

    <view class="score-info">
      <text class="title">分数</text>
      <text class="score">{{currentScore}}</text>
    </view>

    <view class="score-info">
      <text class="title">最高分</text>
      <text class="score">{{highestScore}}</text>
    </view>
  </view>

  <view class="game-board"
        bindtouchstart="onTouchStart"
        bindtouchmove="onTouchMove"
        bindtouchend="onTouchEnd">
    <canvas type="2d" id="canvas"></canvas>
  </view>

  <view class="action-panel">
    <view class="start-new-game" bindtap="onStartNewGame">New Game!</view>
  </view>
</view>

小游戏的逻辑才是重点。

const regeneratorRuntime = require('../../lib/runtime') // eslint-disable-line

//获取应用实例
const app = getApp()
const board = require('./board')
const gameManager = require('./game-manager')
const MOVE_DIRECTION = board.MOVE_DIRECTION

const MIN_OFFSET = 40;

Page({
  data: {
    motto: 'Hello World',
    // 棋盘
    highestScore: 0,
    currentScore: 0,
  },
  board: null,
  async onLoad() {
    await this.initCanvas()
    this.startGame()
  },
  startGame() {
    this.board = new board.Board(this.canvas, this.context, this.canvasSize)
    this.board.startGame()
    this.setData({
      currentScore: 0,
      highestScore: gameManager.getHighestScore()
    })
  },
  onStartNewGame() {
    this.startGame()
  },
  canvasSize: null,
  canvas: null,
  context: null,
  async initCanvas() {
    this.canvas = await this.getCanvas()
    this.canvasSize = await this.getCanvasSize()
    this.context = this.canvas.getContext('2d')
  },
  async getCanvasSize() {
    return new Promise((resolve, reject) => {
      wx.createSelectorQuery().select('#canvas')
        .boundingClientRect(function (rect) {
          resolve(rect['width'])
        }).exec()
    })
  },
  async getCanvas() {
    return new Promise(resolve => {
      wx.createSelectorQuery()
        .select('#canvas')
        .fields({
          node: true,
          size: true,
        })
        .exec((res) => {
          console.log('res', res[0])
          const canvas = res[0].node
          resolve(canvas)
        })
    })
  },
  // 用于判断滑动方向的属性值
  touchStartX: 0,
  touchStartY: 0,
  touchEndX: 0,
  touchEndY: 0,
  onTouchStart(e) {
    const touch = e.touches[0]
    this.touchStartX = touch.clientX
    this.touchStartY = touch.clientY
  },
  onTouchMove(e) {
    const touch = e.touches[0]
    this.touchEndX = touch.clientX
    this.touchEndY = touch.clientY
  },
  onTouchEnd(e) {
    const offsetX = this.touchEndX - this.touchStartX
    const offsetY = this.touchEndY - this.touchStartY
    const moveVertical = Math.abs(offsetY) > Math.abs(offsetX)
    if (moveVertical) {
      if (offsetY < -MIN_OFFSET) {
        console.log('move top')
        this.board.move(MOVE_DIRECTION.TOP)
      } else if (offsetY > MIN_OFFSET) {
        console.log('move bottom')
        this.board.move(MOVE_DIRECTION.BOTTOM)
      }
    } else {
      if (offsetX < -MIN_OFFSET) {
        console.log('move left');
        this.board.move(MOVE_DIRECTION.LEFT)
      } else if (offsetX > MIN_OFFSET) {
        console.log('move right');
        this.board.move(MOVE_DIRECTION.RIGHT)
      }
    }
    this.setData({
      currentScore: this.board.currentScore
    });
    if (this.board.isGameOver()) {
      const highestScore = gameManager.getHighestScore()
      if (this.data.currentScore > highestScore) {
        gameManager.setHighestScore(this.data.currentScore)
      }
      wx.showModal({
        title: '游戏结束',
        content: '再玩一次',
        showCancel: false,
        success: () => {
          this.startGame()
        }
      })
    }
    if (this.board.isWinning()) {
      // 显示祝福语,可以继续玩
      wx.showToast({
        title: '达成2048成就',
        icon: 'success'
      })
    }
  }
})

这里的JS用了另一个包,来操作CANVAS,
这个包的代码比较长,大家去文章后面的链接里下载吧。
在这里插入图片描述

画块

const regeneratorRuntime = require('../../lib/runtime') // eslint-disable-line

const MATRIX_SIZE = 4
const PADDING = 8

const MOVE_DIRECTION = {
  LEFT: 0,
  TOP: 1,
  RIGHT: 2,
  BOTTOM: 3
}

const COLOR_MAP = {
  0: {color: '#776e65', bgColor: '#EEE4DA40'},
  2: {color: '#776e65', bgColor: '#eee4da'},
  4: {color: '#776e65', bgColor: '#ede0c8'},
  8: {color: '#f9f6f2', bgColor: '#f2b179'},
  16: {color: '#f9f6f2', bgColor: '#f59563'},
  32: {color: '#f9f6f2', bgColor: '#f67c5f'},
  64: {color: '#f9f6f2', bgColor: '#f65e3b'},
  128: {color: '#f9f6f2', bgColor: '#edcf72'},
  256: {color: '#f9f6f2', bgColor: '#edcc61'},
  512: {color: '#f9f6f2', bgColor: '#edc850'},
  1024: {color: '#f9f6f2', bgColor: '#edc53f'},
  2048: {color: '#f9f6f2', bgColor: '#edc22e'}
}

class Point {
  constructor(rowIndex, columnIndex) {
    this.rowIndex = rowIndex
    this.columnIndex = columnIndex
  }

}

class Cell {
  constructor(value) {
    this.value = value
    // 是否新建方块
    this.isNew = false
    // 移动距离
    this.moveStep = 0
  }

  newStatus(newStatus) {
    if (newStatus !== undefined) {
      this.isNew = newStatus
      return this
    }
    return this.isNew
  }
}

module.exports.MOVE_DIRECTION = MOVE_DIRECTION

function printMatrix(matrix) {
  for (let row of matrix) {
    const values = row.map(cell => cell.value)
    console.log(values.join(' '))
  }
}

class Board {

  constructor(canvas, context, canvasSize) {
    this.matrix = []
    this.currentScore = 0
    this.fillEmptyMatrix()
    this.canvas = canvas
    this.context = context
    this.canvasSize = canvasSize
    this.CELL_SIZE = (canvasSize - (5 * PADDING)) / MATRIX_SIZE
  }

  fillEmptyMatrix() {
    for (let i = 0; i < MATRIX_SIZE; i++) {
      const row = []
      for (let j = 0; j < MATRIX_SIZE; j++) {
        row.push(new Cell(0))
      }
      this.matrix.push(row)
    }
  }

  randomIndex() {
    return Math.floor(Math.random() * MATRIX_SIZE)
  }

  startGame() {
    // 初始化两个cell
    for (let i = 0; i < 2; i++) {
      this.matrix[this.randomIndex()][this.randomIndex()].value = Math.random() < 0.8 ? 2 : 4
      this.matrix[this.randomIndex()][this.randomIndex()].newStatus(true)
      this.drawWithAnimation(MOVE_DIRECTION.LEFT)
    }
  }

  moveValidNumToLeft(matrix) {
    const movedMatrix = []
    for (let i = 0; i < MATRIX_SIZE; i++) {
      const row = []
      for (let j = 0; j < MATRIX_SIZE; j++) {
        if (matrix[i][j].value !== 0) {
          matrix[i][j].moveStep += j - row.length
          row.push(matrix[i][j])
        }
      }
      while (row.length < MATRIX_SIZE) {
        row.push(new Cell(0))
      }
      movedMatrix.push(row)
    }
    return movedMatrix
  }

  drawBoard(process, direction) {
    const context = this.context
    const canvasSize = this.canvasSize
    const matrix = this.matrix
    const CELL_SIZE = this.CELL_SIZE
    context.clearRect(0, 0, canvasSize, canvasSize)
    this.drawBgCells()
    for (let rowIndex = 0; rowIndex < MATRIX_SIZE; rowIndex++) {
      for (let colIndex = 0; colIndex < MATRIX_SIZE; colIndex++) {
        // 画出当前矩形
        const moveStep = matrix[rowIndex][colIndex].moveStep
        const startPoint = {
          x: (PADDING + colIndex * (CELL_SIZE + PADDING)),
          y: PADDING + rowIndex * (CELL_SIZE + PADDING)
        }
        switch (direction) {
          case MOVE_DIRECTION.LEFT:
            startPoint.x += moveStep * (CELL_SIZE + PADDING) * (1 - process)
            break
          case MOVE_DIRECTION.RIGHT:
            startPoint.x -= moveStep * (CELL_SIZE + PADDING) * (1 - process)
            break
          case MOVE_DIRECTION.TOP:
            startPoint.y += moveStep * (CELL_SIZE + PADDING) * (1 - process)
            break
          case MOVE_DIRECTION.BOTTOM:
            startPoint.y -= moveStep * (CELL_SIZE + PADDING) * (1 - process)
            break
        }
        this.drawCell(
          startPoint.x,
          startPoint.y,
          matrix[rowIndex][colIndex],
          process
        )
      }
    }
  }

  drawBgCells() {
    const context = this.context
    context.globalAlpha = 1
    for (let rowIndex = 0; rowIndex < MATRIX_SIZE; rowIndex++) {
      for (let colIndex = 0; colIndex < MATRIX_SIZE; colIndex++) {
        context.fillStyle = 'rgba(238, 228, 218, 0.35)'
        this.drawRoundSquare(
          PADDING + colIndex * (this.CELL_SIZE + PADDING),
          PADDING + rowIndex * (this.CELL_SIZE + PADDING),
          this.CELL_SIZE
        )
        context.fill()
      }
    }
  }

  drawCell(x, y, cell, process) {
    const text = cell.value
    if (text === 0) return
    const context = this.context
    context.globalAlpha = cell.isNew ? process * process : 1
    context.fillStyle = COLOR_MAP[text].bgColor
    this.drawRoundSquare(x, y, this.CELL_SIZE)
    context.fill()
    context.fillStyle = COLOR_MAP[text].color
    context.font = '40px Clear Sans'
    context.textAlign = 'center'
    context.textBaseline = 'middle'
    context.fillText(
      text,
      x + this.CELL_SIZE / 2,
      y + this.CELL_SIZE / 2
    )
  }

  drawRoundSquare(startX, startY, size) {
    const point1 = {
      x: startX,
      y: startY
    }
    const points = {
      point1,
      point2: {
        x: point1.x + size,
        y: point1.y
      },
      point3: {
        x: point1.x + size,
        y: point1.y + size
      },
      point4: {
        x: point1.x,
        y: point1.y + size
      }
    }
    const context = this.context
    context.beginPath()
    context.moveTo((points.point1.x + points.point2.x) / 2,
      (points.point1.y + points.point2.y) / 2)
    context.arcTo(points.point2.x, points.point2.y,
      points.point3.x, points.point3.y, 12)
    context.arcTo(points.point3.x, points.point3.y,
      points.point4.x, points.point4.y, 12)
    context.arcTo(points.point4.x, points.point4.y,
      points.point1.x, points.point1.y, 12)
    context.arcTo(points.point1.x, points.point1.y,
      points.point2.x, points.point2.y, 12)
    context.closePath()
  }

  drawWithAnimation(direction) {
    let process = 0
    const draw = () => {
      this.drawBoard(process / 100, direction)
      if (process < 100) {
        process += 10
        this.canvas.requestAnimationFrame(draw)
      } else {
        // 将cell数据复位
        for (let row of this.matrix) {
          for (let cell of row) {
            cell.newStatus(false)
            cell.moveStep = 0
          }
        }
      }
    }
    draw()
  }

  move(direction) {
    if (!this.canMove(direction)) {
      console.log('该方向不可用')
      return
    }
    const rotatedMatrix = this.transformMatrixToDirectionLeft(this.matrix, direction)
    const leftMovedMatrix = this.moveValidNumToLeft(rotatedMatrix)
    this.matrix = this.reverseTransformMatrixFromDirectionLeft(leftMovedMatrix, direction)
    // 相同数字合并
    for (let i = 0; i < MATRIX_SIZE; i++) {
      for (let j = 0; j < MATRIX_SIZE - 1; j++) {
        if (leftMovedMatrix[i][j].value > 0
          && leftMovedMatrix[i][j].value === leftMovedMatrix[i][j + 1].value) {
          leftMovedMatrix[i][j].value *= 2;
          leftMovedMatrix[i][j].newStatus(true)
          this.currentScore += leftMovedMatrix[i][j].value;
          leftMovedMatrix[i][j + 1].value = 0;
        }
      }
    }
    const againMovedMatrix = this.moveValidNumToLeft(leftMovedMatrix)
    this.matrix = this.reverseTransformMatrixFromDirectionLeft(againMovedMatrix, direction)
    // 增加一个新数字
    const emptyPoints = this.getEmptyCells();
    if (emptyPoints.length !== 0) {
      const emptyPoint = emptyPoints[Math.floor(Math.random() * emptyPoints.length)]
      const cell = Math.random() < 0.8 ?
        new Cell(2)
        : new Cell(4)
      cell.newStatus(true)
      this.matrix[emptyPoint.rowIndex][emptyPoint.columnIndex] = cell
    }
    this.drawWithAnimation(direction)
  }

  transformMatrixToDirectionLeft(matrix, direction) {
    switch (direction) {
      case MOVE_DIRECTION.LEFT:
        return matrix
      case MOVE_DIRECTION.TOP:
        return this.rotateMultipleTimes(matrix, 3);
      case MOVE_DIRECTION.RIGHT:
        return this.rotateMultipleTimes(matrix, 2);
      case MOVE_DIRECTION.BOTTOM:
        return this.rotateMatrix(matrix);
      default:
        return matrix
    }
  }

  reverseTransformMatrixFromDirectionLeft(matrix, direction) {
    switch (direction) {
      case MOVE_DIRECTION.LEFT:
        return matrix
      case MOVE_DIRECTION.TOP:
        return this.rotateMultipleTimes(matrix, 1);
      case MOVE_DIRECTION.RIGHT:
        return this.rotateMultipleTimes(matrix, 2);
      case MOVE_DIRECTION.BOTTOM:
        return this.rotateMultipleTimes(matrix, 3);
      default:
        return matrix
    }
  }

  rotateMultipleTimes(matrix, rotateNum) {
    let newMatrix = matrix
    while (rotateNum > 0) {
      newMatrix = this.rotateMatrix(newMatrix)
      rotateNum--
    }
    return newMatrix
  }

  // 顺时针旋转90°
  rotateMatrix(matrix) {
    const rotatedMatrix = []
    for (let i = 0; i < MATRIX_SIZE; i++) {
      const row = []
      for (let j = MATRIX_SIZE - 1; j >= 0; j--) {
        row.push(matrix[j][i])
      }
      rotatedMatrix.push(row)
    }
    return rotatedMatrix
  }

  canMove(direction) {
    const rotatedMatrix = this.transformMatrixToDirectionLeft(this.matrix, direction)
    // 根据direction, 改为向左判断
    for (let i = 0; i < MATRIX_SIZE; i++) {
      for (let j = 0; j < MATRIX_SIZE - 1; j++) {
        // 如果有两个连着相等的,可以滑动
        if (rotatedMatrix[i][j].value > 0
          && rotatedMatrix[i][j].value === rotatedMatrix[i][j + 1].value) {
          return true;
        }
        // 如果有数字左边有0,可以滑动
        if (rotatedMatrix[i][j].value === 0 && rotatedMatrix[i][j + 1].value > 0) {
          return true;
        }
      }
    }
    return false
  }

  isGameOver() {
    return !this.canMove(MOVE_DIRECTION.LEFT) &&
      !this.canMove(MOVE_DIRECTION.TOP) &&
      !this.canMove(MOVE_DIRECTION.RIGHT) &&
      !this.canMove(MOVE_DIRECTION.BOTTOM)
  }

  getEmptyCells() {
    const emptyCells = []
    for (let i = 0; i < MATRIX_SIZE; i++) {
      for (let j = 0; j < MATRIX_SIZE; j++) {
        if (this.matrix[i][j].value === 0) {
          emptyCells.push(new Point(i, j))
        }
      }
    }
    return emptyCells
  }

  isWinning() {
    let max = 0
    const winNum = 2048
    for (let row of this.matrix) {
      for (let cell of row) {
        max = Math.max(cell, max)
      }
      if (max > winNum) {
        return false
      }
    }
    return max === winNum
  }
}

module.exports.Board = Board

游戏里常见的最高分

const HIGHEST_SCORE_KEY = 'highest_score'
const DEFAULT_HIGHEST_SCORE = 0

function setHighestScore(score) {
  if (score <= 0) throw new Error("score is invalid")
  wx.setStorageSync(HIGHEST_SCORE_KEY, score)
}

function getHighestScore() {
  return wx.getStorageSync(HIGHEST_SCORE_KEY) | DEFAULT_HIGHEST_SCORE
}


module.exports = {
  setHighestScore,
  getHighestScore
}

运行界面:

在这里插入图片描述

提示:如果一不小心出错
这个时候,Ctrl+Z (这个快捷键有必要记住) 基本上80%以上的编辑器程序都是用这个快捷方式恢复到前一步正确的代码。
如果在恢复这个错之前,把这个错误的截图,并保存,查看几下那就更好了。面对页面出错不是大惊小怪,而是看看是啥错误信息,那你基本具备了向高手迈进的姿势了。

总结:

如果按着书一步一步的学习,那真的不知道啥时候才能做出一个比较完整的小程序。
在这里插入图片描述
但是有了【小程序花园】这个系列之后,就不一样了。基本的知识点一看,就可以迅速的搭建出自己的相应的小程序。
这个小游戏其实并不简单。真的要自己写出来。那差不多得3年以上的老程序员。这个思路其实,跟是不是小程序没有太多的关系。都是取得CANVAS之后,大量的算法。
你要是真的能看下去的话,你会发现,其实语言,工具的那一层都不是难度。年轻的时候,可以多花点时间做一些烧脑的算法的练习,等到过了35岁了,可能你会感觉心有余而力不足了。
可以参考《详细解读java的连连看游戏的源代码–【课程设计】
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

配套资源

微信小程序的2048小游戏–【小程序千寻】
https://download.csdn.net/download/dearmite/88210296

作业:

1 下载配套资源阅读里面的代码,加界面的其它元素,让它不这么丑(难度★★★★✫)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

项目花园范德彪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值