微信目录集链接在此:
详细解析黑马微信小程序视频–【思维导图知识范围】 | 难度★✰✰✰✰ |
---|
不会导入/打开小程序的看这里:参考
让别人的小程序长成自己的样子-更换window上下颜色–【浅入深出系列001】 |
---|
本系列校训
用免费公开视频,卷飞培训班哈人!打死不报班,赚钱靠狠干!
只要自己有电脑,前后项目都能搞!N年苦学无人问,一朝成名天下知!
啥是2048
很多人都玩过2048,我就比较老套,因为我一向看不上这类单机游戏。但是就在某一天泡脚的无聊时光,拿了媳妇儿的手机,左看看右点点,莫名打开了2048。嗯… 这真是一款打发无聊时光的 “good game”。通过滑动来使得每行或每列相邻并且相同的数字相加而得到一个最大的数字,最后的数字越大,得分越高!于是,我在想,是否能像魔方一样,有一定的套路来帮助我们决定每一步该往哪个方向滑动最佳,以便获得最好的成绩呢?
通过滑动来使得每行或每列相邻并且相同的数字相加
这个游戏怎么玩好象就跟本文没有啥关系了。知道怎么玩就行了。这一次用微信小程序来实现,这种无聊小游戏真的是说不定怎么回事就能火。不信你看看“羊了个羊”,好了,本文章没有那么高级。先看效果图吧。
微信小程序里的效果
题外话:
其实我本来是想写一个简单一些的从后台取列表的小程序的。但是,找到了身边的一本书。沈顺天的《微信小程序项目开发实战》然后,去GIT上下载了这本书的代码:
https://github.com/ssthouse/mini-program-development-code
不推荐!!严重的不推荐
书上的第五章的例子运行不出来,然后,我把各章的代码都运行一下。就感觉这个小游戏里还有一个移动的知识点。就写了本文了。
第五章的代码应该是抓这个头条的新闻里的页面的内容,但是代码有问题。而且页面又丑。完全就不想下手了。
就看到这个小游戏还能运行,就拿出来写写了。
项目里的理论知识
老生常谈的,先说说目录结构,不会导入项目的,打不开项目的。先看前面的文章
目录结构
app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
注意:
- json配置中键名、键值必须使用双引号,不能使用单引号。
- 以下配置中除了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 下载配套资源阅读里面的代码,加界面的其它元素,让它不这么丑(难度★★★★✫)