From 9baddd54629790b8b6ed1a71d7c2b0ed4c2314cf Mon Sep 17 00:00:00 2001 From: panw Date: Tue, 28 Apr 2026 16:15:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BA=91=E7=AB=AF=E6=95=B0=E6=8D=AE=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=9C=BA=E5=88=B6=EF=BC=8C=E5=BC=95=E5=85=A5=E9=98=B2?= =?UTF-8?q?=E6=8A=96=E4=B8=8E=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GameDataSync 类,封装数据同步逻辑,支持防抖与时间戳冲突解决 - 重构 SingletonModuleComp 的云端同步方法,统一调用 GameDataSync - 优化 TalentsComp 天赋升级流程,使用新的同步机制 - 添加本地缓存支持,提升离线体验与数据恢复能力 --- assets/script/game/common/GameDataSync.ts | 143 ++++++++++++++++++ .../script/game/common/SingletonModuleComp.ts | 117 ++++++-------- assets/script/game/map/TalentsComp.ts | 8 +- 3 files changed, 194 insertions(+), 74 deletions(-) create mode 100644 assets/script/game/common/GameDataSync.ts diff --git a/assets/script/game/common/GameDataSync.ts b/assets/script/game/common/GameDataSync.ts new file mode 100644 index 00000000..fa097085 --- /dev/null +++ b/assets/script/game/common/GameDataSync.ts @@ -0,0 +1,143 @@ +import { sys } from "cc"; +import { WxCloudApi } from "../wx_clound_client_api/WxCloudApi"; +import { mLogger } from "./Logger"; +import { smc, GameDate, CloudData } from "./SingletonModuleComp"; + +export class GameDataSync { + private debugMode: boolean = false; + private _localDataDirty: boolean = false; + private _lastSyncTime: number = 0; + private _syncTimerId: any = null; + private readonly LOCAL_STORAGE_KEY = "Heros_GameData_Local"; + + /** 标记数据为脏,并更新时间戳,然后保存到本地 */ + public markDataDirty() { + this._localDataDirty = true; + this.saveToLocal(); + // 尝试触发异步同步 + this.tryAsyncCloudSync(); + } + + /** 同步数据到本地 localStorage */ + private saveToLocal() { + try { + const data = smc.getGameDate(); + data.timestamp = Date.now(); // 更新时间戳 + sys.localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(data)); + } catch (error) { + mLogger.error(this.debugMode, 'GameDataSync', '保存本地数据失败:', error); + } + } + + /** 从本地 localStorage 读取数据 */ + private loadFromLocal(): GameDate | null { + try { + const str = sys.localStorage.getItem(this.LOCAL_STORAGE_KEY); + if (str) { + return JSON.parse(str) as GameDate; + } + } catch (error) { + mLogger.error(this.debugMode, 'GameDataSync', '读取本地数据失败:', error); + } + return null; + } + + /** + * 判断是否为微信客户端 + */ + public isWxClient(): boolean { + return sys.platform === sys.Platform.WECHAT_GAME; + } + + public updateCloudData() { + this.markDataDirty(); + return true; + } + + /** 尝试异步同步云端数据,带有防抖(Debounce)保护 */ + private tryAsyncCloudSync() { + if (!this.isWxClient()) return; + + // 如果当前有同步在等待,清除之前的定时器 + if (this._syncTimerId !== null) { + clearTimeout(this._syncTimerId); + } + + // 防抖:延迟 3 秒同步,期间多次操作合并为一次同步请求 + this._syncTimerId = setTimeout(() => { + this._syncTimerId = null; + this.executeCloudSync(); + }, 3000); + } + + /** 实际执行云端同步 */ + private executeCloudSync() { + if (!this._localDataDirty) return; + + let gameData = smc.getGameDate(); + // 保证云端存一份时间戳,供下次登录对比 + gameData.timestamp = Date.now(); + + WxCloudApi.save(gameData).then((result) => { + if (result.result.code === 200) { + mLogger.log(this.debugMode, 'GameDataSync', "静默云端保存成功", result.result); + // 同步成功,清除脏标记 + this._localDataDirty = false; + this._lastSyncTime = Date.now(); + } else { + mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 静默同步失败(等待下次重试): ${result.result.msg}`); + // 失败了不清除脏标记,下次有变化或定时器检查时会再次重试 + } + }).catch((error) => { + mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 静默同步异常(等待下次重试):`, error); + }); + } + + public getCloudData() { + const localData = this.loadFromLocal(); + + // 未登录微信云端前,先用本地数据顶上(让玩家秒进游戏) + if (localData && !this.isWxClient()) { + smc.overrideLocalDataWithRemote({ data: localData }); + 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); + + // 冲突解决:基于时间戳比较(云端时间戳 > 本地时间戳 则覆盖) + let cloudTs = cloudData?.data?.timestamp || 0; + let localTs = localData?.timestamp || 0; + + if (!localData || cloudTs > localTs) { + mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据更新 (Cloud: ${cloudTs} > Local: ${localTs}), 执行覆盖。`); + 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(); + } + + 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) { + smc.overrideLocalDataWithRemote({ data: localData }); + } + }); + } +} + +export const gameDataSync = new GameDataSync(); \ No newline at end of file diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index b371719d..3984f891 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -1,3 +1,4 @@ +import { sys } from "cc"; import { VM } from "../../../../extensions/oops-plugin-framework/assets/libs/model-view/ViewModel"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { Initialize } from "../initialize/Initialize"; @@ -8,15 +9,17 @@ import { GameEvent } from "./config/GameEvent"; import { GameScoreStats } from "./config/HeroAttrs"; import { mLogger } from "./Logger"; import { TalentType } from "./config/TalentSet"; +import { gameDataSync } from "./GameDataSync"; /** * 用远程数据覆盖本地数据(统一方法) * @param remoteData 远程数据(云端或本地调试) */ -interface GameDate{ +export interface GameDate{ gold:number, heros:any, fight_hero:number, + timestamp?: number, // 用于比对本地与云端数据的最新状态 collection?: { talents: Partial>, player_level: number, @@ -26,7 +29,7 @@ interface GameDate{ friend?: {uuid:0,count:0}, } } -interface CloudData { +export interface CloudData { openid: string; data: GameDate; } @@ -212,9 +215,8 @@ export class SingletonModuleComp extends ecs.Comp { /** * 判断是否为微信客户端 */ - private isWxClient(): boolean { - // 检查是否存在微信API - return typeof wx !== 'undefined' && typeof (wx as any).getSystemInfoSync === 'function'; + isWxClient(): boolean { + return gameDataSync.isWxClient(); } finishGuide(index:number){ @@ -223,97 +225,72 @@ export class SingletonModuleComp extends ecs.Comp { } updateCloudData(){ - - let gemeDate=this.getGameDate() - WxCloudApi.save(gemeDate).then((result) => { - mLogger.log(this.debugMode, 'SMC', '云端保存') - if(result.result.code === 200) { - mLogger.log(this.debugMode, 'SMC', "保存成功",result.result) - return true - } else { - mLogger.warn(this.debugMode, 'SMC', `[SMC]: 游戏数据增加失败: ${result.result.msg}`); - return false - } - }).catch((error) => { - mLogger.error(this.debugMode, 'SMC', `[SMC]: 增加游戏数据异常:`, error); - return false - }); - return true + return gameDataSync.updateCloudData(); } + getCloudData(){ - WxCloudApi.get().then(async (result) => { - if(result.result.code === 200) { - let data=result.result.data - mLogger.log(this.debugMode, 'SMC', `[SMC]: 获取游戏数据成功:`, result.result); - this.overrideLocalDataWithRemote(data) - return true - } else { - mLogger.warn(this.debugMode, 'SMC', `[SMC]: 游戏数据增加失败`); - return false - } - }).catch((error) => { - mLogger.error(this.debugMode, 'SMC', `[SMC]: 获取游戏数据异常:`, error); - }); + gameDataSync.getCloudData(); } - public async overrideLocalDataWithRemote(CloudData) { + public async overrideLocalDataWithRemote(cloudData: any) { try { // 直接覆盖基础游戏数据 - if (CloudData.openid) { - this.openid=CloudData.openid + if (cloudData.openid) { + this.openid = cloudData.openid; } // 直接覆盖出战英雄配置 - if (CloudData.data) { - if(CloudData.data.gold) this.vmdata.gold=CloudData.data.gold - if(CloudData.data.heros) this.heros=CloudData.data.heros - if(CloudData.data.fight_hero) this.fight_hero=CloudData.data.fight_hero + if (cloudData.data) { + const data = cloudData.data; + // 同步金币 + if (data.gold !== undefined) { + this.vmdata.gold = data.gold; + } + // 同步英雄 + if (data.heros && Array.isArray(data.heros)) { + this.heros = data.heros; + } + // 同步出战英雄 + if (data.fight_hero !== undefined) { + this.fight_hero = data.fight_hero; + } // 恢复收集记录 - if(CloudData.data.collection) { - this.collection = CloudData.data.collection; + if (data.collection) { + const remoteCol = data.collection; + if (remoteCol.talents) this.collection.talents = remoteCol.talents; + if (typeof remoteCol.player_level === 'number') this.collection.player_level = remoteCol.player_level; + if (typeof remoteCol.player_exp === 'number') this.collection.player_exp = remoteCol.player_exp; + if (typeof remoteCol.talent_points === 'number') this.collection.talent_points = remoteCol.talent_points; } } + // 触发UI更新 + oops.message.dispatchEvent(GameEvent.GOLD_UPDATE); + } catch (error) { mLogger.error(this.debugMode, 'SMC', `[SMC]: 数据覆盖失败:`, error); } } getGameDate(){ - return { - gold:this.vmdata.gold, - heros:this.heros, - fight_hero:this.fight_hero, - collection: this.collection - } + let data: GameDate = { + gold: this.vmdata.gold, + heros: this.heros, + fight_hero: this.fight_hero, + collection: this.collection, + timestamp: Date.now() // 每次获取当前数据结构时都附带最新的时间戳 + }; + return data; } addHero(hero_uuid:number){ if(this.heros.indexOf(hero_uuid)==-1){ this.heros.push(hero_uuid) + gameDataSync.markDataDirty(); } - if(this.isWxClient()){ - let res = this.updateCloudData() - if (res){ - return true - }else{ - // 同步不成功,删除uuid - this.heros.splice(this.heros.indexOf(hero_uuid), 1); - oops.gui.toast("数据同步失败,已回滚操作"); - return false - } - } - return true } updateGold(gold:number, is_sync: boolean = true){ this.vmdata.gold += gold; - if(this.isWxClient() && is_sync){ - let res = this.updateCloudData() - if (res){ - oops.message.dispatchEvent(GameEvent.GOLD_UPDATE) - return true - }else{ - this.vmdata.gold -= gold - return false - } + if (is_sync) { + gameDataSync.markDataDirty(); } oops.message.dispatchEvent(GameEvent.GOLD_UPDATE) return true diff --git a/assets/script/game/map/TalentsComp.ts b/assets/script/game/map/TalentsComp.ts index 11f46860..0d7b79d7 100644 --- a/assets/script/game/map/TalentsComp.ts +++ b/assets/script/game/map/TalentsComp.ts @@ -202,15 +202,15 @@ export class TalentsComp extends CCComp { let points = collection.talent_points || 0; if (points >= cost && currentLevel < 5) { - // 扣除天赋点 + // 1. 扣除消耗 collection.talent_points -= cost; - // 增加天赋等级 + // 2. 更新等级 collection.talents[talentId] = currentLevel + 1; - // 同步到云端 + // 3. 同步数据(通过 SingletonModuleComp 新增的机制,这里会触发标记脏数据并自动尝试云端同步) smc.updateCloudData(); - // 刷新界面 + // 4. 刷新 UI this.refreshUI(); oops.gui.toast("天赋升级成功");