Skip to content
存储玩家数据

存储玩家数据

预计阅读时间 15 分钟

本章我们要实现玩家存档的功能,将玩家解锁的家居信息与金币数量存储起来。

前面几章我们已经将核心玩法基本完成了,生成金币、消耗金币、购买等逻辑功能都已经完成,本章我们来实现玩家存档功能(数据持久化),将前面的数据保存到服务器。

PlayerData 玩家数据持久化

将一个变量永久存储

在 subData 的子类中,要将一个属性永久保存在存档中可以使用装饰器 @Decorator.persistence() 来修饰它。

  • 在 PlayerData 脚本中给玩家拥有的金币数量属性加上装饰器。
TypeScript
/** 金币数量 */
@Decorator.persistence()
public gold: number = 55;
/** 金币数量 */
@Decorator.persistence()
public gold: number = 55;
  • 在 PlayerData 脚本中添加玩家的金币增长基数。
TypeScript
/** 金币增长速率 */
@Decorator.persistence()
public goldGrowthRate: number = 1;
/** 金币增长速率 */
@Decorator.persistence()
public goldGrowthRate: number = 1;
  • 在 PlayerData 脚本中添加玩家的建筑解锁进度。
TypeScript
/** 当前解锁到的组 */
@Decorator.persistence()
public curGroupId: number = 0;

/** 这个组已解锁的建筑序号列表 */
@Decorator.persistence()
public unlockedIds: number[] = [];
/** 当前解锁到的组 */
@Decorator.persistence()
public curGroupId: number = 0;

/** 这个组已解锁的建筑序号列表 */
@Decorator.persistence()
public unlockedIds: number[] = [];
  • 在 PlayerData 脚本中,找到上一节编写的 changeGold 函数,调用 save 函数,将金币保存到存档中,并同步给客户端。
typescript
public changeGold(deltaNum: number) {
    this.gold += deltaNum;
    // 服务端改变金币,将这个操作同步给客户端
    this.syncToClient();
    this.onGoldChange.call(this.gold);
    // 保存到存档中并同步客户端 
    this.save(true); 
}
public changeGold(deltaNum: number) {
    this.gold += deltaNum;
    // 服务端改变金币,将这个操作同步给客户端
    this.syncToClient();
    this.onGoldChange.call(this.gold);
    // 保存到存档中并同步客户端 
    this.save(true); 
}

接入建筑解锁信息

  • 在 PlayerData 脚本中添加新函数用于改变金币增长基数、更新建筑解锁信息。在 updateBuildUnlockInfo 函数中,我们新加了一个序号的概念,这个序号之后会添加到 BuildInfo 中,用来标记某个家具。
TypeScript
/**
 * 更改金币增长基数
 * @param deltaNum 改变值
 */
public changeGoldGrowthRate(deltaNum: number) {
    this.goldGrowthRate += deltaNum;
    // 保存到存档中并同步客户端
    this.save(true);
}

/**
 * 更新建筑解锁信息
 * @param groupId 组id
 * @param sId 序号
 */
public updateBuildUnlockInfo(groupId: number, sId: number) {
    // 判断当前组号是否与要解锁的组合相同,如果不同说明当前组全部解锁完毕了
    if (this.curGroupId != groupId) {
        // 解锁组改变,需要先清空解锁建筑列表
        this.unlockedIds.length = 0;
        this.curGroupId = groupId;
    }
    // 将新解锁的建筑的序号保存到已解锁数组中
    this.unlockedIds.push(sId);
    // 保存到存档中并同步客户端
    this.save(true);
}
/**
 * 更改金币增长基数
 * @param deltaNum 改变值
 */
public changeGoldGrowthRate(deltaNum: number) {
    this.goldGrowthRate += deltaNum;
    // 保存到存档中并同步客户端
    this.save(true);
}

/**
 * 更新建筑解锁信息
 * @param groupId 组id
 * @param sId 序号
 */
public updateBuildUnlockInfo(groupId: number, sId: number) {
    // 判断当前组号是否与要解锁的组合相同,如果不同说明当前组全部解锁完毕了
    if (this.curGroupId != groupId) {
        // 解锁组改变,需要先清空解锁建筑列表
        this.unlockedIds.length = 0;
        this.curGroupId = groupId;
    }
    // 将新解锁的建筑的序号保存到已解锁数组中
    this.unlockedIds.push(sId);
    // 保存到存档中并同步客户端
    this.save(true);
}
  • BuildInfo 中新增序号属性: id,它用来标记家具是某个组中的哪一个。这个序号会存储在数据模块 unlockedIds 数组中。
TypeScript
/** 显示创建按钮的组别 */
@Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
public id: number = 1;
/** 显示创建按钮的组别 */
@Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
public id: number = 1;
  • 因为我们的建筑物解锁信息存储在了服务器,所以初始化函数也需要更改 。
  • 我们创建一个服务端RPC方法,res_init ,它将用来同步玩家刚上线时,建筑物的解锁状态。
  • 我们创建一个 req_init 函数,在客户端准备完成后,用来向服务器请求初始化家具。
  • 这两个函数替代了上一节写在 onPlayerEnter 函数的玩家上线建筑同步逻辑,所以这里将 onPlayerEnter 中的代码删除掉,同样把之前在 onStart 中解锁默认家居的方法删除掉。
TypeScript
protected onStart(): void {
    // 客户端请求建筑 解锁信息来初始化 
    if (SystemUtil.isClient()) { 
        this.req_init(); 
        return; 
    } 

    // --------------------------------------服务端操作--------------------------------------

    // 开启服务端的onUpdate
    this.useUpdate = true;

    // 默认隐藏,并显示第0组解锁建筑按钮
    this.gameObject.setVisibility(PropertyStatus.Off);
    // 关闭碰撞
    (this.gameObject as Model).setCollision(PropertyStatus.Off);

	// 显示默认 id 为0 的家具
    // if (this.groupId === 0) {
    //     this.initUnlockBtn();
    // } else {
    // //  监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    // this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    // }

    // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号 
    this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this)); // [!code focus] // [!code ++]
}

/** 服务端响应客户端初始化 */ 
@RemoteFunction(Server) 
public async res_init(curGroupId: number, unlockedIds: number[]) { 
    // 如果当前要解锁的组小于已经解锁完毕的组 说明这个组已经全部解锁了 
    if (curGroupId > this.groupId) { 
        this.gameObject.setVisibility(PropertyStatus.On); 
        (this.gameObject as Model).setCollision(PropertyStatus.On); 
    } 
    if (curGroupId === this.groupId) { 
        if (unlockedIds.includes(this.id)) { 
            // 如果解锁列表中包含这个家居的序号,说明已经解锁了 我们将它显示出来 并且发送事件通知解锁下一个家居的解锁按钮 
            this.gameObject.setVisibility(PropertyStatus.On); 
            (this.gameObject as Model).setCollision(PropertyStatus.On); 
            // 显示下一组解锁按钮 
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1)); 
        } else { 
            this.initUnlockBtn(); 
        } 
    } 
}

