微信小游戏接入好友排行榜(开放数据域)

前言

对于一个微信小游戏来说,好友排行榜绝对是必不可少的功能,能一定程度上增加玩家的战斗力和活跃度,实实在在地增加小游戏的曝光量。

这篇文章皮皮将讲解 如何给小游戏项目加入微信好友排行榜功能 ~

不吹不黑,这绝对是新手开发者的福音!不接受任何反驳!

前排提示:文章中的 排行榜子域项目 已经上传至我的开源主页,甚至 改都不用改,接上项目就能用 ,链接在底部传送门~


正文

微信开放数据域

  1. 要让小游戏接入微信好友排行榜功能,我们必须先了解下什么是 开放数据域 ,来看看 Cocos 官方文档中的解释:

  2. 也就是说,我们的小游戏项目想要加入好友排行榜功能,就需要 单独再创建一个子项目专门用来展示好友排行榜 ,并且 只有在子项目中才可以调用微信提供的数据操作 API

主域(主项目)

  • 首先我们需要在主项目中 增加一个显示排行榜的按钮搭建排行榜的 UI 框架

    • 我们应该尽可能将 UI 部分放在主域中展示。

    • 必要的细节部分才在子域(子项目)中展示,保证效果的前提下越少越好啦。

  • 注意到 wxSubContextView (叫啥名随你啦)节点了吗?

    • 这个节点上有一个 WXSubContentView 组件,有了它,这个节点就会成为子域的容器。也就是说,子域的内容会显示在这个节点上,所以 子域的大小必须和这个节点一致

    • 另外需要注意的是,容器节点还需要一个空的 Sprite 组件来渲染子域的内容。

  • 然后来写一下排行榜的控制脚本,在主域中我们可以通过 wx.postMessage 函数向子域发送信息,获取排行榜或者设置玩家的分数。

const { ccclass, property } = cc._decorator;

@ccclass
export default class RankPanel extends cc.Component {

    @property(cc.Node)
    private main: cc.Node = null;

    @property(cc.Node)
    private closeBtnNode: cc.Node = null;

    private static instance: RankPanel = null;

    protected onLoad() {
        RankPanel.instance = this;

        this.closeBtnNode.on('touchend', RankPanel.hide, this);
    }

    protected onDestroy() {
        this.closeBtnNode.off('touchend', RankPanel.hide, this);
    }

    public static show() {
        this.instance.main.active = true;
        this.getRank();
    }

    public static hide() {
        this.instance.main.active = false;
    }

    /**
     * 设置用户的分数
     * @param value
     */
    public static setScore(value: number) {
        wx.postMessage({
            event: 'setScore',
            score: value
        });
    }

    /**
     * 获取排行榜
     */
    public static getRank() {
        wx.postMessage({
            event: 'getRank'
        });
    }

}

子域(子项目)

  • 新建一个项目作为我们的子域,关于子域我们需要 注意 以下两点:

  • 将子域的 Canvas 节点的 Canvas 组件的设计分辨率调整为我们主域的容器节点的大小 ,否则子域内容会被缩放,导致运行效果与预期不一致!

  • 这里还要提一点,子域的 分辨率越低性能越高 ,反之 分辨率越高性能越低但是显示效果更好

  • 将子域的 Main Camera 节点的 Camera 组件的 Background Color 属性的不透明度(Alpha)设为 0 ,否则运行时子域内容就是一片漆黑!

  • 接下来就是和普通场景一样制作我们需要的好友列表 UI:

    • ScrollView 组件来展示我们的排行榜,并制作展示好友信息的预制体 item

    • 编写 RankItem 组件,添加到 item 预制体上,主要用来设置好友头像、昵称和分数。

const { ccclass, property } = cc._decorator;

@ccclass
export default class RankItem extends cc.Component {

    @property(cc.Label)
    private rankingLabel: cc.Label = null;

    @property(cc.Sprite)
    private avatarSprite: cc.Sprite = null;

