?
掌握<canvas>组件和绘图API
本项目一共需要两个页面:首页和游戏页面。首页用于呈现关卡菜单,点击对应难度的关卡后进入游戏画面。
首页功能需求:
(1)包含标题和关卡列表
(2)关卡至少要有6个关卡选项,每个关卡显示预览图片和第几关
(3)点击关卡列表可以打开对应的游戏界面
游戏页功能需求:
(1)游戏页面需要显示游戏提示图,游戏画面,“重新开始”按钮
(2) 每关游戏提示图显示对应的图片预览
(3)游戏画面随机将原图打乱为3x3的小方块,并且可移动被点击的方块。
(4)点击“重新开始”按钮可以重新随机打乱小方块并开始游戏
创建页面文件
需创建index首页页面和game游戏页面
页面设计
app.json
{
"pages":[
"pages/index/index"
],
"window": {
"navigationBarBackgroundColor": "#E64344",
"navigationBarTitleText": "拼图游戏"
}
}
app.wxss
/**app.wxss**/
.container{
height: 100vh;
color: #E64340;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
/* 顶端标题样式 */
.title{
font-size: 18pt;
}
index.wxml
<!--index.wxml-->
<view class="container">
<!-- 标题 -->
<view class="title"> 游戏选关 </view>
<!-- 关卡列表 -->
<view class="levelBox">
<view class="box">
<image src="/images/p1.jpg"></image>
<text>第一关</text>
</view>
</view>
</view>
index.wxss
/**index.wxss**/
/* 关卡列表区域 */
.levelBox{
width: 100%;
}
/* 单个关卡区域 */
.box{
width: 50%;
float: left;
margin: 25rpx 0;
display: flex;
flex-direction: column;
align-items: center;
}
/* 选关图片 */
image{
width: 260rpx;
height: 260rpx;
}
游戏页面设计
游戏页面需要用户点击首页的关卡列表,然后在新窗口中打开该页面。游戏页面包括游戏提示图,游戏画面和“重新开始”按钮
由于暂时没有做点击跳转的逻辑设计,可以在开发工具顶端选择“普通编译”下的“添加编译模式”,并携带临时测试参数level=pi1.jpg
游戏页面设计
<!--pages/game/game.wxml-->
<view class="container">
<!-- 提示图区域 -->
<view class="title"> 提示图 </view>
<image src="/images/p1.jpg"></image>
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas"></canvas>
<!-- 重新开始按钮 -->
<button type="warn">重新开始</button>
</view>
/* pages/game/game.wxss */
/* 提示图样式 */
image{
width: 250rpx;
height: 250rpx;
}
/* 画布样式 */
canvas{
border: 1rpx solid;
widows: 300rpx;
height: 300rpx;
}
逻辑实现
首页逻辑
首页功能主要是展示关卡列表,点击图片能跳转到游戏界面
关卡列表展示
在JS文件的data中录入关卡图片的数据信息。
index.js
*/
data: {
levels:[
'p1.jpg',
'p2.jpg',
'p3.jpg',
'p4.jpg',
'p5.jpg',
'p6.jpg'
]
},
<view>组件添加wx:for属性循环显示关卡列表数据和图片
index.wxml
<!-- 关卡列表 -->
<view class="levelBox">
<view class="box" wx:for="{{levels}}" wx:key="levels{{index}}">
<image src="/images/{{item}}"></image>
<text>第{{index+1}}关</text>
</view>
</view>
点击跳转游戏页面
若需要用户点击图片即可实现跳转,需要首先为关卡列表项目添加点击事件
index.wxml
<!-- 关卡列表 -->
<view class="levelBox">
<view class="box" wx:for="{{levels}}" wx:key="levels{{index}}" bindtap="chooseLevel" data-level='{{item}}'>
<image src="/images/{{item}}"></image>
<text>第{{index+1}}关</text>
为关卡添加了自定义点击事件函数chooseLevel,并且使用data-level属性携带了关卡图片信息
chooseLevel:function(e){
let level = e.currentTarget.dataset.level
wx.navigateTo({
url: '../game/game?level='+level,
})
},
游戏页逻辑
游戏页主要有两个功能需要实现,一是显示提示图,二是游戏逻辑实现
在游戏页接收关卡信息
Page({
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// 更新图片路径地址
url='/images/'+options.level
// 更新提示图的地址
this.setData({urrl:url})
},
})
<!--pages/game/game.wxml-->
<view class="container">
<!-- 提示图区域 -->
<view class="title"> 提示图 </view>
<image src="{{url}}"></image>
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas"></canvas>
<!-- 重新开始按钮 -->
<button type="warn">重新开始</button>
</view>
游戏逻辑实现
首先在game.js文件的顶端记录一些游戏初始数据信息
// pages/game/game.js
// 方块的初始位置
var num = [
['00','01','02'],
['10','11','12'],
['20','21','22']
]
// 方块的宽度
var w = 100
// 图片的初始地址
var url = '/images/p1.jpg'
Page({
/**
* 页面的初始数据
*/
初始化拼图画面
随机抽取画面中的任意两个方块,然后交换彼此位置,在进行足够多次数的交换之后基本可以实现随机打乱效果。
但有时会陷入死局
可以考虑从空白方块的所在位置入手,每次随机让它和周围的邻近方块交换位置,可以通过方块反向移动回到最初状态(确保本局有解),并且在交换足够多次数之后可以实现随机打乱效果
在game.js文件中添加shuffle函数,用于重新开始游戏
Page({
// 自定义函数-随机打乱方块顺序
shuffle:function(){
// 令所有方块回归初始位置
num = [
['00','01','02'],
['10','11','12'],
['20','21','22']
]
// 记录当前空白方块的行和列
var row = 2
var col = 2
// 打乱方块顺序100次
for(var i=0;i<100;i++){
// 随机产生其中一个方向:上(0),下(1),左(2),右(3)
var direction = Math.round(Math.random()*3)
//上:0
if(direction == 0){
// 空白方块不在最上面一行
if(row != 0){
// 交换位置
num[row][col] = num[row-1][col]
num[row-1][col] = '22'
row -= 1
}
}
//下:1
else if(direction == 1){
// 空白方块不在最下面一行
if(row != 2){
// 交换位置
num[row][col] = num[row+1][col]
num[row+1][col] = '22'
// 更新空白块
row += 1
}
}
//左:2
else if(direction == 2){
// 空白方块不在最左侧
if(col != 0){
// 交换位置
num[row][col] = num[row][col-1]
num[row][col-1] = '22'
// 更新空白块
col -= 1
}
}
//右:3
else if(direction == 3){
// 空白方块不在最右侧
if(col != 2){
// 交换位置
num[row][col] = num[row][col+1]
num[row][col+1] = '22'
// 更新空白块
col += 1
}
}
}
},
/**
* 页面的初始数据
*/
data: {
},
每次使用Math.random()方法从上下左右4个方向中随机产生一个方向,之后如果符合条件则交换空白方块和图片方块位置
在game.js中添加自定义函数drawCanvas,用于打乱后的图片方块绘制到画布上
game.js
// 自定义画布函数
drawCanvas:function(){
let ctx = this.ctx
// 清空画布
ctx.clearRect(0,0,300,300)
// 使用双重for循环绘制3x3的拼图
for(var i = 0; i < 3;i++){
for(var j = 0; j < 3;j++){
if(num[i][j] != '22'){
// 获取行和列
var row = parseInt(num[i][j]/10)
var col = num[i][j]%10
//绘制方块
ctx.drawImage(url,col*w,row*w,w,w,j*w,i*w,w,w)
}
}
}
ctx.draw()
},
最后在game.js的onLoad函数中调用自定义函数shuffle和drawCanvas.
对应的game.js
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// 更新图片路径地址
url='/images/'+options.level
// 更新提示图的地址
this.setData({urrl:url})
// 创建画布上下文
this.ctx = wx.createCanvasContext('myCanvas')
// 打乱方块顺序
this.shuffle()
// 绘制画布内容
this.drawCanvas()
},
移动被点击的块
修改game.wxml页面中的画布组件<canvas>为其绑定触摸事件
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas" bindtouchstart="touchBox"></canvas>
在game.js文件中添加自定义函数touchBox,用于实现图片方块的移动
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas" bindtouchstart="touchBox"></canvas>
// 自定义函数-监听点击方块事件
touchBox:function(e){
// 如果游戏成功,不做任何操作
if(this.data.isWin){
// 终止本函数
return
}
// 获取被点击方块的坐标x和y
var x = e.changedTouches[0].x
var y = e.changedTouches[0].y
// // 换算成行和列
var row = parseInt(y/w) //将坐标除于小方块的宽度w得出行和列
var col = parseInt(x/w)
// 如果点击的不是空白位置
if(num[row][col]!='22'){
//尝试移动方块
this.moveBox(row,col)
}
},
/**
* 自定义函数--移动方块
*/
moveBox: function (i,j) {
//情况1:如果被点击的方块不在最上方,检查可否上移
if(i>0){
//如果方块上方是空白的
if(num[i-1][j] == '22'){
// 交换方块与空白的位置
num[i-1][j] = num[i][j]
num[i][j] = '22'
return
}
}
//情况2:如果被点击的方块不在最下方,检查可否下移
if(i<2){
//如果方块下方是空白的
if(num[i+1][j] == '22'){
// 交换方块与空白的位置
num[i+1][j] = num[i][j]
num[i][j] = '22'
return
}
}
//情况3:如果被点击的方块不在最左边,检查可否左移
if(j>0){
//如果方块上方是空白的
if(num[i][j-1] == '22'){
// 交换方块与空白的位置
num[i][j-1] = num[i][j]
num[i][j] = '22'
return
}
}
//情况4:如果被点击的方块不在最右边,检查可否右移
if(j<2){
//如果方块右边是空白的
if(num[i][j+1] == '22'){
// 交换方块与空白的位置
num[i][j+1] = num[i][j]
num[i][j] = '22'
return
}
}
},
判断游戏成功
首先在game.js文件的data中添加初始数据isWin,用于标记游戏成功与否。
/**
* 页面的初始数据
*/
data: {
isWin:false
},
isWin为false表示游戏尚未成功,当成功时会重置为true.
在game.js文件中添加自定义函数isWin,用于判断游戏是否已经成功。
//自定义函数 - 判断游戏是否成功
isWin:function(){
// 使用双重for循环遍历整个数组
for(var i = 0;i < 3;i++ ){
for(var j = 0;j < 3;j++ ){
// 如果有方块位置不对
if(num[i][j] != i*10 + j){
// 返回假,游戏尚未成功
return false
}
}
}
// 游戏成功,更新状态
this.setData({
isWin:true
})
// 返回i真,游戏成功
return true
},
// 自定义函数-监听点击方块事件
touchBox:function(e){
// 如果游戏成功,不做任何操作
if(this.data.isWin){
// 终止本函数
return
}
修改game.js的touchBox函数,要求被触发时追加对游戏成功状态的判断
// 自定义函数-监听点击方块事件
touchBox:function(e){
// 如果游戏成功,不做任何操作
if(this.data.isWin){
// 终止本函数
return
}
// 获取被点击方块的坐标x和y
。。。略
// 如果点击的不是空白位置
。。。略
// 重新绘制画布内容
this.drawCanvas()
// 判断游戏是否成功
if(this.isWin()){
// 在画面上绘制提示语句
let ctx =this.ctx
// 绘制完整图片
ctx.drawImage(url,0,0)
// 绘制文字
ctx.setFillStyle('#e64340')//调节文字颜色
ctx.setTextAlign('center') //表示这个字在这个坐标点上居中显示
ctx.setFontSize(50) //调节字的大小
ctx.fillText('游戏成功',150,150)
ctx.draw()
}
}
},
重新开始游戏
修改game.wxml代码,为“重新开始”按钮追加定义
<!-- 重新开始按钮 -->
<button type="warn" bindtap="restartGame">重新开始</button>
在game.js文件中添加restartGame函数,用于重新开始游戏
// 自定义函数-重新开始游戏
restartGame:function(){
// 更新游戏成功状态
this.setData({isWin:false})
// 打乱方块顺序
this.shuffle()
//绘制画布内容
this.drawCanvas()
},
完整代码:
app.json
{
"pages":[
"pages/index/index",
"pages/game/game"
],
"window": {
"navigationBarBackgroundColor": "#E64344",
"navigationBarTitleText": "拼图游戏"
}
}
app.wxss
/**app.wxss**/
.container{
height: 100vh;
color: #E64340;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
/* 顶端标题样式 */
.title{
font-size: 18pt;
}
首页代码展示:
index.wxml
<!--index.wxml-->
<view class="container">
<!-- 标题 -->
<view class="title"> 游戏选关 </view>
<!-- 关卡列表 -->
<view class="levelBox">
<view class="box" wx:for="{{levels}}" wx:key="levels{{index}}" bindtap="chooseLevel" data-level='{{item}}'>
<image src="/images/{{item}}"></image>
<text>第{{index+1}}关</text>
</view>
</view>
</view>
index.wxss
/**index.wxss**/
/* 关卡列表区域 */
.levelBox{
width: 100%;
}
/* 单个关卡区域 */
.box{
width: 50%;
float: left;
margin: 25rpx 0;
display: flex;
flex-direction: column;
align-items: center;
}
/* 选关图片 */
image{
width: 260rpx;
height: 260rpx;
}
index.js
// index.js
// 获取应用实例
Page({
/**
* 页面的初始数据
*/
data: {
levels:[
'p1.jpg',
'p2.jpg',
'p3.jpg',
'p4.jpg',
'p5.jpg',
'p6.jpg'
]
},
chooseLevel:function(e){
let level = e.currentTarget.dataset.level
wx.navigateTo({
url: '../game/game?level='+level,
})
},
})
game.wxml
<!--pages/game/game.wxml-->
<view class="container">
<!-- 提示图区域 -->
<view class="title"> 提示图 </view>
<image src="{{url}}"></image>
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas" bindtouchstart="touchBox"></canvas>
<!-- 重新开始按钮 -->
<button type="warn" bindtap="restartGame">重新开始</button>
</view>
game.wxss
/* pages/game/game.wxss */
/* 提示图样式 */
image{
width: 250rpx;
height: 250rpx;
}
/* 画布样式 */
canvas{
border: 1rpx solid;
width: 300px;
height: 300px;
}
game.js
// pages/game/game.js
// 方块的初始位置
var num = [
['00','01','02'],
['10','11','12'],
['20','21','22']
]
// 方块的宽度
var w = 100
// 图片的初始地址
var url = '/images/p1.jpg'
Page({
/**
* 页面的初始数据
*/
data: {
isWin:false
},
// 自定义画布函数
drawCanvas:function(){
let ctx = this.ctx
// 清空画布
ctx.clearRect(0,0,300,300)
// 使用双重for循环绘制3x3的拼图
for(var i = 0; i < 3;i++){
for(var j = 0; j < 3;j++){
if(num[i][j] != '22'){
// 获取行和列
var row = parseInt(num[i][j]/10)
var col = parseInt(num[i][j]%10)
//绘制方块
ctx.drawImage(url,col*w,row*w,w,w,j*w,i*w,w,w)
}
}
}
ctx.draw()
},
// 自定义函数-随机打乱方块顺序
shuffle:function(){
// 令所有方块回归初始位置
num = [
['00','01','02'],
['10','11','12'],
['20','21','22']
]
// 记录当前空白方块的行和列
var row = 2
var col = 2
// 打乱方块顺序100次
for(var i=0;i<100;i++){
// 随机产生其中一个方向:上(0),下(1),左(2),右(3)
var direction = Math.round(Math.random()*3)
//上:0
if(direction == 0){
// 空白方块不在最上面一行
if(row != 0){
// 交换位置
num[row][col] = num[row-1][col]
num[row-1][col] = '22'
row -= 1
}
}
//下:1
else if(direction == 1){
// 空白方块不在最下面一行
if(row != 2){
// 交换位置
num[row][col] = num[row+1][col]
num[row+1][col] = '22'
// 更新空白块
row += 1
}
}
//左:2
else if(direction == 2){
// 空白方块不在最左侧
if(col != 0){
// 交换位置
num[row][col] = num[row][col-1]
num[row][col-1] = '22'
// 更新空白块
col -= 1
}
}
//右:3
else if(direction == 3){
// 空白方块不在最右侧
if(col != 2){
// 交换位置
num[row][col] = num[row][col+1]
num[row][col+1] = '22'
// 更新空白块
col += 1
}
}
}
},
// 自定义函数-监听点击方块事件
touchBox:function(e){
// 如果游戏成功,不做任何操作
if(this.data.isWin){
// 终止本函数
return
}
// 获取被点击方块的坐标x和y
var x = e.changedTouches[0].x
var y = e.changedTouches[0].y
// // 换算成行和列
var row = parseInt(y/w) //将坐标除于小方块的宽度w得出行和列
var col = parseInt(x/w)
// 如果点击的不是空白位置
if(num[row][col]!='22'){
//尝试移动方块
this.moveBox(row,col)
// 重新绘制画布内容
this.drawCanvas()
// 判断游戏是否成功
if(this.isWin()){
// 在画面上绘制提示语句
let ctx =this.ctx
// 绘制完整图片
ctx.drawImage(url,0,0)
// 绘制文字
ctx.setFillStyle('#e64340')//调节文字颜色
ctx.setTextAlign('center') //表示这个字在这个坐标点上居中显示
ctx.setFontSize(50) //调节字的大小
ctx.fillText('游戏成功',150,150)
ctx.draw()
}
}
},
//自定义函数 - 判断游戏是否成功
isWin:function(){
// 使用双重for循环遍历整个数组
for(var i = 0;i < 3;i++ ){
for(var j = 0;j < 3;j++ ){
// 如果有方块位置不对
if(num[i][j] != i*10 + j){
// 返回假,游戏尚未成功
return false
}
}
}
// 游戏成功,更新状态
this.setData({isWin:true})
// 返回i真,游戏成功
return true
},
// 自定义函数-重新开始游戏
restartGame:function(){
// 更新游戏成功状态
this.setData({isWin:false})
// 打乱方块顺序
this.shuffle()
//绘制画布内容
this.drawCanvas()
},
/**
* 自定义函数--移动方块
*/
moveBox: function (i,j) {
//情况1:如果被点击的方块不在最上方,检查可否上移
if(i>0){
//如果方块上方是空白的
if(num[i-1][j] == '22'){
// 交换方块与空白的位置
num[i-1][j] = num[i][j]
num[i][j] = '22'
return
}
}
//情况2:如果被点击的方块不在最下方,检查可否下移
if(i<2){
//如果方块下方是空白的
if(num[i+1][j] == '22'){
// 交换方块与空白的位置
num[i+1][j] = num[i][j]
num[i][j] = '22'
return
}
}
//情况3:如果被点击的方块不在最左边,检查可否左移
if(j>0){
//如果方块上方是空白的
if(num[i][j-1] == '22'){
// 交换方块与空白的位置
num[i][j-1] = num[i][j]
num[i][j] = '22'
return
}
}
//情况4:如果被点击的方块不在最右边,检查可否右移
if(j<2){
//如果方块右边是空白的
if(num[i][j+1] == '22'){
// 交换方块与空白的位置
num[i][j+1] = num[i][j]
num[i][j] = '22'
return
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// 更新图片路径地址
url='/images/'+options.level
// 更新提示图的地址
this.setData({url:url})
// 创建画布上下文
this.ctx = wx.createCanvasContext('myCanvas')
// 打乱方块顺序
this.shuffle()
// 绘制画布内容
this.drawCanvas()
},
})
|