/** 请求客户端的解锁信息来完成初始化 */ 
public async req_init() { 
    // 等到客户端的数据中心准备好 
    await DataCenterC.ready(); 
    const playerData = DataCenterC.getData(PlayerData); 
    this.res_init(playerData.curGroupId, playerData.unlockedIds); 
} 

/** 
 * 玩家进入房间,初始化已经显示出来的世界UI 
 * @param player 上线的玩家 
 */ 
protected onPlayerEnter(player: Player) {  
    // 当前建筑按钮显示且当前建筑隐藏  
    // if (this._unlockBtn && !this.gameObject.getVisibility()) {  
    //     this.initWorldUIOnlyOne(player, this._unlockBtn.guid);  
    // } 
} 
protected onStart(): void {
    // 客户端请求建筑 解锁信息来初始化 
    if (SystemUtil.isClient()) { 
        this.req_init(); 
        return; 
    } 

    // --------------------------------------服务端操作--------------------------------------

    // 开启服务端的onUpdate
    this.useUpdate = true;

    // 默认隐藏,并显示第0组解锁建筑按钮
    this.gameObject.setVisibility(PropertyStatus.Off);
    // 关闭碰撞
    (this.gameObject as Model).setCollision(PropertyStatus.Off);

	// 显示默认 id 为0 的家具
    // if (this.groupId === 0) {
    //     this.initUnlockBtn();
    // } else {
    // //  监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    // this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    // }

    // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号 
    this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this)); // [!code focus] // [!code ++]
}

/** 服务端响应客户端初始化 */ 
@RemoteFunction(Server) 
public async res_init(curGroupId: number, unlockedIds: number[]) { 
    // 如果当前要解锁的组小于已经解锁完毕的组 说明这个组已经全部解锁了 
    if (curGroupId > this.groupId) { 
        this.gameObject.setVisibility(PropertyStatus.On); 
        (this.gameObject as Model).setCollision(PropertyStatus.On); 
    } 
    if (curGroupId === this.groupId) { 
        if (unlockedIds.includes(this.id)) { 
            // 如果解锁列表中包含这个家居的序号,说明已经解锁了 我们将它显示出来 并且发送事件通知解锁下一个家居的解锁按钮 
            this.gameObject.setVisibility(PropertyStatus.On); 
            (this.gameObject as Model).setCollision(PropertyStatus.On); 
            // 显示下一组解锁按钮 
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1)); 
        } else { 
            this.initUnlockBtn(); 
        } 
    } 
}

/** 请求客户端的解锁信息来完成初始化 */ 
public async req_init() { 
    // 等到客户端的数据中心准备好 
    await DataCenterC.ready(); 
    const playerData = DataCenterC.getData(PlayerData); 
    this.res_init(playerData.curGroupId, playerData.unlockedIds); 
} 

/** 
 * 玩家进入房间,初始化已经显示出来的世界UI 
 * @param player 上线的玩家 
 */ 
protected onPlayerEnter(player: Player) {  
    // 当前建筑按钮显示且当前建筑隐藏  
    // if (this._unlockBtn && !this.gameObject.getVisibility()) {  
    //     this.initWorldUIOnlyOne(player, this._unlockBtn.guid);  
    // } 
} 
  • 在解锁建筑时,需要保存当前解锁的建筑以及持久化金币增长基数,我们在 PlayerModuleS 中添加持久化的方法
typescript
import { PlayerData } from "./PlayerData";
import { PlayerModuleC } from "./PlayerModuleC";

export class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerData> {

    /**
     * 客户端改变金币的rpc方法
     * @param deltaNum 要改变的数量
     */
    public net_changeGold(deltaNum: number): boolean {
        return this.changeGold(this.currentPlayerId, deltaNum);
    } 

    public net_changeGoldGrowthRate(deltaNum: number): void { 
        this.changeGoldGrowthRate(this.currentPlayerId, deltaNum); 
    } 

    public net_updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.updateBuildUnlockInfo(this.currentPlayerId, groupId, sId); 
    } 

    /**
     * 改变金币
     * @param pid 要改变金币数量的玩家id
     * @param deltaNum 改变的数量
     */
    public changeGold(pid: number, deltaNum: number): boolean {
        // 获取玩家pid的数据
        const data = this.getPlayerData(pid);
        // 要改变的值是负数,且钱不够
        if (deltaNum < 0 && data.gold < Math.abs(deltaNum)) {
            return false;
        }
        data.changeGold(deltaNum);
        return true;
    }

    /** 
     * 改变金币增长速率 
     * @param pid 要改变金币数量的玩家id 
     * @param deltaNum 改变的数量 
     */ 
    public changeGoldGrowthRate(pid: number, deltaNum: number) { 
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid); 
        data.changeGoldGrowthRate(deltaNum); 
    } 

    /** 
     * 服务端更新建筑解锁信息 
     * @param pid 玩家id 
     * @param groupId 组号 
     * @param sId 序号 
     */ 
    public updateBuildUnlockInfo(pid: number, groupId: number, sId: number) { 
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid); 
        data.updateBuildUnlockInfo(groupId, sId); 
    } 
}
import { PlayerData } from "./PlayerData";
import { PlayerModuleC } from "./PlayerModuleC";

export class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerData> {

    /**
     * 客户端改变金币的rpc方法
     * @param deltaNum 要改变的数量
     */
    public net_changeGold(deltaNum: number): boolean {
        return this.changeGold(this.currentPlayerId, deltaNum);
    } 

    public net_changeGoldGrowthRate(deltaNum: number): void { 
        this.changeGoldGrowthRate(this.currentPlayerId, deltaNum); 
    } 