    @property(cc.Label)
    private nicknameLabel: cc.Label = null;

    @property(cc.Label)
    private scoreLabel: cc.Label = null;

    /**
     * 设置展示的信息
     * @param ranking 排名
     * @param user 用户数据
     */
    public set(ranking: number, user: UserGameData) {
        this.rankingLabel.string = ranking.toString();
        this.nicknameLabel.string = user.nickname;
        this.scoreLabel.string = user.KVDataList[0].value.toString();
        this.updateAvatar(user.avatarUrl);
    }

    /**
     * 更新头像
     * @param url 头像链接
     */
    private updateAvatar(url: string) {
        let image = wx.createImage();
        image.onload = () => {
            let texture = new cc.Texture2D();
            texture.initWithElement(image);
            texture.handleLoadedTexture();
            this.avatarSprite.spriteFrame = new cc.SpriteFrame(texture);
        };
        image.src = url;
    }

}
  • 编写 Rank 组件,包含主要的设置、获取数据和更新好友信息展示的逻辑;在子域中,我们使用 wx.onMessage 来监听主域发来的消息。
import RankItem from "./RankItem";

const { ccclass, property } = cc._decorator;

@ccclass
export default class Rank extends cc.Component {

    @property(cc.Node)
    private content: cc.Node = null;

    @property(cc.Prefab)
    private itemPrefab: cc.Prefab = null;

    @property(cc.Node)
    private loading: cc.Node = null;

    protected onLoad() {
        if (cc.sys.platform !== cc.sys.WECHAT_GAME_SUB) return;
        // 监听来自主域的消息
        wx.onMessage((msg: any) => this.onMessage(msg));
    }

    /**
     * 消息回调
     * @param msg 消息
     */
    private onMessage(msg: any) {
        switch (msg.event) {
            case 'setScore':
                this.setScore(msg.score);
                break;
            case 'getRank':
                this.getRank();
                break;
        }
    }

    /**
     * 获取玩家分数
     */
    private getScore(): Promise<number> {
        return new Promise(resolve => {
            console.log('[getScore]');
            wx.getUserCloudStorage({
                keyList: ['score'],
                success: (res: UserGameData) => {
                    console.log('[getScore]', 'success', res);
                    resolve(res.KVDataList[0] ? parseInt(res.KVDataList[0].value) : 0);
                },
                fail: () => {
                    console.log('[getScore]', 'fail');
                    resolve(-1);
                }
            });
        });
    }

    /**
     * 设置玩家分数
     * @param value 分数
     */
    private async setScore(value: number) {
        console.log('[setScore]', value);
        let oldScore = await this.getScore();
        if (oldScore === -1) return;
        if (value > oldScore) {
            wx.setUserCloudStorage({
                KVDataList: [{
                    key: 'score',
                    value: value.toString()
                }],
                success: () => {
                    console.log('[setScore]', 'success');
                },
                fail: () => {
                    console.log('[setScore]', 'fail');
                }
            });
        }
    }

    /**
     * 获取排行榜
     */
    private async getRank() {
        console.log('[getRank]');
        // 显示加载动画
        this.showLoading();
        // 调用微信的函数
        await new Promise(resolve => {
            wx.getFriendCloudStorage({
                keyList: ['score'],
                success: (res: any) => {
                    console.log('[getRank]', 'success', res);
                    // 对数据进行排序
                    res.data.sort((a: UserGameData, b: UserGameData) => {
                        if (a.KVDataList.length === 0 && b.KVDataList.length === 0) return 0;
                        if (a.KVDataList.length === 0) return 1;
                        if (b.KVDataList.length === 0) return -1;
                        return parseInt(b.KVDataList[0].value) - parseInt(a.KVDataList[0].value);
                    });
                    // 排序之后进行展示
                    this.updateRankList(res.data);
                    resolve();
                },
                fail: (res: any) => {
                    console.log('[getRank]', 'fail');
                    resolve();
                }
            });
        });
        // 关闭加载动画
        this.hideLoading();
    }

