mcpField
Y
y8-js-sdk

y8-js-sdk

github.com/hungify开发016 次安装1 周前更新
代码仓库

关于 y8-js-sdk

y8-js-sdk 是用于将 Y8 玩家、云存储、广告、成就和排行榜接入 JavaScript 游戏的 SDK 集成指南。它面向 Y8 游戏开发者,并提供带验证检查点的集成流程。

主要功能

  1. 01加载并初始化 Y8 SDK
  2. 02读取登录状态并检查授权
  3. 03读写云端玩家数据
  4. 04接入插页式和奖励广告
  5. 05解锁与查询成就
  6. 06提交分数并读取排行榜

适用场景

  1. 01在游戏中读取玩家登录资料
  2. 02保存已授权玩家的云端游戏数据
  3. 03展示插页式或奖励式广告
  4. 04提交分数并呈现成就

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hungify/playables-skills --skill y8-js-sdk

安装后在 Claude Code 中输入/y8-js-sdk确认已加载。

详细介绍

Y8 JavaScript SDK

集成工作流

请按顺序执行以下步骤。每一步都包含验证检查点,完成后再继续。

  1. 加载 SDK — 注入脚本并轮询 window.ID。
    • ✓ 验证:typeof window.ID !== 'undefined'
  2. 在调用 ID.init({ appId }) 之前订阅 id.init。
    • ✓ 验证:id.init 回调已触发;如果一直未触发,请检查 appId。
  3. 在 id.init 内获取登录状态,以填充玩家资料。
    • ✓ 验证:data.status === 'ok' 表示玩家已登录。
  4. 在 id.init 内并行加载 AdSense 和登录状态。
    • ✓ 验证:adsbygoogle 脚本已加载且 onReady 已触发。
  5. 仅当 profile.isAuthorized === true 时,才使用存储、成就和排行榜功能。
    • ✓ 验证每次调用 API 前:确认玩家已授权,或先提示其登录。
  6. 在 ID.login 调用成功后,将 cachedUserData 设为 null,以便新会话读取自己的云端状态。

安装

const SDK_URL = 'https://cdn.y8.com/api/sdk.js';

function loadScript(src) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        script.async = true;
        script.onload = () => resolve();
        script.onerror = () => reject(new Error('Failed to load ' + src));
        document.head.appendChild(script);
    });
}

function waitFor(globalName, timeoutMs = 10000) {
    return new Promise((resolve, reject) => {
        const started = Date.now();
        const tick = () => {
            if (typeof window[globalName] !== 'undefined') {
                resolve(window[globalName]);
                return;
            }
            if (Date.now() - started > timeoutMs) {
                reject(new Error(globalName + ' is not available'));
                return;
            }
            setTimeout(tick, 50);
        };
        tick();
    });
}

初始化

先订阅 id.init,再调用 ID.init({ appId })。appId(又称 Game API Key)由 Y8 在你注册游戏时发放。id.init 触发后,并行获取登录状态并加载 AdSense。

async function initY8({ gameId, adSenseId, channelId }) {
    if (!gameId) {
        throw new Error('gameId (Y8 appId) is required');
    }

    await loadScript(SDK_URL);
    const sdk = await waitFor('ID'); // ✓ checkpoint: window.ID exists

    return new Promise((resolve, reject) => {
        sdk.Event.subscribe('id.init', () => {
            // ✓ checkpoint: id.init fired — SDK is ready
            loadAdsByGoogle({ adSenseId, channelId })
                .then((showAd) => {
                    sdk.getLoginStatus((data) => {
                        const profile = readPlayerInfo(data);
                        resolve({ sdk, showAd, profile });
                    });
                })
                .catch(reject);
        });

        sdk.init({
            appId: gameId
        });
    });
}

玩家 / 授权

ID.getLoginStatus(callback) 会返回当前会话状态,不会提示登录。ID.login(callback) 会打开 Y8 的登录对话框。两者返回的负载结构相同。