    public net_updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.updateBuildUnlockInfo(this.currentPlayerId, groupId, sId); 
    } 

    /**
     * 改变金币
     * @param pid 要改变金币数量的玩家id
     * @param deltaNum 改变的数量
     */
    public changeGold(pid: number, deltaNum: number): boolean {
        // 获取玩家pid的数据
        const data = this.getPlayerData(pid);
        // 要改变的值是负数,且钱不够
        if (deltaNum < 0 && data.gold < Math.abs(deltaNum)) {
            return false;
        }
        data.changeGold(deltaNum);
        return true;
    }

    /** 
     * 改变金币增长速率 
     * @param pid 要改变金币数量的玩家id 
     * @param deltaNum 改变的数量 
     */ 
    public changeGoldGrowthRate(pid: number, deltaNum: number) { 
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid); 
        data.changeGoldGrowthRate(deltaNum); 
    } 

    /** 
     * 服务端更新建筑解锁信息 
     * @param pid 玩家id 
     * @param groupId 组号 
     * @param sId 序号 
     */ 
    public updateBuildUnlockInfo(pid: number, groupId: number, sId: number) { 
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid); 
        data.updateBuildUnlockInfo(groupId, sId); 
    } 
}
  • 在 PlayerModuleC 中也需要添加对应方法
typescript
import MainUI from "../../ui/MainUI";
import { PlayerData } from "./PlayerData";
import { PlayerModuleS } from "./PlayerModuleS";

export class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerData> {

    protected override onStart(): void {
        // 显示mainUI
        UIService.show(MainUI);
    }

    /**
     * 客户端改变金币
     * @param deltaNum 要改变的数量
     */
    public async changeGold(deltaNum: number): Promise<boolean> {
        return this.server.net_changeGold(deltaNum);
    }

    public changeGoldGrowthRate(deltaNum: number): void { 
        this.server.net_changeGoldGrowthRate(deltaNum); 
    } 

    public updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.server.net_updateBuildUnlockInfo(groupId, sId); 
    } 
}
import MainUI from "../../ui/MainUI";
import { PlayerData } from "./PlayerData";
import { PlayerModuleS } from "./PlayerModuleS";

export class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerData> {

    protected override onStart(): void {
        // 显示mainUI
        UIService.show(MainUI);
    }

    /**
     * 客户端改变金币
     * @param deltaNum 要改变的数量
     */
    public async changeGold(deltaNum: number): Promise<boolean> {
        return this.server.net_changeGold(deltaNum);
    }

    public changeGoldGrowthRate(deltaNum: number): void { 
        this.server.net_changeGoldGrowthRate(deltaNum); 
    } 

    public updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.server.net_updateBuildUnlockInfo(groupId, sId); 
    } 
}
  • 之后在 BuildInfo 中解锁的地方调 PlayerModuleC 中的方法
TypeScript
/**
 * 初始化解锁建筑按钮
 */
protected async initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);

    // 初始化所有玩家的世界UI
    this.initWorldUIAllPlayer(this._unlockBtn.guid);

    // 防御性编程,防止解锁按钮没创建出来报错阻碍游戏进程
    if (this._unlockBtn) {

        // 设置按钮的父节点为当前对象
        this._unlockBtn.parent = this.gameObject;
        // 设置按钮的相对位置
        this._unlockBtn.localTransform.position = this.unlockBtnLoc;

        this._unlockBuildFun = (other: GameObject) => {
            // 判断进入的对象是一个Character实例才创建
            if (other instanceof Character) {
                // 钱够吗
                const isGoldEnough = ModuleService.getModule(PlayerModuleS).changeGold(other.player.playerId, -this.unlockPrice);

                // 扣钱成功才显示
                if (isGoldEnough) {
                    // 用完了就先取消绑定
                    trigger.onEnter.remove(this._unlockBuildFun);
                    // 对象池回收解锁按钮
                    GameObjPool.despawn(this._unlockBtn);
                    // 显示这个模型
                    this.showBuild();

                    // 金币增长基数
                    Event.dispatchToClient(other.player, "GoldGrowthRate", this.profit);
                    // 持久化增长基数 
                    ModuleService.getModule(PlayerModuleS).changeGoldGrowthRate(other.player.playerId, this.profit); 
                    // 更新解锁建筑组信息 
                    ModuleService.getModule(PlayerModuleS).updateBuildUnlockInfo(other.player.playerId, this.groupId, this.id); 
                } else {
                    console.error("钱不够!");
                }
            }
        }

        // 拿到解锁按钮预制体下面的触发器
        const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
        // 绑定触发器的进入事件
        trigger.onEnter.add(this._unlockBuildFun);
    } else {
        console.error("初始化解锁按钮失败,请检查是不是spawn的guid");
    }
}
/**
 * 初始化解锁建筑按钮
 */
protected async initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);

    // 初始化所有玩家的世界UI
    this.initWorldUIAllPlayer(this._unlockBtn.guid);

    // 防御性编程,防止解锁按钮没创建出来报错阻碍游戏进程
    if (this._unlockBtn) {

        // 设置按钮的父节点为当前对象
        this._unlockBtn.parent = this.gameObject;
        // 设置按钮的相对位置
        this._unlockBtn.localTransform.position = this.unlockBtnLoc;

        this._unlockBuildFun = (other: GameObject) => {
            // 判断进入的对象是一个Character实例才创建
            if (other instanceof Character) {
                // 钱够吗
                const isGoldEnough = ModuleService.getModule(PlayerModuleS).changeGold(other.player.playerId, -this.unlockPrice);

                // 扣钱成功才显示
                if (isGoldEnough) {
                    // 用完了就先取消绑定
                    trigger.onEnter.remove(this._unlockBuildFun);
                    // 对象池回收解锁按钮
                    GameObjPool.despawn(this._unlockBtn);
                    // 显示这个模型
                    this.showBuild();

                    // 金币增长基数
                    Event.dispatchToClient(other.player, "GoldGrowthRate", this.profit);
                    // 持久化增长基数 
                    ModuleService.getModule(PlayerModuleS).changeGoldGrowthRate(other.player.playerId, this.profit); 
                    // 更新解锁建筑组信息 
                    ModuleService.getModule(PlayerModuleS).updateBuildUnlockInfo(other.player.playerId, this.groupId, this.id); 
                } else {
                    console.error("钱不够!");
                }
            }
        }

        // 拿到解锁按钮预制体下面的触发器
        const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
        // 绑定触发器的进入事件
        trigger.onEnter.add(this._unlockBuildFun);
    } else {
        console.error("初始化解锁按钮失败,请检查是不是spawn的guid");
    }
}
  • 在 MailBox 脚本中,对于玩家进游戏时,等待客户端数据中心准备好后 ,初始化金币增长速率
