3
文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。如果喜欢,请关注公众号:前端工坊
版权归公众号所有,转载请注明出处。
作者:毛科 刘麒麟

**我们做了一个小游戏!快来看一下!纯干货分享!
阅读本文需要5分钟,只需5分钟,你就会跟我一样,爱上这款游戏~**

图片描述

策划上

这款游戏具体的玩法是通过点击屏幕左右区域来控制小狗的前进方向进行跳跃,而阶梯是无穷尽的,若遇到障碍物或者踩空、或者小狗脚下的阶梯陨落,游戏失败;这款游戏提供指尖娱乐,考验大家左右手的配合能力,和迅速反应能力,长时间训练,就能得高分。

技术上

「使用3D图形引擎three.js随机渲染阶梯」

阶梯由一个个方块随机组成「无障碍物的阶梯」和「有障碍物的阶梯」,无障碍物的阶梯随机组成一条畅通无阻的路径,有障碍物的阶梯随机生成世界杯32强旗子。

方块纹理的绘制:
直接看代码,一个方块由6个面组成
图片描述

// 创建纹理
new THREE.MeshBasicMaterial({
    map: assets.getTexture("cubeWallpaper"),
    overdraw: true,
})

// 创建6个面
const materials = [
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cubeWallpaper"),
        overdraw: true,
    }),
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cubeWallpaper"),
        overdraw: true,
    }),
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cube"),
        overdraw: true,
    }),
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cubeWallpaper"),
        overdraw: true,
    }),
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cobeWallpaper2"),
        overdraw: true,
    }),
    new THREE.MeshBasicMaterial({
        map: assets.getTexture("cubeWallpaper"),
        overdraw: true,
            }),
]
// 创建盒子
const boxmat = new THREE.MeshFaceMaterial(materials)

「使用canvas绘制2D图形,渲染排行榜」
图片描述

排行榜本身的功能实现并不复杂,主要是因为开放数据域的限制,显示麻烦一些。
排行榜主要功能进行拆解后主要涉及以下功能点

  • 上报用户分数
  • 获取用户好友分数
  • 对好友分数进行排序
  • 获取用户好友头像
  • 绘制排行榜
  • 增加前端的分页功能
  • 获取当前用户自己的信息

微信的开放数据域
在微信小程序当中,当我们需要获取微信好友的关系链时,会受到一些限制。微信为了保护自己的用户关系链不被恶意获取盗用,同时又想要建立开放的小程序生态,巩固自身地位。在对信息的开放与封闭之间,微信的解决办法是这样的,通过建立一个开放的数据域,所有和微信好友关系相关的API一律被限制,只能在开放数据域当中使用。且获取的数据,禁止以任何方式直接传递给主域,只能将数据绘制在一个离屏的shareCanvas,而主域通过将这个离屏的shareCavans绘制到上屏canvas上,从而达到展示微信好友关系链的数据。

在绘制排行榜的过程中遇到的坑和解决方法:
主域可以通过wx.postMessage向开放数据域发送消息通信,开放数据域通过监听主域的消息来决定什么时机调用相关API并绘制相关数据。但因为实际的调用API以及绘制页面和加载微信用户头像等等操作都是完全异步的,而主域并不知道什么时候开放数据域完成了接口的调用,和图像的绘制。

解决办法:当用户触发了 点击排行榜按钮之后,主域先绘制一个全透明的蒙层阻止用户继续操作交互。然后通过postMessage通知开放数据域子域,开放数据域子域接收到消息后,先判断这是个什么消息。比如是绘制排行榜ShowRankingList还是游戏结束GameOver,上报用户分数。
如果是绘制排行榜,则先异步加载排行榜的素材资源,然后将黑色背景与排行榜素材先绘制到canvas上,而与此同时主域则每隔0.5秒就获取一次shareCanvas将纹理绘制到主屏上。在开放数据域绘制完素材之后,会立即通过getFriendCloudStorage获取微信好友数据,然后通过getTopDataList对数据进行解析处理,因为拿到的数据是没有排序的,此时还会进行按分数进行排序。代码如下

topDataList = Lodash.sortBy(topDataList, (item) => {
    return -item.score
})

得到数据之后,先克隆一份,然后用slice切割为6个元素传递给displayTopDataList,这个是上屏显示用的6个好友数据。然后通过promiseTopDataList,该方法会将这6个元素的头像进行异步的获取。

这里有几个坑,一次并行请求3个以上的微信头像时,网络上很容易出现562错误,此时会导致头像绘制失败,程序上对头像绘制失败进行了容错,如果获取不到用户头像则显示一个空白的头像。但这始终不是个办法,最后的解决办法是按顺序依次加载完头像,然后返回。两种实现代码如下:
异步获取所有用户的头像

const taskList = []
for (const item of topDataList) {
    taskList.push(this.promiseTopData(item))
}
return Promise.all(taskList)

按步列依次获取用户的头像

return new Promise((resolve, reject) => {
    let index = 0
    let taskList = []
    let task = (index) => {
        const item = topDataList[index]
        this.promiseTopData(item).then((itemReady) => {
            taskList.push(itemReady)
            if (index < topDataList.length - 1) {
                index++
                task(index)
            } else {
                resolve(taskList)
            }
        }).catch((error) => {
            reject(error)
        })
    }
    task(index)
})

当资源准备好之后,调用drawImage方法开始排行榜数据绘制,这个就是普通的2d绘制了,没有什么太多的技巧,主要就是有一个初始的initTop值,元素的位置会相对该Top值偏移以此来进行定位。关于头像画圆,主要是通过创建一个cavnas,然后把头像画上去,再用arc剪裁,然后再把头像纹理画到shareCanvas上。

