Compare commits
2 Commits
f841779e94
...
ac2de96bd0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac2de96bd0 | ||
|
|
076e03742d |
@@ -94,60 +94,65 @@ export class GameDataSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将获取到的云端数据与本地对比并合并
|
* 登录阶段与云端双向同步数据(sync 模式)。
|
||||||
|
* 时间戳对比在云端完成:
|
||||||
|
* - 本地较新:云端已被本地数据覆盖,无需额外处理
|
||||||
|
* - 云端较新:用云端数据覆盖本地,并写入 localStorage
|
||||||
|
* @returns true=本次以云端数据为准覆盖了本地;false=以本地数据为准或同步失败
|
||||||
*/
|
*/
|
||||||
public syncWithCloudData(cloudData: CloudData | null) {
|
public async syncFromCloud(): Promise<boolean> {
|
||||||
const localData = this.loadFromLocal();
|
const localData = this.loadFromLocal();
|
||||||
|
|
||||||
let cloudTs = cloudData?.data?.timestamp || 0;
|
// 非微信客户端:仅使用本地缓存
|
||||||
let localTs = localData?.timestamp || 0;
|
if (!this.isWxClient()) {
|
||||||
|
|
||||||
if (!localData || cloudTs > localTs) {
|
|
||||||
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据较新 (Cloud: ${cloudTs} > Local: ${localTs}), 执行覆盖。`);
|
|
||||||
if (cloudData) {
|
|
||||||
smc.overrideLocalDataWithRemote(cloudData);
|
|
||||||
this.saveToLocal(); // 同步到本地
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 本地数据较新 (Local: ${localTs} >= Cloud: ${cloudTs}), 使用本地数据,触发强制云同步。`);
|
|
||||||
smc.overrideLocalDataWithRemote({ data: localData });
|
|
||||||
// 本地数据较新,需要强制推送到云端以保证多端一致
|
|
||||||
this._localDataDirty = true;
|
|
||||||
this.executeCloudSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCloudData() {
|
|
||||||
const localData = this.loadFromLocal();
|
|
||||||
|
|
||||||
// 未登录微信云端前,先用本地数据顶上(让玩家秒进游戏)
|
|
||||||
if (localData && !this.isWxClient()) {
|
|
||||||
smc.overrideLocalDataWithRemote({ data: localData });
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return WxCloudApi.get().then(async (result) => {
|
|
||||||
if(result.result.code === 200) {
|
|
||||||
let cloudData = result.result.data as CloudData;
|
|
||||||
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据成功:`, cloudData);
|
|
||||||
|
|
||||||
this.syncWithCloudData(cloudData);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据失败,使用本地缓存兜底。`);
|
|
||||||
if (localData) {
|
|
||||||
smc.overrideLocalDataWithRemote({ data: localData });
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据异常:`, error);
|
|
||||||
if (localData) {
|
if (localData) {
|
||||||
smc.overrideLocalDataWithRemote({ data: localData });
|
smc.overrideLocalDataWithRemote({ data: localData });
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 本地无缓存时用空对象参与对比,云端会直接下发
|
||||||
|
const gameData = localData || ({} as GameDate);
|
||||||
|
gameData.timestamp = gameData.timestamp || 0;
|
||||||
|
|
||||||
|
const result = await WxCloudApi.sync(gameData);
|
||||||
|
const response = result.result;
|
||||||
|
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const cloudData: CloudData = {
|
||||||
|
openid: response.data.openid || '',
|
||||||
|
data: (response.data.game_data || {}) as GameDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.updated) {
|
||||||
|
// 本地数据较新,云端已被覆盖
|
||||||
|
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 本地数据较新,已覆盖云端。`);
|
||||||
|
if (localData) {
|
||||||
|
smc.overrideLocalDataWithRemote({ data: localData });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// 云端数据较新,覆盖本地
|
||||||
|
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据较新,覆盖本地。`);
|
||||||
|
smc.overrideLocalDataWithRemote(cloudData);
|
||||||
|
this.saveToLocal();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端同步失败,使用本地缓存兜底。`, response?.msg);
|
||||||
|
if (localData) {
|
||||||
|
smc.overrideLocalDataWithRemote({ data: localData });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端同步异常,使用本地缓存兜底。`, error);
|
||||||
|
if (localData) {
|
||||||
|
smc.overrideLocalDataWithRemote({ data: localData });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,21 @@ import { FightSet } from "./config/GameSet";
|
|||||||
* @param remoteData 远程数据(云端或本地调试)
|
* @param remoteData 远程数据(云端或本地调试)
|
||||||
*/
|
*/
|
||||||
export interface GameDate {
|
export interface GameDate {
|
||||||
gold: number,
|
openid?: string,
|
||||||
|
score?: number,
|
||||||
|
mission?: number,
|
||||||
|
diamond?: number,
|
||||||
|
gold?: number,
|
||||||
|
task?: number,
|
||||||
|
noStop?: boolean,
|
||||||
|
showInfo?: boolean,
|
||||||
|
tip_done?: {
|
||||||
|
start: boolean,
|
||||||
|
hero: boolean,
|
||||||
|
equip: boolean,
|
||||||
|
skill: boolean,
|
||||||
|
shop: boolean,
|
||||||
|
},
|
||||||
switch_effect?: boolean, // 音效开关
|
switch_effect?: boolean, // 音效开关
|
||||||
switch_music?: boolean, // 音乐开关
|
switch_music?: boolean, // 音乐开关
|
||||||
timestamp?: number, // 用于比对本地与云端数据的最新状态
|
timestamp?: number, // 用于比对本地与云端数据的最新状态
|
||||||
@@ -37,7 +51,6 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
/** 全局缓存的通用图集 */
|
/** 全局缓存的通用图集 */
|
||||||
uiconsAtlas: SpriteAtlas | null = null;
|
uiconsAtlas: SpriteAtlas | null = null;
|
||||||
|
|
||||||
openid: string = ''
|
|
||||||
mission: any = {
|
mission: any = {
|
||||||
status: 0, //0:未开始 1:进行中 2:胜利 3:失败
|
status: 0, //0:未开始 1:进行中 2:胜利 3:失败
|
||||||
play: false,
|
play: false,
|
||||||
@@ -49,6 +62,7 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||||
};
|
};
|
||||||
data: any = {
|
data: any = {
|
||||||
|
openid: '',
|
||||||
score: 0,
|
score: 0,
|
||||||
mission: 1,
|
mission: 1,
|
||||||
diamond: 100, //商店购买 及 双倍奖励资源
|
diamond: 100, //商店购买 及 双倍奖励资源
|
||||||
@@ -65,7 +79,7 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
shop: false, // 购买药品面板按钮
|
shop: false, // 购买药品面板按钮
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
vmdata: any = {
|
vmdata: any = {
|
||||||
game_over: false,
|
game_over: false,
|
||||||
game_pause: false,
|
game_pause: false,
|
||||||
@@ -214,31 +228,17 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCloudData() {
|
getCloudData() {
|
||||||
gameDataSync.getCloudData();
|
gameDataSync.syncFromCloud();
|
||||||
}
|
}
|
||||||
public async overrideLocalDataWithRemote(cloudData: any) {
|
public async overrideLocalDataWithRemote(cloudData: any) {
|
||||||
try {
|
try {
|
||||||
// 直接覆盖基础游戏数据
|
// 直接覆盖基础游戏数据
|
||||||
if (cloudData.openid) {
|
if (cloudData.openid) {
|
||||||
this.openid = cloudData.openid;
|
this.data.openid = cloudData.openid;
|
||||||
}
|
}
|
||||||
// 直接覆盖出战英雄配置
|
// 直接覆盖出战英雄配置
|
||||||
if (cloudData.data) {
|
if (cloudData.data) {
|
||||||
const data = cloudData.data;
|
Object.assign(this.data, cloudData.data);
|
||||||
// 同步金币
|
|
||||||
if (data.gold !== undefined) {
|
|
||||||
this.vmdata.gold = data.gold;
|
|
||||||
}
|
|
||||||
// 同步音效开关
|
|
||||||
if (data.switch_effect !== undefined) {
|
|
||||||
this.vmdata.switch_effect = data.switch_effect;
|
|
||||||
if (oops.audio) oops.audio.switchEffect = data.switch_effect;
|
|
||||||
}
|
|
||||||
// 同步音乐开关
|
|
||||||
if (data.switch_music !== undefined) {
|
|
||||||
this.vmdata.switch_music = data.switch_music;
|
|
||||||
if (oops.audio) oops.audio.switchMusic = data.switch_music;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发UI更新
|
// 触发UI更新
|
||||||
@@ -250,9 +250,7 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
}
|
}
|
||||||
getGameDate() {
|
getGameDate() {
|
||||||
let data: GameDate = {
|
let data: GameDate = {
|
||||||
gold: this.vmdata.gold,
|
...this.data,
|
||||||
switch_effect: this.vmdata.switch_effect,
|
|
||||||
switch_music: this.vmdata.switch_music,
|
|
||||||
timestamp: Date.now() // 每次获取当前数据结构时都附带最新的时间戳
|
timestamp: Date.now() // 每次获取当前数据结构时都附带最新的时间戳
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class Initialize extends ecs.Entity {
|
|||||||
// --- 全局注入:为所有 Button 组件增加点击音效 ---
|
// --- 全局注入:为所有 Button 组件增加点击音效 ---
|
||||||
const originalOnTouchEnded = Button.prototype["_onTouchEnded"];
|
const originalOnTouchEnded = Button.prototype["_onTouchEnded"];
|
||||||
if (originalOnTouchEnded) {
|
if (originalOnTouchEnded) {
|
||||||
Button.prototype["_onTouchEnded"] = function(event) {
|
Button.prototype["_onTouchEnded"] = function (event) {
|
||||||
if (this.interactable && this.node && this.node.activeInHierarchy) {
|
if (this.interactable && this.node && this.node.activeInHierarchy) {
|
||||||
oops.audio.playEffect("music/button");
|
oops.audio.playEffect("music/button");
|
||||||
}
|
}
|
||||||
@@ -57,15 +57,15 @@ export class Initialize extends ecs.Entity {
|
|||||||
private loadCustom(queue: AsyncQueue) {
|
private loadCustom(queue: AsyncQueue) {
|
||||||
queue.push(async (next: NextFunction, params: any, args: any) => {
|
queue.push(async (next: NextFunction, params: any, args: any) => {
|
||||||
try {
|
try {
|
||||||
// 加载多语言对应字体
|
// 加载多语言对应字体
|
||||||
oops.res.load("language/font/" + oops.language.current, async () => {
|
oops.res.load("language/font/" + oops.language.current, async () => {
|
||||||
// 统一的数据加载流程
|
// 统一的数据加载流程
|
||||||
await this.loadGameDataUnified();
|
await this.loadGameDataUnified();
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
//加载精灵配置表
|
//加载精灵配置表
|
||||||
// oops.res.load("config/game/heros", next);
|
// oops.res.load("config/game/heros", next);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mLogger.error(this.debugMode, 'Initialize', "[Initialize]: 自定义内容加载失败:", error);
|
mLogger.error(this.debugMode, 'Initialize', "[Initialize]: 自定义内容加载失败:", error);
|
||||||
next(); // 即使失败也要继续,不阻塞游戏启动
|
next(); // 即使失败也要继续,不阻塞游戏启动
|
||||||
@@ -110,7 +110,7 @@ export class Initialize extends ecs.Entity {
|
|||||||
private async loadGameDataUnified() {
|
private async loadGameDataUnified() {
|
||||||
try {
|
try {
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 开始统一数据加载流程...");
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 开始统一数据加载流程...");
|
||||||
|
|
||||||
if (gameDataSync.isWxClient()) {
|
if (gameDataSync.isWxClient()) {
|
||||||
// 微信客户端:加载云端数据
|
// 微信客户端:加载云端数据
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 检测到微信客户端,使用云端数据");
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 检测到微信客户端,使用云端数据");
|
||||||
@@ -120,7 +120,7 @@ export class Initialize extends ecs.Entity {
|
|||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 非微信客户端,使用本地调试数据");
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 非微信客户端,使用本地调试数据");
|
||||||
await this.loadFromLocalDebug();
|
await this.loadFromLocalDebug();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 统一数据加载失败:", error);
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 统一数据加载失败:", error);
|
||||||
// 失败时使用默认数据 游戏需要退出
|
// 失败时使用默认数据 游戏需要退出
|
||||||
@@ -128,44 +128,21 @@ export class Initialize extends ecs.Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从云端加载数据(微信客户端)
|
* 从云端加载数据(微信客户端,sync 模式)
|
||||||
*/
|
*/
|
||||||
private async loadFromCloud() {
|
private async loadFromCloud() {
|
||||||
try {
|
try {
|
||||||
// 1. 初始化微信云环境
|
// 1. 初始化微信云环境
|
||||||
this.initWxCloudEnv();
|
this.initWxCloudEnv();
|
||||||
|
|
||||||
// 2. 登录并获取云端数据
|
|
||||||
const loginResult = await WxCloudApi.login();
|
|
||||||
const response = loginResult.result;
|
|
||||||
|
|
||||||
if (response && response.code === 200) {
|
// 2. 一次调用完成登录与双向数据同步(时间戳对比在云端完成)
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端登录成功");
|
const cloudNewer = await gameDataSync.syncFromCloud();
|
||||||
|
mLogger.log(this.debugMode, 'Initialize', `[Initialize]: 云端同步完成,云端数据较新=${cloudNewer}`);
|
||||||
const cloudData = response.data;
|
|
||||||
|
|
||||||
if (cloudData && cloudData.openid) {
|
|
||||||
smc.openid = cloudData.openid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 login 获取的 game_data 封装为统一下发格式,交给 gameDataSync 对比与合并
|
|
||||||
const formattedCloudData = {
|
|
||||||
openid: cloudData?.openid || '',
|
|
||||||
data: cloudData?.game_data || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
gameDataSync.syncWithCloudData(formattedCloudData);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端登录失败:", response?.msg);
|
|
||||||
// 登录失败时使用本地数据
|
|
||||||
gameDataSync.syncWithCloudData(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端数据加载异常:", error);
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端数据加载异常:", error);
|
||||||
// 异常时使用本地数据兜底
|
// 异常时使用本地数据兜底
|
||||||
gameDataSync.syncWithCloudData(null);
|
await gameDataSync.syncFromCloud();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +151,8 @@ export class Initialize extends ecs.Entity {
|
|||||||
*/
|
*/
|
||||||
private async loadFromLocalDebug() {
|
private async loadFromLocalDebug() {
|
||||||
try {
|
try {
|
||||||
// 用本地调试数据覆盖客户端数据
|
// 非微信客户端仅使用本地缓存数据
|
||||||
gameDataSync.syncWithCloudData(null);
|
await gameDataSync.syncFromCloud();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 本地调试数据加载异常:", error);
|
mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 本地调试数据加载异常:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,32 @@ export class WxCloudApi{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Sync game data with the cloud (two-way, timestamp-based)
|
||||||
|
* @zh 与云端双向同步游戏数据:客户端较新则上传覆盖云端,云端较新则返回云端数据
|
||||||
|
* @param gameData 本地游戏数据(需包含 timestamp 字段)
|
||||||
|
* @return 返回结果
|
||||||
|
* result.result = {
|
||||||
|
* code: number, // 200成功,其他都是失败
|
||||||
|
* msg?: string, // 失败原因
|
||||||
|
* updated: boolean, // true=云端已被本地数据覆盖;false=客户端应使用返回的云端数据
|
||||||
|
* data: {
|
||||||
|
* openid: string,
|
||||||
|
* game_data: object, // 同步后的最终数据
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static async sync(gameData: any): Promise<CloudCallFunctionResult<CloudReturnType<{
|
||||||
|
openid: string,
|
||||||
|
game_data: any
|
||||||
|
}> & { updated: boolean }>> {
|
||||||
|
return await wx?.cloud.callFunction({
|
||||||
|
name: 'cocos_cloud',
|
||||||
|
data: {
|
||||||
|
cmd: 'sync',
|
||||||
|
data: gameData
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,53 @@ exports.main = async (event, context) => {
|
|||||||
msg: `Save fail, ${JSON.stringify(saveDataRes)}`
|
msg: `Save fail, ${JSON.stringify(saveDataRes)}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (cmd === "sync") {
|
||||||
|
// 双向同步:比较客户端与云端时间戳,新的一方覆盖另一方
|
||||||
|
let clientData = event.data || {};
|
||||||
|
let user = await getOrCreaterUser(db, wxContext.OPENID);
|
||||||
|
if (!user) {
|
||||||
|
return { code: -1, msg: "Get or create user fail" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let cloudData = user.game_data || {};
|
||||||
|
let clientTs = clientData.timestamp || 0;
|
||||||
|
let cloudTs = cloudData.timestamp || 0;
|
||||||
|
|
||||||
|
if (clientTs > cloudTs) {
|
||||||
|
// 客户端较新:写入云端
|
||||||
|
let saveDataRes = await db.collection(user_db_name).doc(user._id).update({
|
||||||
|
data: {
|
||||||
|
game_data: _.set(clientData)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("Sync: client newer, saved to cloud:", saveDataRes, clientData);
|
||||||
|
if (saveDataRes?.stats?.updated >= 1) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
updated: true, // 云端已被客户端数据覆盖
|
||||||
|
data: {
|
||||||
|
openid: wxContext.OPENID,
|
||||||
|
game_data: clientData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: -1,
|
||||||
|
msg: `Sync save fail, ${JSON.stringify(saveDataRes)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 云端较新或一致:下发云端数据
|
||||||
|
console.log("Sync: cloud newer, return cloud data");
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
updated: false, // 客户端应使用云端数据
|
||||||
|
data: {
|
||||||
|
openid: wxContext.OPENID,
|
||||||
|
game_data: cloudData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user