TypeScript
public async init() {
    //  等待这个模型在客户端加载好
    await this.gameObject.asyncReady();

    // 等到客户端的数据中心准备好 
    await DataCenterC.ready(); 
    // 初始化金币增长速率 
    this._alterNum = DataCenterC.getData(PlayerData).goldGrowthRate; 

    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
    // 拿到targetUI
    const targetUI = worldUI.getTargetUIWidget();
    // 拿到文本控件
    this._goldNumTxt = targetUI.findChildByPath("RootCanvas/goldNumTxt") as TextBlock;
    this._addGoldTxt = targetUI.findChildByPath("RootCanvas/addGoldPerSecTxt") as TextBlock;
    // 初始化文本
    this._goldNumTxt.text = this._goldNum.toString();
    this._addGoldTxt.text = this._alterNum + "/秒";
    // 定时器
    this.inter = setInterval(() => {
        this._goldNum += this._alterNum;
        this._goldNumTxt.text = this._goldNum.toString();
    }, 1000);

    const trigger = this.gameObject.getChildByName("触发器") as Trigger;
    trigger.onEnter.add(() => {
        ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
        this._goldNum = 0;
        this._goldNumTxt.text = this._goldNum.toString();
    });

    // 监听金币基数改变的事件 
    Event.addServerListener("GoldGrowthRate", (deltaNum: number) => {
        this._alterNum += deltaNum;
        this._addGoldTxt.text = this._alterNum + "/秒";
    })
}
public async init() {
    //  等待这个模型在客户端加载好
    await this.gameObject.asyncReady();

    // 等到客户端的数据中心准备好 
    await DataCenterC.ready(); 
    // 初始化金币增长速率 
    this._alterNum = DataCenterC.getData(PlayerData).goldGrowthRate; 

    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
    // 拿到targetUI
    const targetUI = worldUI.getTargetUIWidget();
    // 拿到文本控件
    this._goldNumTxt = targetUI.findChildByPath("RootCanvas/goldNumTxt") as TextBlock;
    this._addGoldTxt = targetUI.findChildByPath("RootCanvas/addGoldPerSecTxt") as TextBlock;
    // 初始化文本
    this._goldNumTxt.text = this._goldNum.toString();
    this._addGoldTxt.text = this._alterNum + "/秒";
    // 定时器
    this.inter = setInterval(() => {
        this._goldNum += this._alterNum;
        this._goldNumTxt.text = this._goldNum.toString();
    }, 1000);

    const trigger = this.gameObject.getChildByName("触发器") as Trigger;
    trigger.onEnter.add(() => {
        ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
        this._goldNum = 0;
        this._goldNumTxt.text = this._goldNum.toString();
    });

    // 监听金币基数改变的事件 
    Event.addServerListener("GoldGrowthRate", (deltaNum: number) => {
        this._alterNum += deltaNum;
        this._addGoldTxt.text = this._alterNum + "/秒";
    })
}
  • 运行游戏,解锁和持有一些金币后,关闭游戏,再次进入游戏,测试存档是否还在。

TIP

为了性能考虑,服务端每 10 秒保存一次存档。在 PC 测试时请不要在调用保存数据方法后马上关闭服务端。

  • 查看本地的 DBCache 存档 (这个就是玩家的存档,方便我们在测试时检查问题)
    • 找一个目录右键,选择“打开文件所在的位置”
    • 找到这个项目“TycoonDemo”的根目录
    • 点击“DBChache”文件夹,即可查看自己的存档

  • 如果需要重新测试,把这个存档删掉即可
    • 点击工程按钮
    • 点击“删除 PIE 缓存”

img

完整代码

当前 MailBox 脚本完整代码(点击展开)
typescript
import PlayerData from "../modules/player/PlayerData";
import PlayerModuleC from "../modules/player/PlayerModuleC";

@Component
export default class MailBox extends Script {

    /** 当前金币 */
    private _goldNum: number = 0;

    /** 金币增加基数 */
    private _alterNum: number = 1;

    /** 金币数量文本控件 */
    private _goldNumTxt: TextBlock;

    /** 金币增加基数文本控件 */
    private _addGoldTxt: TextBlock;

    /** 定时器对象 */
    private inter: any;

    protected onStart(): void {
        // 如果是服务端直接退出
        if (SystemUtil.isServer()) return;

        this.init();
    }

    public async init() {
        // 等待邮箱模型加载好
        await this.gameObject.asyncReady();
        // 等待客户端数据中心准备好
        await DataCenterC.ready();
        // 初始化金币增长速率
        this._alterNum = DataCenterC.getData(PlayerData).goldGrowthRate;
        // 拿到世界UI逻辑对象
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 拿到targetUI
        const targetUI = worldUI.getTargetUIWidget();
        // 拿到文本控件
        this._goldNumTxt = targetUI.findChildByPath("RootCanvas/goldNumTxt") as TextBlock;
        this._addGoldTxt = targetUI.findChildByPath("RootCanvas/addGoldPerSecTxt") as TextBlock;
        // 初始化文本控件内容
        this._goldNumTxt.text = this._goldNum.toString();
        this._addGoldTxt.text = this._alterNum + "/秒";
        // 创建定时器
        this.inter = setInterval(() => {
            this._goldNum += this._alterNum;
            this._goldNumTxt.text = this._goldNum.toString();
        }, 1000);

        // 获取触发器 添加进入事件 进入之后获取金币
        const trigger = this.gameObject.getChildByName("触发器") as Trigger;
        trigger.onEnter.add(() => {
            ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
            this._goldNum = 0;
            this._goldNumTxt.text = this._goldNum.toString();
        });

        // 监听金币增加基数改变的事件
        Event.addServerListener("GoldGrowthRate", (deltaNum: number) => {
            this._alterNum += deltaNum;
            this._addGoldTxt.text = this._alterNum + "/秒";
        })
    }

    protected onDestroy(): void {
        // 销毁计时器
        if(this.inter){
            clearInterval(this.inter);
            this.inter = null;
        }
    }

}
import PlayerData from "../modules/player/PlayerData";
import PlayerModuleC from "../modules/player/PlayerModuleC";

@Component
export default class MailBox extends Script {

    /** 当前金币 */
    private _goldNum: number = 0;

    /** 金币增加基数 */
    private _alterNum: number = 1;

    /** 金币数量文本控件 */
    private _goldNumTxt: TextBlock;

    /** 金币增加基数文本控件 */
    private _addGoldTxt: TextBlock;

    /** 定时器对象 */
    private inter: any;

    protected onStart(): void {
        // 如果是服务端直接退出
        if (SystemUtil.isServer()) return;

        this.init();
    }