    /**
     * 更新好友排行
     * @param data 数据
     */
    private updateRankList(data: UserGameData[]) {
        let count = Math.max(data.length, this.content.childrenCount);
        for (let i = 0; i < count; i++) {
            if (data[i] && this.content.children[i]) {
                // 已存在节点,更新并展示
                this.content.children[i].active = true;
                this.content.children[i].getComponent(RankItem).set(i + 1, data[i]);
            } else if (data[i] && !this.content.children[i]) {
                // 节点不足,再实例化一个,更新信息
                let node = cc.instantiate(this.itemPrefab);
                node.setParent(this.content);
                node.getComponent(RankItem).set(i + 1, data[i]);
            } else {
                // 节点多了,关掉吧
                this.content.children[i].active = false;
            }
        }
    }

    /**
     * 显示加载动画
     */
    private showLoading() {
        this.loading.active = true;
    }

    /**
     * 关闭加载动画
     */
    private hideLoading() {
        this.loading.active = false;
    }

}
  • 点击编辑器左上方工具栏的 [ 项目 --> 项目设置 --> 模块设置 ]取消勾选我们子域中没有用到的组件并保存 ,这一步的目的是 减少子域的包体大小

打包运行

  • 首先是我们的 主域 (主项目):打开 构建发布 面板,设置好参数后填写你的微信小游戏 appid ,然后将下方的 开放数据域目录设置为你的子域项目名 ,然后进行构建。

  • 然后是 子域 (子项目):同样是打开 构建发布 面板,将 发布平台 设置为 微信小游戏开放数据域发布路径 设置为 **我们主项目的导出目录(一般为 ${ 你的项目目录/build/wechatgame/ }) **,然后进行构建。

  • 最后,主项目和子项目都构建完成后,我们用 微信开发者工具运行我们的主项目 ,点击加载排行榜,可以看到我们已经成功加载了好友排行榜啦!

    • 如果你那里没有显示任何东西的话,那说明你还没有上传过分数~

    • 另外如果微信开发者工具报错“[GameOpenDataContext] 子域只支持使用 2D 渲染模式”,不用担心,这是正常情况,你的代码没问题,不必理会。

补充

  • 最后再补充一点,wx 是微信的内置全局变量,而在 Cocos Creator 中不存在 wx 这个全局变量,所以在 VSCode 中调用 wx 的函数会报错。

  • 所以我在主项目和子项目中都添加了一个 wx.d.ts 声明文件,来表明 wx 以及其函数的存在,VSCode 就不会再报错了,而且还有智能提示!

  • 我的开源游戏脚手架 eazax-ccc 就包含此文件,另外我的开源主页中也有单独的 wx.d.ts 仓库,下面是 wx.d.ts 文件的内容:

// 以下为 wx.d.ts 文件的内容

/**
 * 微信的命名空间
 */
declare namespace wx {

    /**
     * 打开另一个小程序。
     */
    export function navigateToMiniProgram(object: object): void;

    /**
     * 提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。
     */
    export function authorize(object: object): void;

    /**
     * 获取用户信息。
     */
    export function getUserInfo(object: object): void;

    /**
     * 向开放数据域发送消息。
     */
    export function postMessage(object: object): void;

    /**
     * 监听主域发送的消息。
     */
    export function onMessage(callback: Function): void;

    /**
     * 对用户托管数据进行写数据操作。允许同时写多组 KV 数据。
     */
    export function setUserCloudStorage(object: object): void;

    /**
     * 获取当前用户托管数据当中对应 key 的数据。该接口只可在开放数据域下使用。
     */
    export function getUserCloudStorage(object: object): void;

    /**
     * 删除用户托管数据当中对应 key 的数据。
     */
    export function removeUserCloudStorage(object: object): void;

    /**
     * 监听成功修改好友的互动型托管数据事件,该接口在游戏主域使用。
     */
    export function onInteractiveStorageModified(callback: Function): void;