function readPlayerInfo(data) {
    const profile = {
        isAuthorized: false,
        id: null,
        name: null,
        locale: null,
        photos: []
    };

    if (!data || data.status !== 'ok') {
        return profile;
    }

    const details = data.authResponse.details;
    const { pid, locale, nickname, first_name: firstName, last_name: lastName, avatars } = details;

    profile.isAuthorized = true;
    profile.id = pid || null;
    profile.locale = locale || null;
    profile.name = [firstName, lastName].filter((x) => !!x).join(' ') || nickname || null;

    const { thumb_url: photoSmall, medium_url: photoMedium, large_url: photoLarge } = avatars || {};

    if (photoSmall) profile.photos.push(photoSmall);
    if (photoMedium) profile.photos.push(photoMedium);
    if (photoLarge) profile.photos.push(photoLarge);

    return profile;
}

function authorizePlayer(sdk) {
    return new Promise((resolve, reject) => {
        sdk.login((response) => {
            const profile = readPlayerInfo(response);
            if (response && response.status === 'ok') {
                resolve(profile);
            } else {
                reject(new Error('Y8 login was cancelled or failed'));
            }
        });
    });
}

存储

通过 ID.api() 访问云端用户数据。将所有游戏数据存储为单个键下的一个 JSON 数据块——读取一次,在本地修改,然后写回。

登录后: 将 cachedUserData = null,以便新获授权的玩家读取自己的云端状态。

const USERDATA_KEY = 'userData';
const NOT_FOUND_ERROR = 'Key not found';

let cachedUserData = null;

function loadUserData(sdk) {
    return new Promise((resolve, reject) => {
        if (cachedUserData) {
            resolve(cachedUserData);
            return;
        }

        sdk.api('user_data/retrieve', 'POST', { key: USERDATA_KEY }, (response) => {
            if (response.error && response.error !== NOT_FOUND_ERROR) {
                reject(response);
                return;
            }

            let userData = {};
            try {
                if (response.jsondata) {
                    userData = JSON.parse(response.jsondata);
                }
            } catch (e) {
                // Leave userData as an empty object on parse failure
            }

            cachedUserData = userData;
            resolve(userData);
        });
    });
}

function saveUserData(sdk, userData) {
    return new Promise((resolve, reject) => {
        const payload = { key: USERDATA_KEY, value: JSON.stringify(userData) };
        sdk.api('user_data/submit', 'POST', payload, (response) => {
            if (response.status === 'ok') {
                cachedUserData = userData;
                resolve();
            } else {
                reject(response);
            }
        });
    });
}

function getStorageValue(sdk, key) {
    return loadUserData(sdk).then((userData) => userData[key] ?? null);
}

function setStorageValue(sdk, key, value) {
    return loadUserData(sdk).then((userData) => {
        const next = { ...userData, [key]: value };
        return saveUserData(sdk, next);
    });
}

function deleteStorageValue(sdk, key) {
    return loadUserData(sdk).then((userData) => {
        const next = { ...userData };
        delete next[key];
        return saveUserData(sdk, next);
    });
}

广告

adsbygoogle.push 提供 adBreak API,其中 type: 'start' 用于插屏广告,type: 'reward' 用于激励广告。如果 Y8 提供了频道 ID,请使用 data-ad-channel;否则,使用你的 AdSense 客户端 ID。

const ADS_ID = '6129580795478709';

function loadAdsByGoogle({ adSenseId, channelId }) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
        script.setAttribute('crossorigin', 'anonymous');
        script.setAttribute('data-ad-frequency-hint', '180s');

        script.setAttribute('data-ad-client', channelId ? `ca-pub-${ADS_ID}` : adSenseId);
        if (channelId) {
            script.setAttribute('data-ad-channel', channelId);
        } else {
            script.setAttribute('data-ad-host', `ca-host-pub-${ADS_ID}`);
        }

        script.addEventListener('load', () => {
            window.adsbygoogle = window.adsbygoogle || [];
            window.adsbygoogle.push({
                preloadAdBreaks: 'on',
                sound: 'on',
                onReady: () => {}
            });
            resolve((adOptions) => window.adsbygoogle.push(adOptions));
        });

        script.addEventListener('error', () => reject(new Error('adsbygoogle failed to load')));
        document.head.appendChild(script);
    });
}
插屏广告
function showInterstitial(showAd, { onOpened, onClosed, onFailed } = {}) {
    if (!showAd) {
        onFailed?.('ads_not_ready');
        return;
    }

    showAd({
        type: 'start',
        name: 'start-game',
        beforeAd: () => {
            onOpened?.();
        },
        afterAd: () => {
            onClosed?.();
        },
        adBreakDone: (placementInfo) => {
            if (placementInfo.breakStatus !== 'viewed') {
                onFailed?.(placementInfo.breakStatus);
            }
        }
    });
}
激励广告