    public async init() {
        // 等待邮箱模型加载好
        await this.gameObject.asyncReady();
        // 等待客户端数据中心准备好
        await DataCenterC.ready();
        // 初始化金币增长速率
        this._alterNum = DataCenterC.getData(PlayerData).goldGrowthRate;
        // 拿到世界UI逻辑对象
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 拿到targetUI
        const targetUI = worldUI.getTargetUIWidget();
        // 拿到文本控件
        this._goldNumTxt = targetUI.findChildByPath("RootCanvas/goldNumTxt") as TextBlock;
        this._addGoldTxt = targetUI.findChildByPath("RootCanvas/addGoldPerSecTxt") as TextBlock;
        // 初始化文本控件内容
        this._goldNumTxt.text = this._goldNum.toString();
        this._addGoldTxt.text = this._alterNum + "/秒";
        // 创建定时器
        this.inter = setInterval(() => {
            this._goldNum += this._alterNum;
            this._goldNumTxt.text = this._goldNum.toString();
        }, 1000);

        // 获取触发器 添加进入事件 进入之后获取金币
        const trigger = this.gameObject.getChildByName("触发器") as Trigger;
        trigger.onEnter.add(() => {
            ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
            this._goldNum = 0;
            this._goldNumTxt.text = this._goldNum.toString();
        });

        // 监听金币增加基数改变的事件
        Event.addServerListener("GoldGrowthRate", (deltaNum: number) => {
            this._alterNum += deltaNum;
            this._addGoldTxt.text = this._alterNum + "/秒";
        })
    }

    protected onDestroy(): void {
        // 销毁计时器
        if(this.inter){
            clearInterval(this.inter);
            this.inter = null;
        }
    }

}
当前 BuildInfo 脚本完整代码(点击展开)
typescript
import PlayerData from "../modules/player/PlayerData";
import PlayerModuleS from "../modules/player/PlayerModuleS";

@Component
export default class BuildInfo extends Script {

    /** 显示创建按钮的组别 */
    @Property({ group: "基本信息", tooltip: "组号,用来确认显示建造按钮的组,配置时需保证组号之间是衔接的,即第一组从0开始,第二组就是1" })
    public groupId: number = 0;

    /** 这个建筑解锁按钮的相对位置 */
    @Property({ group: "基本信息", displayName: "解锁按钮的相对位置", tooltip: "指当将这个建筑设置为父节点时,子节点的相对位置relativeLocation" })
    public unlockBtnLoc: Vector = Vector.zero;

    /** 显示这个按钮需要的前置解锁家具数量 */
    @Property({ group: "基本信息", displayName: "需要数量", tooltip: "显示这个解锁按钮组,需要多少前置解锁" })
    public needs: number = 1;

    /** 解锁价格 */
    @Property({ group: "基本信息", displayName: "解锁价格" })
    public unlockPrice: number = 10;

    @Property({ group: "基本信息", displayName: "每秒带来收益" })
    public profit: number = 1;

    @Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
    public id: number = 1;

    /** 事件监听器 需要在解锁按钮回收时注销 */
    private _listener: EventListener;

    /** 显示当前解锁按钮组进度 */
    private _curPro: number = 0;

    /** 进入触发器事件 */
    private _unlockbuildFun = null;

    /** 解锁按钮 */
    private _unlockBtn: GameObject = null;

    protected onStart(): void {

        // 客户端直接 返回
        if (SystemUtil.isClient()) {
            this.req_init();
            return;
        }

        // 开启服务端 onUpdate 方法
        this.useUpdate = true;

        // 关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        // 关闭显示
        this.gameObject.setVisibility(PropertyStatus.Off);
        
        //  监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    }

    /**
     * 服务端响应客户端初始化
     * @param curGroupId 组id
     * @param unlockedIds 当前组解锁的家具id列表
     */
    @RemoteFunction(Server)
    public res_init(curGroupId: number, unlockedIds: number[]) {
        // 如果当前要解锁的组小于已经解锁完毕的组 说明这个组已经全部解锁了 
        if (curGroupId > this.groupId) {
            this.gameObject.setVisibility(PropertyStatus.On);
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        }
        if (curGroupId === this.groupId) {
            // 如果解锁列表中包含这个家居的序号,说明已经解锁了 我们将它显示出来 并且发送事件通知解锁下一个家居的解锁按钮
            if (unlockedIds.includes(this.id)) {
                this.gameObject.setVisibility(PropertyStatus.On);
                (this.gameObject as Model).setCollision(PropertyStatus.On);
                Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
            } else {
                this.initUnlockBtn();
            }
        }
    }

    /**
     * 向服务端发送解锁请求
     */
    public async req_init() {
        // 等待客户端数据中心准备好
        await DataCenterC.ready();
        const playerData = DataCenterC.getData(PlayerData);
        this.res_init(playerData.curGroupId, playerData.unlockedIds);
    }

    protected onUpdate(dt: number): void {
        TweenUtil.TWEEN.update();
    }

    /**
     * 验证是否满足解锁条件
     */
    private ensureNeeds() {
        // 满足条件就显示解锁按钮
        if (++this._curPro >= this.needs) {
            // 先注销 listener
            this._listener.disconnect();
            // 初始化按钮
            this.initUnlockBtn();
        }
    }

    /**
     * 给指定玩家初始化 UI
     * @param player 玩家
     * @param unlockBtnGuid 按钮GUID
     */
    @RemoteFunction(Client)
    private initWorldUIOnlyOne(player: Player, unlockBtnGuid: string): void {
        this.initWorldUI(unlockBtnGuid);
    }

    /**
     * 初始化所有玩家的世界UI
     * @param unlockBtnGuid 按钮GUID
     */
    @RemoteFunction(Client, Multicast)
    private initWorldUIAllPlayer(unlockBtnGuid: string): void {
        this.initWorldUI(unlockBtnGuid);
    }

    /**
     * 客户端初始化世界UI
     * @param unlockBtnGuid 解锁按钮的GUID
     */
    private async initWorldUI(unlockBtnGuid: string): Promise<void> {
        // 异步查找按钮
        this._unlockBtn = await GameObject.asyncFindGameObjectById(unlockBtnGuid);
        const worldUI = this._unlockBtn.getChildByName("世界UI") as UIWidget;
        const targetUI = worldUI.getTargetUIWidget();
        const buildName = targetUI.findChildByPath("RootCanvas/buildNameTxt") as TextBlock;
        const buildNeeds = targetUI.findChildByPath("RootCanvas/buildNeedsTxt") as TextBlock;
        buildName.text = this.gameObject.name;
        buildNeeds.text = this.unlockPrice.toString();
    }

    /**
     * 初始化解锁建筑按钮
     */
    private async initUnlockBtn(): Promise<void> {
        // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
        this._unlockBtn = await GameObjPool.asyncSpawn("FE08DDF04F44547200C8CF9E415D3904", GameObjPoolSourceType.Prefab);
        // 初始化所有玩家的世界UI
        this.initWorldUIAllPlayer(this._unlockBtn.gameObjectId);
        // 设置父节点为当前对象
        this._unlockBtn.parent = this.gameObject;
        // 设置按钮的相对位置
        this._unlockBtn.localTransform.position = this.unlockBtnLoc;
        // 获取预制体下的触发器
        const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
        this._unlockbuildFun = (other: GameObject) => {
            // 判断进入触发器的物体是否为玩家
            if (other instanceof Character) {
                // 判断钱是否足够
                const isGoldEnough = ModuleService.getModule(PlayerModuleS).changeGold(other.player.playerId, -this.unlockPrice);
                // 如果钱够
                if (isGoldEnough) {
                    // 解绑触发器进入事件
                    trigger.onEnter.remove(this._unlockbuildFun);
                    // 回收按钮模型
                    GameObjPool.despawn(this._unlockBtn);
                    // 使用动画 显示建筑模型 & 开启碰撞 
                    this.showBuild();
                    // 通知客户端金币增加基数改变
                    Event.dispatchToClient(other.player, "GoldGrowthRate", this.profit);
                    // 更改金币增长基数
                    ModuleService.getModule(PlayerModuleS).changeGoldGrowthRate(other.player.playerId, this.profit);
                    // 更新解锁建筑的信息
                    ModuleService.getModule(PlayerModuleS).updateBuildUnlockInfo(other.player.playerId, this.groupId, this.id);
                } else {
                    console.error("钱不够!");
                }

            }
        }
        // 绑定到触发器进入事件
        trigger.onEnter.add(this._unlockbuildFun);
    }

    /**
     * 显示建筑
     */
    private showBuild(): void {
        // 定义一个 tween 要改变的数值是物体的 scale ,它的初始值是 {x:0,y:0,z:0}
        const tween = new Tween({ scale: Vector.zero });
        // 更改家居默认的缩放,在 500 毫秒内完成
        tween.to({ scale: this.gameObject.worldTransform.scale.clone() }, 500);
        // 启动tween时显示建筑
        tween.onStart(() => {
            // 显示模型
            this.gameObject.setVisibility(PropertyStatus.On);
            // 开启碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        });
        // 设置 tween 每帧更新时更新缩放
        tween.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale });
        // 动画完成时关闭 useUpdate 减少无用调用
        tween.onComplete(() => {
            this.useUpdate = false;
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
        });
        // 启动动画
        tween.start();
    }

}
import PlayerData from "../modules/player/PlayerData";
import PlayerModuleS from "../modules/player/PlayerModuleS";

@Component
export default class BuildInfo extends Script {

