Bootstrap

【扑克牌】翻牌游戏-微信小程序开发流程详解

还记得小时候玩过扑克牌游戏吗,这样的玩法可多了,除了玩过叠牌,还能玩翻牌,在这里把翻牌游戏给轻松实现了,适合新手做来玩玩很不错,能训练记忆,感兴趣的话来看看实现过程。

给新手学习微信小程序入门一个建议,把HTML网页设计基础知识掌握好,最好再熟悉一些JavaScript基础知识和理解使用Vue,以后学习会更容易一些,

打开微信开发者工具,选择小程序,新建一个项目,

例如,项目名可填写mimiparogram-hlf-pass,如下图

tu0

  • 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张扑克牌的背景图,显示效果如下图,
图1
还没完哦,此时编译运行,会发现页面是无法正常显示的,

那是布局显示部分,用到的数据变量,还没初始化,

要处理一下初始化,就会正常显示了

游戏逻辑

项目里有些文件的后缀名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
})

运行测试

就讲到这里,以上代码全看懂了吗,

写完代码,基本上就可以运行测试了,

那么微信小程序项目的运行效果,录制的动图如下
在这里插入图片描述

发牌动画是否流畅呢,实现里面用到了定时器,还可修改最小延迟,
最终是否流畅取决于设备性能,很适合绘制动画

这样的实现思路还可以在微信小游戏上实现,

可以参考文章【贪吃蛇】微信小程序的游戏转到小游戏上实现方法详解,

微信小游戏项目已整理出来了,运行效果与小程序一样的,录制的动图如下
在这里插入图片描述

对比发现,由于微信小游戏的没有标题栏,就自己画上去

这款小游戏,可以把记忆能力最差的哪个谁给揪出来,不服输的话可以多练习,连小朋友们也喜欢玩哦。

想要项目源码的请点此处前往查找下载,(可能手机上浏览看不到,请用电脑上浏览器查看),找里面的项目名翻扑克牌关键词源码,请放心下载,感谢支持!
请添加图片描述

;