还记得小时候玩过扑克牌游戏吗,这样的玩法可多了,除了玩过叠牌,还能玩翻牌,在这里把翻牌游戏给轻松实现了,适合新手做来玩玩很不错,能训练记忆,感兴趣的话来看看实现过程。
给新手学习微信小程序入门一个建议,把HTML网页设计基础知识掌握好,最好再熟悉一些JavaScript基础知识和理解使用Vue,以后学习会更容易一些,
打开微信开发者工具,选择小程序,新建一个项目,
例如,项目名可填写mimiparogram-hlf-pass
,如下图
- AppID 使用自己的测试号
- 不使用云服务
- JavaScript - 基础模板
创建的项目会自动生成一些文件,
如果想做小游戏项目,这里面自动创建的一些文件与小程序项目是有区别的,
有什么区别,可参考这篇文章 微信开发者工具-导入小程序项目会自动切换到小游戏打开出错的解决方案
接下来,打开小程序项目里的文件夹pages,
在里面新建一个页面文件夹,一个页面,名称均为game
小程序有一个布局的文件,用它可以做出的游戏界面布局,
小游戏项目是没有布局文件的,游戏界面要用canvas绘制,
小游戏相对小程序来说,更加复杂一些,如没有可用的UI框架来写,做出来会多花些时间,
页面布局文件,文件后缀名都是wxml,
打开一个布局文件pages/game/game.wxml
,写好的页面内容大致如下
<view class="column">
<view class="column-item">
<view class="row">
view>
<canvas class="canvas" id="zs1028_csdn" type="2d" bindtouchstart="onTouchStart">canvas>
<progress percent="{{progressPercent}}">progress>
view>
<view class="column-item column-item-full">
view>
view>
显示效果的一些样式类名如
column,column-item...
等,在样式文件game.wxss
中设置,
绘图组件canvas
,叫画布,点击画布时会调用方法onTouchStart
,实现方法后面讲
在页面布局中,放置了5x4张扑克牌的背景图,显示效果如下图,
还没完哦,此时编译运行,会发现页面是无法正常显示的,
那是布局显示部分,用到的数据变量,还没初始化,
要处理一下初始化,就会正常显示了
项目里有些文件的后缀名js是处理逻辑代码文件,
打开一个文件pages/game/game.js
,在这里开始写游戏初始化的逻辑,
文件内容大致如下,看看初始化逻辑是怎样的
//...
Page({
/**
* 页面的初始数据
*/
data: {
timerNum:0,// 显示倒计时
scopeCount:0,// 显示记录(分)
errorCount:0,// 显示错误数
progressPercent:100 // 显示进度(倒计时)
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
async onReady() {
//...
},
//...
})
从上文代码中可以看到,页面在加载时会调用
onReady()
这个方法,
接下来,运行一下,看看显示是否正常,
会发现,只有canvas组件画布的显示是一片空白的,
接着写,在onReady()方法里,去执行初始化逻辑代码,
添加代码,如下
// 学过前端 JQuery 的应该熟悉吧,类似的查询工具方法
const { width, height, node:canvas } = await ZS1028_CSDN.queryAsync('#zs1028_csdn')
// 微信小程序的画布canvas的宽高需要调整一致
Object.assign(canvas, { width, height })
// 加载扑克牌图片资源对象方法,传入canaas用于创建Image对象,返回的是images集合数据
const data = await ZS1028_CSDN.loadStaticToImagesAsync(canvas)
// 这里定义一些参数值
let rows = GridRows //网格行数4,这里用全局常量表示
let cols = 5 //网格列数
//以下是计算出的单元格宽,高
let w = Math.trunc(canvas.width/cols)
let h = Math.trunc(data[0].image.height*w/data[0].image.width)
//以下是计算出画布的内边距,左边和上边
let left = canvas.width%w/2
let top = canvas.height%h/2
//获取画布的Context,绘图的方法集合
const ctx = canvas.getContext('2d')
//以下是定义和初始化网格list数据
let list = []
for(let r=0,i=0; r<rows; r++){
for(let c=0; c<cols; c++,i++){
//加入每个单元格的坐标x,y,以平面的横纵坐标轴计算单位
list.push({
x: left+c*w,
y: top+r*h
})
}
}
//定义一个画布数据,将所有参数值缓存到里面
this.canvasData = {
canvas,
context:ctx,
grid:{
width:w,
height:h
},
size:cols,
paddingLeft:left,
paddingTop:top,
data,
list,
showIndex:-1
}
//执行开始动画方法,发牌动画
await this.restartAsync()
//展示对话框,开始游戏确认
await ZS1028_CSDN.showModalAsync('扑克翻牌游戏已准备好,请点击开始游戏,加油(ง •_•)ง','开始游戏')
//开始计时
this.startTimer()
看上面的代码,是否容易理解呢,
为什么有一些方法后面带Async()
,这就是用于区分异步方法的,
什么是异步方法,
Function
回调函数;Promise
对象;如何调用异步方法对初学者来说,理解是有些困难的,
若想不明白可暂时放下,将来提升水平再想吧;
这里把异步方法改成同步执行来写,对初学者来说是容易理解的,
await
,await
之前,看看调用它的方法名称前面是否有加上async
;看上面的代码中,使用了ZS1028_CSDN
模块,此模块是TA远方作者亲自编写的,封装了一些需要调用的方法,现在只看方法名和注释就能理解,
怎样实现它的呢,里面封装好的代码不多,不到100行,看里面代码会感觉复杂,
若看不懂的话,要认清自己水平不够,建议慢慢研究,学到就赚到了,
想要学就去看项目源码吧,如果能研究明白,说不定自己会达到资深程序员一样的水平呢
不好意思,跑题了,接着讲
游戏开始就调用方法restartAsync()
,实现发牌动画,
看前面带了async,这是异步方法,代码如下,看看处理逻辑是怎样的
async restartAsync(){
// 将需要的一些参数值取出来用
const { data, list, canvas, context:ctx, size:cols, grid } = this.canvasData
const { width:w, height:h } = grid
// 网格行数
let rows = GridRows
// indexs 是随机存放的牌索引组合,rows/2*cols=10
let indexs = ZS1028_CSDN.getRandomListIndexs(data.slice(1).map((m,i)=>i+1),rows/2*cols)
// 将索引顺序再次打乱,得indexs2
let indexs2 = ZS1028_CSDN.getRandomListIndexs(indexs)
// 两个加起来,就是20张扑克牌的数量了
indexs = indexs.concat(indexs2)
// 遍历一遍,把每个牌的索引设置到网格中
list.forEach((grid,index)=>{
grid.index = indexs[index] //设置索引
grid.count = 0 //重置牌被翻过的次数
})
// 如果没有,给其赋上默认值,90秒
if (this.maxTimerNum==undefined) this.maxTimerNum = 90
this.setData({
timerNum:MinTimerNum+this.maxTimerNum //倒计时计数: 30 + 90 = 120 秒
})
// 给画布绘制上背景色
ctx.fillStyle='white'
ctx.rect(0,0,canvas.width,canvas.height)
ctx.fill()
// 在调用异步方法前,设置等待状态,防止用户触摸点击处理
this.isWait = true
// 调用一个异步的遍历列表方法,执行每一个发牌的动画
await ZS1028_CSDN.eachListAsync(list, (grid)=>ZS1028_CSDN.startAnimationAsync(canvas, {
duration: 300, //动画时长,毫秒
fromX: (canvas.width-w)/2, // 起始坐标
fromY: canvas.height,
toX: grid.x, // 目的坐标
toY: grid.y,
image: data[0].image, //第一个图片,是牌的背面图片
width: w,
height: h
}))
// 以上计算出:300 * 20张牌 = 6000毫秒, 就是所有动画完成时间大约6秒
this.isWait = false //上面异步方法执行完,表示整个动画结束了,恢复一下这个状态
},
会发现模块
ZS1028_CSDN
封装了一些异步方法,因为执行动画是异步的,
这里只是把异步方法变成同步执行了,看上去是不是很容易理解
当玩家点击游戏提示中的开始游戏按钮,会调用开始定时方法,
定时方法是startTimer()
,代码如下,
startTimer() {
this.timer = setInterval(()=>{
let {timerNum} = this.data
timerNum--
if(timerNum<=0){
this.closeTimer() //关闭定时器
this.showModalForGame('游戏时间已用完!') //弹出对话框提示玩家游戏结束了
}
//更新显示
this.setData({
timerNum, //定时计数的
progressPercent: Math.trunc(timerNum*100/(this.maxTimerNum+MinTimerNum)) //进度条的
})
},1100)
}
接下来处理玩家通关的规则,要在设定的时间内把所有牌消完才能通关,否则游戏结束,
通关的触发时机,是在玩家翻牌的逻辑中去处理才对,
玩家触摸到画布,就会调用画布绑定的触摸开始事件方法,
在这个方法onTouchStart(e)
里,实现翻牌操作,代码如下
async onTouchStart(event){
// 如果是在进行动画未完成,是不处理点击的
if (this.isWait) return
// 如果有已打开的第二张牌,没有来得及关闭的就先处理关闭
if (this.isWaitClose) this.isWaitClose.close()
// 开始处理触摸事件
const touch = event.touches[0]
const { data, list, canvas, context:ctx, paddingLeft:left, paddingTop:top, grid, size, showIndex } = this.canvasData
// 判断是否在触摸到牌区域范围内
if (!(left<touch.x && top<touch.y && canvas.width-left>touch.x && canvas.height-top>touch.y)) return
// 根据坐标计算在网格中的位置
let col = Math.trunc((touch.x-left)/grid.width)
let row = Math.trunc((touch.y-top)/grid.height)
let index = row*size+col
// 如果不在网格中
if (index>=list.length) return
let selectGrid = list[index]
// 返回指定的单元格的扑克牌索引
let retIndex = selectGrid.index
if (retIndex<0) return
// 通过索引取出选择到的扑克牌图像数据
let selectImage = data[retIndex]
// 判断是否与上次选择的同一个位置单元格牌
if (showIndex==index) {
// 如果是,直接处理将翻开的牌关闭,然后绘制牌
this.canvasData.showIndex = -1
this.drawImage(data[0].image, row, col)
return
}
// 绘制一下,这里是将关闭的牌翻开了
this.drawImage(selectImage.image, row, col)
// 判断上次是否有翻开的牌,
if (showIndex>=0) {
// 将两个翻开的牌拿来比较
let beforeSelectGrid = list[showIndex]
let beforeSelectImage = data[beforeSelectGrid.index]
// 判断牌的id是否一致,一样的牌
if (beforeSelectImage.id == selectImage.id) {
this.isWaitClose?.close() // 将没有关闭的牌关闭
this.clearGrid(index) // 擦除指定绘制的牌
this.canvasData.showIndex = -1
this.isWait = true // 设置等待状态,开始动画
let toX = (canvas.width-grid.width)/2 // 设置目标坐标
let toY = canvas.height
let duration = 600
// 处理拿走第一个牌的动画
await ZS1028_CSDN.startAnimationAsync(canvas, {
duration,
fromX: selectGrid.x,
fromY: selectGrid.y,
toX,
toY,
image: selectImage.image,
width: grid.width,
height: grid.height
})
this.clearGrid(showIndex)
// 处理拿走第二个牌的动画
await ZS1028_CSDN.startAnimationAsync(canvas, {
duration,
fromX: beforeSelectGrid.x,
fromY: beforeSelectGrid.y,
toX,
toY,
image: beforeSelectImage.image,
width: grid.width,
height: grid.height
})
this.isWait = false //动画结束,取消等待状态
// 判断所有的牌,过滤出没翻开的牌还有多少
let { timerNum, scopeCount:scope, errorCount } = this.data
scope += ScopeStep
if (list.filter(grid=>grid.index>0).length<=0){
this.closeTimer()
// 这里计算游戏得分,并更新展示...
}
// 更新分数记录
this.setData({
scopeCount:scope
})
return
}else if (selectGrid.count>0){
let { errorCount } = this.data
// 如果这个牌被翻过还要翻,说明没记住,就更新错误记录
this.setData({
errorCount:errorCount+1
})
}
selectGrid.count++
await this.waitCloseAsync()
// 绘制翻开的牌
this.drawImage(data[0].image, row, col)
return
}
// 更新选择的
this.canvasData.showIndex = index
},
用户翻牌的时候,判断对的要计分,判断错的要记过
如果倒计时完了,或者把牌消完了,就会结束游戏,
在游戏结束时实现计算成绩得分,最后弹出窗展示,代码如下
let msg = '' //用于显示游戏消息
// 记错次数多,会影响奖励分发放的
if (errorCount>0) {
let count = scope + timerNum - errorCount*2
if (count>scope) {
scope = count
msg = '已追加奖励记录分,'
}
}else{
scope += timerNum
msg = '已追加奖励记录分,'
}
if (this.historyScopeCount < scope){
// 刷新记录,就存到本地,下次打开展示最高记录
app.setHistoryScopeCount(scope)
msg = `游戏结束,恭喜刷新记录:${scope},`+msg
}else{
msg = `游戏结束,恭喜过关,当前记录:${scope},`+msg
}
// 弹出对话框,输出游戏成绩
this.showModalForGameAsync(msg+'是否继续?',true)
this.setData({
scopeCount:scope
})
就讲到这里,以上代码全看懂了吗,
写完代码,基本上就可以运行测试了,
那么微信小程序项目的运行效果,录制的动图如下
发牌动画是否流畅呢,实现里面用到了定时器,还可修改最小延迟,
最终是否流畅取决于设备性能,很适合绘制动画
这样的实现思路还可以在微信小游戏上实现,
可以参考文章【贪吃蛇】微信小程序的游戏转到小游戏上实现方法详解,
微信小游戏项目已整理出来了,运行效果与小程序一样的,录制的动图如下
对比发现,由于微信小游戏的没有标题栏,就自己画上去
这款小游戏,可以把记忆能力最差的哪个谁给揪出来,不服输的话可以多练习,连小朋友们也喜欢玩哦。
想要项目源码的请点此处前往查找下载,(可能手机上浏览看不到,请用电脑上浏览器查看),找里面的项目名翻扑克牌关键词源码,请放心下载,感谢支持!