    /** 显示创建按钮的组别 */
    @Property({ group: "基本信息", tooltip: "组号,用来确认显示建造按钮的组,配置时需保证组号之间是衔接的,即第一组从0开始,第二组就是1" })
    public groupId: number = 0;

    /** 这个建筑解锁按钮的相对位置 */
    @Property({ group: "基本信息", displayName: "解锁按钮的相对位置", tooltip: "指当将这个建筑设置为父节点时,子节点的相对位置relativeLocation" })
    public unlockBtnLoc: Vector = Vector.zero;

    /** 显示这个按钮需要的前置解锁家具数量 */
    @Property({ group: "基本信息", displayName: "需要数量", tooltip: "显示这个解锁按钮组,需要多少前置解锁" })
    public needs: number = 1;

    /** 解锁价格 */
    @Property({ group: "基本信息", displayName: "解锁价格" })
    public unlockPrice: number = 10;

    @Property({ group: "基本信息", displayName: "每秒带来收益" })
    public profit: number = 1;

    @Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
    public id: number = 1;

    /** 事件监听器 需要在解锁按钮回收时注销 */
    private _listener: EventListener;

    /** 显示当前解锁按钮组进度 */
    private _curPro: number = 0;

    /** 进入触发器事件 */
    private _unlockbuildFun = null;

    /** 解锁按钮 */
    private _unlockBtn: GameObject = null;