    /**
     * 修改好友的互动型托管数据,该接口只可在开放数据域下使用。
     */
    export function modifyFriendInteractiveStorage(object: object): void;

    /**
     * 获取当前用户互动型托管数据对应 key 的数据。
     */
    export function getUserInteractiveStorage(object: object): void;

    /**
     * 获取可能对游戏感兴趣的未注册的好友名单。每次调用最多可获得 5 个好友,此接口只能在开放数据域中使用。
     */
    export function getPotentialFriendList(object: object): void;

    /**
     * 获取群信息。小游戏通过群分享卡片打开的情况下才可以调用。该接口只可在开放数据域下使用。
     */
    export function getGroupInfo(object: object): void;

    /**
     * 获取群同玩成员的游戏数据。小游戏通过群分享卡片打开的情况下才可以调用。该接口只可在开放数据域下使用。
     */
    export function getGroupCloudStorage(object: object): void;

    /**
     * 拉取当前用户所有同玩好友的托管数据。该接口只可在开放数据域下使用。
     */
    export function getFriendCloudStorage(object: object): void;

    /**
     * 给指定的好友分享游戏信息,该接口只可在开放数据域下使用。接收者打开之后,可以用 wx.modifyFriendInteractiveStorage 传入参数 quiet=true 发起一次无需弹框确认的好友互动。
     */
    export function shareMessageToFriend(object: object): void;

    /**
     * 获取主域和开放数据域共享的 sharedCanvas。只有开放数据域能调用。
     */
    export function getSharedCanvas(): any;

    /**
     * 获取开放数据域。
     */
    export function getOpenDataContext(): any;

    /**
     * 创建一个图片对象。
     */
    export function createImage(): any;

}

/**
 * 托管数据
 */
declare type UserGameData = {
    avatarUrl: string;
    nickname: string;
    openid: string;
    KVDataList: KVData[];
}

/**
 * 托管的 KV 数据
 */
declare type KVData = {
    key: string;
    value: string;
}

/**
 * 用户信息
 */
declare type FriendInfo = {
    avatarUrl: string;
    nickname: string;
    openid: string;
}

传送门

开源主页:陈皮皮

Eazax-CCC 游戏开发脚手架

微信好友排行榜通用模板

微信小游戏函数与类型声明(wx.d.ts)


更多分享

多平台通用的屏幕分辨率适配方案

围绕物体旋转的方案以及现成的组件

一个全能的挖孔 Shader

一个开源的自动代码混淆插件


结束语

以上皆为陈皮皮的个人观点,小生不才,文采不佳,如果写得不好还请各位多多包涵。如果有哪些地方说的不对,还请各位指出,希望与大家共同进步。

接下来我会持续分享自己所学的知识与见解,欢迎各位关注本公众号。

我们,下次见!


公众号:文弱书生陈皮皮

qrcode

27赞

皮皮越来越牛逼了:+1:

:clap::clap::clap:

感谢大佬分享,如果可以的话能否出一期 关于 关系链互动数据的教程~ 比如实现 ----- 好友 助力、赠送、索取 道具之类的功能~

mark。即将用到

如果可以通吃小程序系就好多了,手Q上子域内容很难加载出来(附带两种报错),头条上不容易传数据进去(各种bad kv_list)

mark!

mark,…

感谢,正好在找这个东西

又白嫖了皮皮一个功能

感谢 这比官方文档写的细 一目了然

大佬,请教一下,微信开放域要怎么销毁呢?

排行榜mark

皮皮佬越来越牛逼帅了

皮神,你的这个排行榜不能滑动

现在微信排行榜还有人看吗, 还要不要做呢

image

mark~学到了去试试

皮神再给个3.X的鲁,我看子域是构建发布时候引擎帮你生成的,和2.x不一样吧


这个限制有影响么

1赞

大佬,跟着你的教程走的,子域也是用的你的。但是排行榜没得显示内容,有提示授权