beforeReward(showAdFn) 用于控制广告展示——调用 showAdFn(0) 即可展示广告。玩家看完广告后会触发 adViewed;如果玩家提前退出,则会触发 adDismissed。

function showRewarded(showAd, { onOpened, onRewarded, onClosed, onFailed } = {}) {
    if (!showAd) {
        onFailed?.('ads_not_ready');
        return;
    }

    showAd({
        type: 'reward',
        name: 'rewarded Ad',
        beforeAd: () => {
            onOpened?.();
        },
        afterAd: () => {
            onClosed?.();
        },
        beforeReward: (showAdFn) => {
            showAdFn(0);
        },
        adDismissed: () => {
            onFailed?.('dismissed');
        },
        adViewed: () => {
            onRewarded?.();
        },
        adBreakDone: (placementInfo) => {
            if (
                placementInfo.breakStatus === 'frequencyCapped' ||
                placementInfo.breakStatus === 'other'
            ) {
                onFailed?.(placementInfo.breakStatus);
            }
        }
    });
}

成就

Y8 原生成就位于 ID.GameAPI.Achievements 下。必须同时提供 achievement(显示名称)和 achievementkey(稳定 ID)。

解锁
function unlockAchievement(sdk, options) {
    if (!options || !options.achievement || !options.achievementkey) {
        return Promise.reject(new Error('achievement and achievementkey are required'));
    }

    return new Promise((resolve) => {
        sdk.GameAPI.Achievements.save(options, (data) => {
            resolve(data);
        });
    });
}
列出成就(自定义查询)

listCustom 会返回包含嵌套 player 对象的成就。将其展平以便于使用。

function getAchievementsList(sdk, options = {}) {
    return new Promise((resolve, reject) => {
        sdk.GameAPI.Achievements.listCustom(options, (data) => {
            if (!data.success) {
                reject(new Error(data.errorcode));
                return;
            }

            const list = data.achievements.map(({ player, ...achievement }) => ({
                ...achievement,
                playerid: player.playerid,
                playername: player.playername,
                lastupdated: player.lastupdated,
                date: player.date,
                rdate: player.rdate
            }));
            resolve(list);
        });
    });
}
原生弹窗

ID.GameAPI.Achievements.list(options) 会打开 Y8 内置的成就界面,且不返回任何内容。

function showAchievementsPopup(sdk, options = {}) {
    sdk.GameAPI.Achievements.list(options);
}

排行榜

每个排行榜都由 Y8 控制面板中配置的字符串 table ID 标识。

提交分数
function submitScore(sdk, tableId, score) {
    return new Promise((resolve, reject) => {
        const options = {
            table: tableId,
            points: score
        };

        sdk.GameAPI.Leaderboards.save(options, ({ success, errormessage }) => {
            if (success) {
                resolve();
            } else {
                reject(new Error(errormessage || 'leaderboard_save_failed'));
            }
        });
    });
}
获取条目

有效的 mode 值:'alltime'(默认)、'daily'、'weekly'、'monthly'。

function getLeaderboardEntries(sdk, tableId) {
    return new Promise((resolve, reject) => {
        const options = {
            table: tableId,
            mode: 'alltime'
        };

        sdk.GameAPI.Leaderboards.listCustom(options, ({ scores, success, errormessage }) => {
            if (!success) {
                reject(new Error(errormessage || 'leaderboard_list_failed'));
                return;
            }

            const entries = scores.map((entry) => ({
                id: entry.playerid,
                name: entry.playername,
                score: entry.points,
                rank: entry.rank,
                photo: null
            }));
            resolve(entries);
        });
    });
}

评论

支持 Markdown