    protected onStart(): void {

        // 客户端直接 返回
        if (SystemUtil.isClient()) {
            this.req_init();
            return;
        }

        // 开启服务端 onUpdate 方法
        this.useUpdate = true;

        // 关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        // 关闭显示
        this.gameObject.setVisibility(PropertyStatus.Off);
        
        //  监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    }

    /**
     * 服务端响应客户端初始化
     * @param curGroupId 组id
     * @param unlockedIds 当前组解锁的家具id列表
     */
    @RemoteFunction(Server)
    public res_init(curGroupId: number, unlockedIds: number[]) {
        // 如果当前要解锁的组小于已经解锁完毕的组 说明这个组已经全部解锁了 
        if (curGroupId > this.groupId) {
            this.gameObject.setVisibility(PropertyStatus.On);
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        }
        if (curGroupId === this.groupId) {
            // 如果解锁列表中包含这个家居的序号,说明已经解锁了 我们将它显示出来 并且发送事件通知解锁下一个家居的解锁按钮
            if (unlockedIds.includes(this.id)) {
                this.gameObject.setVisibility(PropertyStatus.On);
                (this.gameObject as Model).setCollision(PropertyStatus.On);
                Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
            } else {
                this.initUnlockBtn();
            }
        }
    }

    /**
     * 向服务端发送解锁请求
     */
    public async req_init() {
        // 等待客户端数据中心准备好
        await DataCenterC.ready();
        const playerData = DataCenterC.getData(PlayerData);
        this.res_init(playerData.curGroupId, playerData.unlockedIds);
    }

    protected onUpdate(dt: number): void {
        TweenUtil.TWEEN.update();
    }

    /**
     * 验证是否满足解锁条件
     */
    private ensureNeeds() {
        // 满足条件就显示解锁按钮
        if (++this._curPro >= this.needs) {
            // 先注销 listener
            this._listener.disconnect();
            // 初始化按钮
            this.initUnlockBtn();
        }
    }

    /**
     * 给指定玩家初始化 UI
     * @param player 玩家
     * @param unlockBtnGuid 按钮GUID
     */
    @RemoteFunction(Client)
    private initWorldUIOnlyOne(player: Player, unlockBtnGuid: string): void {
        this.initWorldUI(unlockBtnGuid);
    }

    /**
     * 初始化所有玩家的世界UI
     * @param unlockBtnGuid 按钮GUID
     */
    @RemoteFunction(Client, Multicast)
    private initWorldUIAllPlayer(unlockBtnGuid: string): void {
        this.initWorldUI(unlockBtnGuid);
    }

    /**
     * 客户端初始化世界UI
     * @param unlockBtnGuid 解锁按钮的GUID
     */
    private async initWorldUI(unlockBtnGuid: string): Promise<void> {
        // 异步查找按钮
        this._unlockBtn = await GameObject.asyncFindGameObjectById(unlockBtnGuid);
        const worldUI = this._unlockBtn.getChildByName("世界UI") as UIWidget;
        const targetUI = worldUI.getTargetUIWidget();
        const buildName = targetUI.findChildByPath("RootCanvas/buildNameTxt") as TextBlock;
        const buildNeeds = targetUI.findChildByPath("RootCanvas/buildNeedsTxt") as TextBlock;
        buildName.text = this.gameObject.name;
        buildNeeds.text = this.unlockPrice.toString();
    }

    /**
     * 初始化解锁建筑按钮
     */
    private async initUnlockBtn(): Promise<void> {
        // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
        this._unlockBtn = await GameObjPool.asyncSpawn("FE08DDF04F44547200C8CF9E415D3904", GameObjPoolSourceType.Prefab);
        // 初始化所有玩家的世界UI
        this.initWorldUIAllPlayer(this._unlockBtn.gameObjectId);
        // 设置父节点为当前对象
        this._unlockBtn.parent = this.gameObject;
        // 设置按钮的相对位置
        this._unlockBtn.localTransform.position = this.unlockBtnLoc;
        // 获取预制体下的触发器
        const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
        this._unlockbuildFun = (other: GameObject) => {
            // 判断进入触发器的物体是否为玩家
            if (other instanceof Character) {
                // 判断钱是否足够
                const isGoldEnough = ModuleService.getModule(PlayerModuleS).changeGold(other.player.playerId, -this.unlockPrice);
                // 如果钱够
                if (isGoldEnough) {
                    // 解绑触发器进入事件
                    trigger.onEnter.remove(this._unlockbuildFun);
                    // 回收按钮模型
                    GameObjPool.despawn(this._unlockBtn);
                    // 使用动画 显示建筑模型 & 开启碰撞 
                    this.showBuild();
                    // 通知客户端金币增加基数改变
                    Event.dispatchToClient(other.player, "GoldGrowthRate", this.profit);
                    // 更改金币增长基数
                    ModuleService.getModule(PlayerModuleS).changeGoldGrowthRate(other.player.playerId, this.profit);
                    // 更新解锁建筑的信息
                    ModuleService.getModule(PlayerModuleS).updateBuildUnlockInfo(other.player.playerId, this.groupId, this.id);
                } else {
                    console.error("钱不够!");
                }

            }
        }
        // 绑定到触发器进入事件
        trigger.onEnter.add(this._unlockbuildFun);
    }

    /**
     * 显示建筑
     */
    private showBuild(): void {
        // 定义一个 tween 要改变的数值是物体的 scale ,它的初始值是 {x:0,y:0,z:0}
        const tween = new Tween({ scale: Vector.zero });
        // 更改家居默认的缩放,在 500 毫秒内完成
        tween.to({ scale: this.gameObject.worldTransform.scale.clone() }, 500);
        // 启动tween时显示建筑
        tween.onStart(() => {
            // 显示模型
            this.gameObject.setVisibility(PropertyStatus.On);
            // 开启碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        });
        // 设置 tween 每帧更新时更新缩放
        tween.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale });
        // 动画完成时关闭 useUpdate 减少无用调用
        tween.onComplete(() => {
            this.useUpdate = false;
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
        });
        // 启动动画
        tween.start();
    }

}
当前 PlayerModuleS 脚本完整代码(点击展开)
typescript
import PlayerData from "./PlayerData";
import PlayerModuleC from "./PlayerModuleC";

export default class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerData> {

    /**
     * 改变金币方法 如果成功返回 true 不成功返回 false
     * @param pid 玩家id
     * @param deltaNum 改变的数量
     */
    public changeGold(pid: number, deltaNum: number): boolean {
        // 通过玩家id获取数据
        const data = this.getPlayerData(pid);
        // 要改变的如果是负数 就要判断钱是否够
        if (deltaNum < 0 && data.gold < Math.abs(deltaNum)) {
            return false;
        }
        data.changeGold(deltaNum);
        return true;
    }

    /**
     * 服务端改变金币 支持RPC调用
     * @param deltaNum 要改变的数量
     * @returns 是否成功
     */
    public net_changeGold(deltaNum: number): boolean {
        return this.changeGold(this.currentPlayerId, deltaNum);
    }

    /** 
     * 改变金币增长速率 
     * @param pid 要改变金币数量的玩家id 
     * @param deltaNum 改变的数量 
     */ 
    public changeGoldGrowthRate(pid: number, deltaNum: number) {
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid);
        data.changeGoldGrowthRate(deltaNum);
    }

    public net_changeGoldGrowthRate(deltaNum: number): void {
        this.changeGoldGrowthRate(this.currentPlayerId, deltaNum);
    }

    /** 
     * 服务端更新建筑解锁信息 
     * @param pid 玩家id 
     * @param groupId 组号 
     * @param sId 序号 
     */ 
    public updateBuildUnlockInfo(pid: number, groupId: number, sId: number) { 
        // 获取玩家pid的数据
        const data = this.getPlayerData(pid); 
        data.updateBuildUnlockInfo(groupId, sId); 
    } 

    public net_updateBuildUnlockInfo(groupId: number, sId: number): void {
        this.updateBuildUnlockInfo(this.currentPlayerId, groupId, sId);
    }

}
import PlayerData from "./PlayerData";
import PlayerModuleC from "./PlayerModuleC";

export default class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerData> {

    /**
     * 改变金币方法 如果成功返回 true 不成功返回 false
     * @param pid 玩家id
     * @param deltaNum 改变的数量
     */
    public changeGold(pid: number, deltaNum: number): boolean {
        // 通过玩家id获取数据
        const data = this.getPlayerData(pid);
        // 要改变的如果是负数 就要判断钱是否够
        if (deltaNum < 0 && data.gold < Math.abs(deltaNum)) {
            return false;
        }
        data.changeGold(deltaNum);
        return true;
    }

    /**
     * 服务端改变金币 支持RPC调用
     * @param deltaNum 要改变的数量
     * @returns 是否成功
     */
    public net_changeGold(deltaNum: number): boolean {
        return this.changeGold(this.currentPlayerId, deltaNum);
    }