// 头像剪裁成圆
const avatarCanvas = wx.createCanvas()
avatarCanvas.width = 80
avatarCanvas.height = 80
const avatarContext = avatarCanvas.getContext('2d')

avatarContext.save();
avatarContext.arc(40, 40, 40, 0, Math.PI * 2);
// 从画布上裁剪出这个圆形
avatarContext.clip();
avatarContext.drawImage(topItem.avatarDOM, 0, 0, 80, 80)
avatarContext.restore();

// 实际的画
shareContext.drawImage(avatarCanvas, 130, (initTop + (i * 105)))

有什么方法可以把开放数据域的数据带出来?

目前尝试有,给shareCanvas附加属性,innerHTML,appendChild,data属性,均没有用,在开放数据域是个受限的环境,几乎绝大部分api都无法使用,包括localStorage等,也不能通过canvas的getDataURL把数据转换成Image,之所以这样的限制,是因为为了防止部分人通过开放数据域获取用户信息之后,再用getDataURL拿到绘制的用户数据,传递给后端API,做OCR图像识别解析微信用户关系,所以微信限制shareCanvas输出成图像,且限制,必须只能画在主屏上面。然后再通过人工审核机制,防范恶意小程序偷数据。

「生成并保存战绩页」
图片描述

把用户玩游戏的成果,包装成一个专属的宣传卡片,是诱导并激发用户分享意愿的一个强有力的方法。我们使用canvas结合微信小游戏开发生态,保存当前用户昵称和当场游戏结果生成不同的文案和图片,完成了从画布到截屏和存入相册一系列动作。

微信小游戏开发生态已经为你打通了画布到截屏到存入相册这一系列动作。

在本游戏中使用了 toTempFilePathsaveImageToPhotosAlbum 方法。这两者方法都挂载在wx对象下。

toTempFilePath, 将当前 Canvas 保存为一个临时文件,并生成相应的临时文件路径。

saveImageToPhotosAlbum,保存图片到系统相册。
核心代码:

//  loadCanvas 为我们专门为渲染战绩卡片而初始化的一个 2d canvas。

loadCanvas.toTempFilePath({
    fileType: 'jpg',
    success: function(res) {
        wx.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: function() {
            }
        })
    }
})

注意事项:

saveImageToPhotosAlbum会弹出相册授权弹框,记得处理失败回调哦。

如果里面要引用用户相关信息,记得也要处理用户不授权的情况。

关于生成到相册的图片清晰度问题,经验是,再用canvas绘制背景图的时候,别用高清图,都会被wx压缩的。这个自己去压缩纹理素材,这样既不会触发微信的压缩,又比微信压缩的效果好。

「游戏声音开关的控制」

音频创建

    let audio = new Audio(`${path}`);
    //path 为音频文件地址

音频播放

  audio.play();

音频停止

  audio.pause();

小游戏有背景音乐/小狗撞击障碍物失败音乐/小狗踩空音乐/阶梯陨落音乐/游戏升级音乐5种不同类型的音频文件。我们的做法是统一将音频文件集中预加载,并缓存到一个音频库的对象中,后续按需调用。

音频库对象中,我们定义了, begin, change,off,on等方法,在全局需要的地方做统一调度,这样可以使得,所有音频管理来自于一个music center,不会因为逻辑而紊乱。

在music center中,change,on,off均做了变量锁,使得了全域的操控触发,唯一相应。

for (let t of soundassets) {
    let audio = new Audio(assetpath + t.url);

    audio.autoplay = t.setting.autoplay;
    audio.loop = t.setting.loop;

    audio.addEventListener("load", () => {
        soundonload(audio, t);
        loaded++;
        onProgress(t.url, loaded, itesmtotal);
        if (loaded === itesmtotal) {
            onLoad();
        }
    });
    audio.addEventListener("error", (e) => {
        onError(assetpath + t.url);
    });
    itesmtotal++;
}

「游戏难度方面」

游戏的第一版本,我们的游戏难度是每一层阶梯以1.3s的速度陨落,每完成50s加大一倍难度的策略;小伙伴们反应游戏难得分低之后,我们迅速的调整游戏策略,把阶梯陨落速度降低,并且改为每100s加大一倍难度的玩法。

BitmapFont游戏数字的绘制
图片描述

在游戏当中经常有需要绘制一些图形的文字数字等,这个时候可以使用BitmapFont,也就是准备好文字与数字的纹理,并用TexturePack组成一张大图及记录图片尺寸位置信息的font.json。在代码当中加载这两个资源,然后就能直接使用了。

在我们的游戏当中也涉及到需要绘制数字图形分数的部分,首先是制作了一个用于处理分数的Score类,该类负责对分数进行记录,清零以及管理和数字图形的渲染。

当玩家分数变化时,我们会调用render进行分数的绘制,先通过string pad,将数字型的分数补零为5位数字字符串

_pad(num, n) {
    var len = num.toString().length;
    while (len < n) {
        num = "0" + num;
        len++;
    }
    return num;
}

打散字符串,并进行循环,更新对应位数的纹理

// 得到当前数字的纹理
const texture =this.resources.getTexture(texturePath);
// 替换对应位置的material的纹理为分数的纹理
this.score[`n${i}`].material.map = texture;

其它一些方法,reset用于分数清零,init负责对分数部件的及初始位数初始化

reset() {
    this.render(0)
}

游戏体验,请扫小程序码:
如果你有更有趣的想法,欢迎留言区讨论~
图片描述

更多小游戏,请关注微信公众号:前端工坊
后续,我们会推出更多纯干货技术分享
图片描述


前端工坊
163 声望18 粉丝

不定期发布有趣、好玩、专业的前端相关技术文章~欢迎投稿~