    /** 
     * 改变金币增长速率 
     * @param pid 要改变金币数量的玩家id 
     * @param deltaNum 改变的数量 
     */ 
    public changeGoldGrowthRate(pid: number, deltaNum: number) {
        // 获取玩家pid的数据 
        const data = this.getPlayerData(pid);
        data.changeGoldGrowthRate(deltaNum);
    }

    public net_changeGoldGrowthRate(deltaNum: number): void {
        this.changeGoldGrowthRate(this.currentPlayerId, deltaNum);
    }

    /** 
     * 服务端更新建筑解锁信息 
     * @param pid 玩家id 
     * @param groupId 组号 
     * @param sId 序号 
     */ 
    public updateBuildUnlockInfo(pid: number, groupId: number, sId: number) { 
        // 获取玩家pid的数据
        const data = this.getPlayerData(pid); 
        data.updateBuildUnlockInfo(groupId, sId); 
    } 

    public net_updateBuildUnlockInfo(groupId: number, sId: number): void {
        this.updateBuildUnlockInfo(this.currentPlayerId, groupId, sId);
    }

}
当前 PlayerModuleC 脚本完整代码(点击展开)
typescript
import MainUI from "../../ui/MainUI";
import PlayerData from "./PlayerData";
import PlayerModuleS from "./PlayerModuleS";


export default class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerData> {

    protected onStart(): void {
        // 显示 MainUI 这里传递得事我们创建出得 UI 脚本的类 而不是导出的!
        UIService.show(MainUI);
    }

    /**
     * 客户端改变金币数量方法
     * @param deltaNum 要改变的数量
     */
    public async changeGold(deltaNum: number): Promise<boolean> {
        return this.server.net_changeGold(deltaNum);
    }

    /**
     * 改变金币增长速率 
     * @param deltaNum 改变值
     */
    public changeGoldGrowthRate(deltaNum: number): void { 
        this.server.net_changeGoldGrowthRate(deltaNum); 
    } 

    /**
     * 更新建筑解锁信息
     * @param groupId 当前组
     * @param sId 当前家具序号
     */
    public updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.server.net_updateBuildUnlockInfo(groupId, sId); 
    } 

}
import MainUI from "../../ui/MainUI";
import PlayerData from "./PlayerData";
import PlayerModuleS from "./PlayerModuleS";


export default class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerData> {

    protected onStart(): void {
        // 显示 MainUI 这里传递得事我们创建出得 UI 脚本的类 而不是导出的!
        UIService.show(MainUI);
    }

    /**
     * 客户端改变金币数量方法
     * @param deltaNum 要改变的数量
     */
    public async changeGold(deltaNum: number): Promise<boolean> {
        return this.server.net_changeGold(deltaNum);
    }

    /**
     * 改变金币增长速率 
     * @param deltaNum 改变值
     */
    public changeGoldGrowthRate(deltaNum: number): void { 
        this.server.net_changeGoldGrowthRate(deltaNum); 
    } 

    /**
     * 更新建筑解锁信息
     * @param groupId 当前组
     * @param sId 当前家具序号
     */
    public updateBuildUnlockInfo(groupId: number, sId: number): void { 
        this.server.net_updateBuildUnlockInfo(groupId, sId); 
    } 

}

:::

当前 PlayerData 脚本完整代码(点击展开)
typescript
export default class PlayerData extends Subdata {

    /** 金币数量 */
    @Decorator.persistence()
    public gold: number = 55;

    /** 玩家金币增长基数 */
    @Decorator.persistence()
    public goldGrowthRate: number = 1;

    /** 玩家当前解锁到的组号 */
    @Decorator.persistence()
    public curGroupId: number = 0;

    /** 这个组当前已经解锁的建筑序号列表 */
    @Decorator.persistence()
    public unlockedIds: number[] = [];

    /** 金币改变时触发事件 */
    public onGoldChange: Action = new Action();

    /**
     * 修改金币数量
     * @param deltaNum 改变值,为负数就是减少
     */
    public changeGold(deltaNum: number): void {
        this.syncToClient();
        this.gold += deltaNum;
        this.onGoldChange.call(this.gold);
        // 保存所有数据 并且同步给客户端
        this.save(true);
    }

    /**
     * 更改金币增长基数
     * @param deltaNum 改变值
     */
    public changeGoldGrowthRate(deltaNum: number): void {
        this.goldGrowthRate += deltaNum;
        // 保存所有数据 并且同步给客户端
        this.save(true);
    }

    /**
     * 更新建筑解锁信息
     * @param groupId 家具组号
     * @param sId 家具序号
     */
    public updateBuildUnlockInfo(groupId: number, sId: number): void {
        // 判断当前组号是否与要解锁的组号相同,如果不同说明当前组全部解锁完毕了
        if(this.curGroupId != groupId){
            this.unlockedIds.length = 0;
            this.curGroupId = groupId;
        }
        // 将新解锁的建筑的序号保存到已解锁数组中
        this.unlockedIds.push(sId);
        // 保存并同步给客户端
        this.save(true);
    }

}
export default class PlayerData extends Subdata {

    /** 金币数量 */
    @Decorator.persistence()
    public gold: number = 55;

    /** 玩家金币增长基数 */
    @Decorator.persistence()
    public goldGrowthRate: number = 1;

    /** 玩家当前解锁到的组号 */
    @Decorator.persistence()
    public curGroupId: number = 0;

    /** 这个组当前已经解锁的建筑序号列表 */
    @Decorator.persistence()
    public unlockedIds: number[] = [];

    /** 金币改变时触发事件 */
    public onGoldChange: Action = new Action();

    /**
     * 修改金币数量
     * @param deltaNum 改变值,为负数就是减少
     */
    public changeGold(deltaNum: number): void {
        this.syncToClient();
        this.gold += deltaNum;
        this.onGoldChange.call(this.gold);
        // 保存所有数据 并且同步给客户端
        this.save(true);
    }

    /**
     * 更改金币增长基数
     * @param deltaNum 改变值
     */
    public changeGoldGrowthRate(deltaNum: number): void {
        this.goldGrowthRate += deltaNum;
        // 保存所有数据 并且同步给客户端
        this.save(true);
    }

    /**
     * 更新建筑解锁信息
     * @param groupId 家具组号
     * @param sId 家具序号
     */
    public updateBuildUnlockInfo(groupId: number, sId: number): void {
        // 判断当前组号是否与要解锁的组号相同,如果不同说明当前组全部解锁完毕了
        if(this.curGroupId != groupId){
            this.unlockedIds.length = 0;
            this.curGroupId = groupId;
        }
        // 将新解锁的建筑的序号保存到已解锁数组中
        this.unlockedIds.push(sId);
        // 保存并同步给客户端
        this.save(true);
    }

}