Skip to content
实现家园系统(2)

实现家园系统(2)

预计阅读时间 15 分钟

本章我们会修改之前的 BuildInfo 和 MailBox 脚本的逻辑,来实现多人联机功能。本章比较复杂,最好配合视频一起学习。

修改 BuildInfo 脚本

在工程内容 --> 脚本,打开之前编写的 BuildInfo 脚本。

  • 找到之前添加的 _listener 监听器,我们在它的下面再添加一个监听器,命名为 _listener2
typescript
// ... 上文代码略 
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
/** 事件监听器,需在解锁按钮回收时注销 */ 
private _listener2: EventListener; 
/** 解锁建筑的方法 */
private _unlockBuildFun;
// ... 下文代码略 
// ... 上文代码略 
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
/** 事件监听器,需在解锁按钮回收时注销 */ 
private _listener2: EventListener; 
/** 解锁建筑的方法 */
private _unlockBuildFun;
// ... 下文代码略 
  • 创建一个初始化函数,并使用装饰器将它标记为客户端函数,将上一步删掉的 req_init 函数调用挪动到这里。
typescript
@RemoteFunction(Client)
public initClient(player: Player) {
    this.req_init();
    // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    // 客户端监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    this._listener2 = Event.addServerListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
@RemoteFunction(Client)
public initClient(player: Player) {
    this.req_init();
    // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    // 客户端监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
    this._listener2 = Event.addServerListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
  • 将 onStart 函数中的代码修改,去掉不需要的 req_init 函数调用、删除掉 _listener 的赋值、并删除多余的注释。
  • 获取家园脚本,然后给家园脚本的 bindAction 赋值。
typescript
protected onStart(): void {
    if (SystemUtil.isServer()) {
        // 开启服务端的onUpdate
        this.useUpdate = true;
        // 默认隐藏
        this.gameObject.setVisibility(PropertyStatus.Off);
        // 默认关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);

        /** 拿到自己的家园脚本 */
        const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

        homeInfo.bindAction.add((isBind: boolean) => {
            // 判断绑定还是解绑
            if (isBind) {
                this.initClient(Player.getPlayer(homeInfo.ownerId));
            } else {
                // 隐藏
                this.gameObject.setVisibility(PropertyStatus.Off);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.Off);
            }
        })
    }
}
protected onStart(): void {
    if (SystemUtil.isServer()) {
        // 开启服务端的onUpdate
        this.useUpdate = true;
        // 默认隐藏
        this.gameObject.setVisibility(PropertyStatus.Off);
        // 默认关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);

        /** 拿到自己的家园脚本 */
        const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

        homeInfo.bindAction.add((isBind: boolean) => {
            // 判断绑定还是解绑
            if (isBind) {
                this.initClient(Player.getPlayer(homeInfo.ownerId));
            } else {
                // 隐藏
                this.gameObject.setVisibility(PropertyStatus.Off);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.Off);
            }
        })
    }
}
  • 找到 ensureNeeds 函数,在这里添加注销 _listener2 逻辑。
typescript
protected ensureNeeds() {
    // 满足需求,显示解锁按钮
    if (++this._curPro >= this.needs) {
        // 初始化这个建筑的解锁按钮
        this.initUnlockBtn();
        // 注销listener
        this._listener.disconnect();
        // 注销listener2 
        this._listener2.disconnect(); 
    }
}
protected ensureNeeds() {
    // 满足需求,显示解锁按钮
    if (++this._curPro >= this.needs) {
        // 初始化这个建筑的解锁按钮
        this.initUnlockBtn();
        // 注销listener
        this._listener.disconnect();
        // 注销listener2 
        this._listener2.disconnect(); 
    }
}
  • req_int 函数中添加判断家居是否解锁的功能逻辑。
typescript
public async req_init() {
    // 等到客户端的数据中心准备好
    await DataCenterC.ready();
    const playerData = DataCenterC.getData(PlayerData);
    const curGroupId = playerData.curGroupId;
    const unlockedIds = playerData.unlockedIds;
    this.res_init(curGroupId, unlockedIds);
    if (curGroupId === this.groupId) { 
        // 是否已经解锁 
        if (unlockedIds.includes(this.id)) { 
            // 已经解锁了就显示显示下一组解锁按钮 
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1)); 
        } else { 
            // 没有解锁就初始化解锁按钮 
            this.initUnlockBtn();  
        } 
    }
}
public async req_init() {
    // 等到客户端的数据中心准备好
    await DataCenterC.ready();
    const playerData = DataCenterC.getData(PlayerData);
    const curGroupId = playerData.curGroupId;
    const unlockedIds = playerData.unlockedIds;
    this.res_init(curGroupId, unlockedIds);
    if (curGroupId === this.groupId) { 
        // 是否已经解锁 
        if (unlockedIds.includes(this.id)) { 
            // 已经解锁了就显示显示下一组解锁按钮 
            Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1)); 
        } else { 
            // 没有解锁就初始化解锁按钮 
            this.initUnlockBtn();  
        } 
    }
}
  • res_init 函数中删除掉 Event.addLocalListener 事件以及初始化解锁按钮的逻辑,因为我们已经在 initClient 中去调用了
typescript
@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(); 
        } 
    }
}
@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(); 
        } 
    }
}
  • 接下来简化一下代码,现在我们的 initWorldUiAllPlayer 函数中只调用 initWorldUI ,我们可以直接将initWorldUiAllPlayer 删除掉,在 initunlockBtn 中调用 initWorldUI
  • 现在变成多人游戏后,所有涉及到玩家的操作都需要判断一下是否是对应玩家,这样会让代码更佳保险,比如进入邮箱和解锁家居。
  • initUnlockBtn 函数现在变为了客户端调用,我们要将所有对 PlayerModuleS 的操作改成 PlayerModuleC,最终修改结果如下:
typescript
protected async initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);
    // 设置按钮的父节点为当前对象
    this._unlockBtn.parent = this.gameObject;
    // 设置按钮的相对大小
    this._unlockBtn.worldTransform.scale = Vector.one;
    // 设置按钮的相对位置
    this._unlockBtn.localTransform.position = this.unlockBtnLoc;

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

    this._unlockBuildFun = async (other: GameObject) => {
        // 判断进入的对象是一个Character实例才创建
        if (other instanceof Character && Player.localPlayer.character === other) {
            // 钱够吗
            const isGoldEnough = await ModuleService.getModule(PlayerModuleC).changeGold(-this.unlockPrice);
            // 扣钱成功才显示
            if (isGoldEnough) {
                // 用完了就先取消绑定
                trigger.onEnter.remove(this._unlockBuildFun);
                // 对象池回收解锁按钮
                GameObjPool.despawn(this._unlockBtn);
                // 显示这个模型
                this.showBuild(Player.localPlayer.playerId);
                // 金币增长基数
                Event.dispatchToLocal("GoldGrowthRate", this.profit); 
                // 持久化增长基数
                ModuleService.getModule(PlayerModuleC).changeGoldGrowthRate(this.profit); 
                // 更新解锁建筑组信息
                ModuleService.getModule(PlayerModuleC).updateBuildUnlockInfo(this.groupId, this.id); 
            } else {
                console.error("钱不够!");
            }
        }
    }

    // 拿到解锁按钮预制体下面的触发器
    const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
    // 绑定触发器的进入事件
    trigger.onEnter.add(this._unlockBuildFun);

}
protected async initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);
    // 设置按钮的父节点为当前对象
    this._unlockBtn.parent = this.gameObject;
    // 设置按钮的相对大小
    this._unlockBtn.worldTransform.scale = Vector.one;
    // 设置按钮的相对位置
    this._unlockBtn.localTransform.position = this.unlockBtnLoc;

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

    this._unlockBuildFun = async (other: GameObject) => {
        // 判断进入的对象是一个Character实例才创建
        if (other instanceof Character && Player.localPlayer.character === other) {
            // 钱够吗
            const isGoldEnough = await ModuleService.getModule(PlayerModuleC).changeGold(-this.unlockPrice);
            // 扣钱成功才显示
            if (isGoldEnough) {
                // 用完了就先取消绑定
                trigger.onEnter.remove(this._unlockBuildFun);
                // 对象池回收解锁按钮
                GameObjPool.despawn(this._unlockBtn);
                // 显示这个模型
                this.showBuild(Player.localPlayer.playerId);
                // 金币增长基数
                Event.dispatchToLocal("GoldGrowthRate", this.profit); 
                // 持久化增长基数
                ModuleService.getModule(PlayerModuleC).changeGoldGrowthRate(this.profit); 
                // 更新解锁建筑组信息
                ModuleService.getModule(PlayerModuleC).updateBuildUnlockInfo(this.groupId, this.id); 
            } else {
                console.error("钱不够!");
            }
        }
    }

    // 拿到解锁按钮预制体下面的触发器
    const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
    // 绑定触发器的进入事件
    trigger.onEnter.add(this._unlockBuildFun);

}
  • 修改 showBuild 函数为服务端函数,给它加上装饰器,让我们可以在客户端通过 RPC 方式调用,因为修改为了服务端方法,所以要把调用的玩家 ID 传入过去,并且修改 dispatchToClient 事件,加上玩家 ID。
typescript
@RemoteFunction(Server) 
protected showBuild(pid: number) { 
    // 定义一个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.onStart(() => {
        // 显示
        this.gameObject.setVisibility(PropertyStatus.On);
        // 开启碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.On);
    });
    // 设置每帧更新时,更新它的缩放
    tween.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale; });
    // 动画完成时关闭useUpdate
    tween.onComplete(() => {
        // 动画播放完成,显示下一组解锁按钮
        Event.dispatchToClient(Player.getPlayer(pid), "Show_Unlock_Button" + (this.groupId + 1)); 
        this.useUpdate = false;
    });
    // 启动这个tween动画
    tween.start();
}
@RemoteFunction(Server) 
protected showBuild(pid: number) { 
    // 定义一个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.onStart(() => {
        // 显示
        this.gameObject.setVisibility(PropertyStatus.On);
        // 开启碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.On);
    });
    // 设置每帧更新时,更新它的缩放
    tween.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale; });
    // 动画完成时关闭useUpdate
    tween.onComplete(() => {
        // 动画播放完成,显示下一组解锁按钮
        Event.dispatchToClient(Player.getPlayer(pid), "Show_Unlock_Button" + (this.groupId + 1)); 
        this.useUpdate = false;
    });
    // 启动这个tween动画
    tween.start();
}

脚本完整代码

修改完毕最终 BuildInfo 脚本中的代码(点击展开)。
TypeScript
import { PlayerModuleC } from "../modules/player/PlayerModuleC";
import { PlayerData } from "../modules/player/PlayerData";
import HomeInfo from "./HomeInfo";

@Component
export default class BuildInfo extends Script {

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

    /** 显示创建按钮的组别 */
    @Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
    public id: number = 1;

    /** 这个建筑解锁按钮的相对位置 */
    @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;

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

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

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

    /** 解锁建筑的方法 */
    private _unlockBuildFun;

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {
        // --------------------------------------服务端操作--------------------------------------
        if (SystemUtil.isServer()) {
            // 开启服务端的onUpdate
            this.useUpdate = true;

            // 默认隐藏
            this.gameObject.setVisibility(PropertyStatus.Off);
            // 关闭碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.Off);

            /** 拿到自己的家园脚本 */
            const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

            homeInfo.bindAction.add((isBind: boolean) => {
                // 绑定还是解绑
                if (isBind) {
                    this.initClient(Player.getPlayer(homeInfo.ownerId));
                } else {
                    // 隐藏
                    this.gameObject.setVisibility(PropertyStatus.Off);
                    // 关闭碰撞
                    this.gameObject.setCollision(PropertyStatus.Off);
                }
            })
        }
    }

    @RemoteFunction(Client)
    public initClient(player: Player) {
        this.req_init();
        // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
        // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener2 = Event.addServerListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    }

    /** 服务端响应客户端初始化 */
    @RemoteFunction(Server)
    public async res_init(curGroupId: number, unlockedIds: number[]) {
        if (curGroupId > this.groupId) {
            // 默认隐藏
            this.gameObject.setVisibility(PropertyStatus.On);
            // 关闭碰撞
            this.gameObject.setCollision(PropertyStatus.On);
        }
        if (curGroupId === this.groupId) {
            if (unlockedIds.includes(this.id)) {
                // 显示模型
                this.gameObject.setVisibility(PropertyStatus.On);
                // 打开碰撞
                this.gameObject.setCollision(PropertyStatus.On);
            }
        }
    }

    /** 请求客户端的解锁信息来完成初始化 */
    public async req_init() {
        // 等到客户端的数据中心准备好
        await DataCenterC.ready();
        const playerData = DataCenterC.getData(PlayerData);
        const curGroupId = playerData.curGroupId;
        const unlockedIds = playerData.unlockedIds;
        this.res_init(curGroupId, unlockedIds);
        if (curGroupId === this.groupId) {
            if (unlockedIds.includes(this.id)) {
                // 显示下一组解锁按钮
                Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
            } else {
                this.initUnlockBtn();
            }
        }
    }

    /**
     * 验证是否满足显示解锁按钮的需求
     */
    protected ensureNeeds() {
        // 满足需求,显示解锁按钮
        if (++this._curPro >= this.needs) {
            // 初始化这个建筑的解锁按钮
            this.initUnlockBtn();
            // 注销listener
            this._listener.disconnect();
            // 注销listener
            this._listener2.disconnect();
        }
    }

    /**
     * 客户端初始化世界UI
     * @param unlockBtnGuid 解锁按钮的guid
     */
    protected initWorldUI() {
        const worldUI = this._unlockBtn.getChildren()[0].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 + "";
    }

    /**
     * 初始化解锁建筑按钮
     */
    protected async initUnlockBtn() {
        // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
        this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);

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

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

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

            this._unlockBuildFun = async (other: GameObject) => {
                // 判断进入的对象是一个Character实例才创建,并且是自己踩上去才会触发
                if (other instanceof Character && Player.localPlayer.character === other) {
                    // 钱购吗
                    const isGoldEnough = await ModuleService.getModule(PlayerModuleC).changeGold(-this.unlockPrice);

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

                        // 显示这个模型
                        this.showBuild(Player.localPlayer.playerId);

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

            // 拿到解锁按钮预制体下面的解锁按钮模型,最后拿到触发器
            const trigger = this._unlockBt.getChildByName("触发器") as Trigger;
            // 绑定触发器的进入事件
            trigger.onEnter.add(this._unlockBuildFun);
        } else {
            console.error("初始化解锁按钮失败,请检查是不是spawn的guid");
        }
    }

    /**
     * 显示建筑(服务端)
     */
    @RemoteFunction(Server)
    protected showBuild(pid: number) {
        // 定义一个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.onStart(() => {
            // 显示
            this.gameObject.setVisibility(PropertyStatus.On);
            // 开启碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        });
        // 设置每帧更新时,更新它的缩放
        tween.onUpdate(t => { this.gameObject.worldTransform.scale.set(t.scale); });
        // 动画完成时关闭useUpdate
        tween.onComplete(() => {
            // 动画播放完成,显示下一组解锁按钮
            Event.dispatchToClient(Player.getPlayer(pid), "Show_Unlock_Button" + (this.groupId + 1));
            this.useUpdate = false;
        });
        // 启动这个tween动画
        tween.start();
    }

    protected onUpdate(DeltaTime: number): void {
        TweenUtil.TWEEN.update();
    }
}
import { PlayerModuleC } from "../modules/player/PlayerModuleC";
import { PlayerData } from "../modules/player/PlayerData";
import HomeInfo from "./HomeInfo";

@Component
export default class BuildInfo extends Script {

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

    /** 显示创建按钮的组别 */
    @Property({ group: "基本信息", displayName: "序号", tooltip: "这个组的第几个,默认从1开始" })
    public id: number = 1;

    /** 这个建筑解锁按钮的相对位置 */
    @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;

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

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

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

    /** 解锁建筑的方法 */
    private _unlockBuildFun;

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {
        // --------------------------------------服务端操作--------------------------------------
        if (SystemUtil.isServer()) {
            // 开启服务端的onUpdate
            this.useUpdate = true;

            // 默认隐藏
            this.gameObject.setVisibility(PropertyStatus.Off);
            // 关闭碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.Off);

            /** 拿到自己的家园脚本 */
            const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

            homeInfo.bindAction.add((isBind: boolean) => {
                // 绑定还是解绑
                if (isBind) {
                    this.initClient(Player.getPlayer(homeInfo.ownerId));
                } else {
                    // 隐藏
                    this.gameObject.setVisibility(PropertyStatus.Off);
                    // 关闭碰撞
                    this.gameObject.setCollision(PropertyStatus.Off);
                }
            })
        }
    }

    @RemoteFunction(Client)
    public initClient(player: Player) {
        this.req_init();
        // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
        // 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
        this._listener2 = Event.addServerListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
    }

    /** 服务端响应客户端初始化 */
    @RemoteFunction(Server)
    public async res_init(curGroupId: number, unlockedIds: number[]) {
        if (curGroupId > this.groupId) {
            // 默认隐藏
            this.gameObject.setVisibility(PropertyStatus.On);
            // 关闭碰撞
            this.gameObject.setCollision(PropertyStatus.On);
        }
        if (curGroupId === this.groupId) {
            if (unlockedIds.includes(this.id)) {
                // 显示模型
                this.gameObject.setVisibility(PropertyStatus.On);
                // 打开碰撞
                this.gameObject.setCollision(PropertyStatus.On);
            }
        }
    }

    /** 请求客户端的解锁信息来完成初始化 */
    public async req_init() {
        // 等到客户端的数据中心准备好
        await DataCenterC.ready();
        const playerData = DataCenterC.getData(PlayerData);
        const curGroupId = playerData.curGroupId;
        const unlockedIds = playerData.unlockedIds;
        this.res_init(curGroupId, unlockedIds);
        if (curGroupId === this.groupId) {
            if (unlockedIds.includes(this.id)) {
                // 显示下一组解锁按钮
                Event.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
            } else {
                this.initUnlockBtn();
            }
        }
    }

    /**
     * 验证是否满足显示解锁按钮的需求
     */
    protected ensureNeeds() {
        // 满足需求,显示解锁按钮
        if (++this._curPro >= this.needs) {
            // 初始化这个建筑的解锁按钮
            this.initUnlockBtn();
            // 注销listener
            this._listener.disconnect();
            // 注销listener
            this._listener2.disconnect();
        }
    }

    /**
     * 客户端初始化世界UI
     * @param unlockBtnGuid 解锁按钮的guid
     */
    protected initWorldUI() {
        const worldUI = this._unlockBtn.getChildren()[0].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 + "";
    }

    /**
     * 初始化解锁建筑按钮
     */
    protected async initUnlockBtn() {
        // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
        this._unlockBtn = await GameObjPool.asyncSpawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);

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

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

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

            this._unlockBuildFun = async (other: GameObject) => {
                // 判断进入的对象是一个Character实例才创建,并且是自己踩上去才会触发
                if (other instanceof Character && Player.localPlayer.character === other) {
                    // 钱购吗
                    const isGoldEnough = await ModuleService.getModule(PlayerModuleC).changeGold(-this.unlockPrice);

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

                        // 显示这个模型
                        this.showBuild(Player.localPlayer.playerId);

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

            // 拿到解锁按钮预制体下面的解锁按钮模型,最后拿到触发器
            const trigger = this._unlockBt.getChildByName("触发器") as Trigger;
            // 绑定触发器的进入事件
            trigger.onEnter.add(this._unlockBuildFun);
        } else {
            console.error("初始化解锁按钮失败,请检查是不是spawn的guid");
        }
    }

    /**
     * 显示建筑(服务端)
     */
    @RemoteFunction(Server)
    protected showBuild(pid: number) {
        // 定义一个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.onStart(() => {
            // 显示
            this.gameObject.setVisibility(PropertyStatus.On);
            // 开启碰撞
            (this.gameObject as Model).setCollision(PropertyStatus.On);
        });
        // 设置每帧更新时,更新它的缩放
        tween.onUpdate(t => { this.gameObject.worldTransform.scale.set(t.scale); });
        // 动画完成时关闭useUpdate
        tween.onComplete(() => {
            // 动画播放完成,显示下一组解锁按钮
            Event.dispatchToClient(Player.getPlayer(pid), "Show_Unlock_Button" + (this.groupId + 1));
            this.useUpdate = false;
        });
        // 启动这个tween动画
        tween.start();
    }

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

修改 MailBox 脚本

在工程内容 --> 脚本,打开之前编写的 MailBox 脚本。

  • 添加隐藏客户端世界 UI 的方法
typescript
/** 隐藏客户端的UI */
public async hideWorldUI() {
    //  等待这个模型在客户端加载好
    await this.gameObject.asyncReady();
    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
    // 隐藏
    worldUI.setVisibility(PropertyStatus.Off);
}
/** 隐藏客户端的UI */
public async hideWorldUI() {
    //  等待这个模型在客户端加载好
    await this.gameObject.asyncReady();
    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
    // 隐藏
    worldUI.setVisibility(PropertyStatus.Off);
}
  • 修改 onStart 函数,在客户端默认先将邮箱 UI 隐藏起来,等待服务端消息再做处理。
typescript
protected onStart(): void {
    if (SystemUtil.isClient()) {
        // 默认隐藏 UI
        this.hideWorldUI();
    }

    if (SystemUtil.isServer()) {
        // 关闭碰撞先
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        /** 拿到自己的家园脚本 */
        const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

        homeInfo.bindAction.add((isBind: boolean) => {
            // 绑定还是解绑
            if (isBind) {
                this.gameObject.setVisibility(PropertyStatus.On);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.On);
                this.initClient(Player.getPlayer(homeInfo.ownerId));
            } else {
                this.gameObject.setVisibility(PropertyStatus.Off);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.Off);
            }
        })
    }
}
protected onStart(): void {
    if (SystemUtil.isClient()) {
        // 默认隐藏 UI
        this.hideWorldUI();
    }

    if (SystemUtil.isServer()) {
        // 关闭碰撞先
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        /** 拿到自己的家园脚本 */
        const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;

        homeInfo.bindAction.add((isBind: boolean) => {
            // 绑定还是解绑
            if (isBind) {
                this.gameObject.setVisibility(PropertyStatus.On);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.On);
                this.initClient(Player.getPlayer(homeInfo.ownerId));
            } else {
                this.gameObject.setVisibility(PropertyStatus.Off);
                // 关闭碰撞
                (this.gameObject as Model).setCollision(PropertyStatus.Off);
            }
        })
    }
}
  • 修改 initClient 方法,加上装饰器表示它是客户端方法,服务端可以通过 RPC 方式调用它。
  • 因为现在变为了多人游戏,需要在邮箱触发器上判断一下是否进入的是本地用户,这样代码更佳健壮。
  • 获取到世界 UI 后还需要添加一行代码,让它显示,因为默认状态是隐藏得。
typescript
@RemoteFunction(Client) 
public async initClient(player: Player) { 
    //  等待这个模型在客户端加载好
    await this.gameObject.asyncReady();

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

    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget; 
    // 显示 
    worldUI.setVisibility(PropertyStatus.On); 
    // 拿到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._addGoldIter = setInterval(() => {
        this._goldNum += this._alterNum;
        this._goldNumTxt.text = this._goldNum.toString();
    }, 1000);

    const trigger = this.gameObject.getChildByName("触发器") as Trigger; 
    trigger.onEnter.add((other: GameObject) => { 
        if (other instanceof Character && player.character === other) { 
            ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum); 
            this._goldNum = 0;
            this._goldNumTxt.text = this._goldNum.toString();
        } 
    }); 

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

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

    // 拿到世界UI
    const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget; 
    // 显示 
    worldUI.setVisibility(PropertyStatus.On); 
    // 拿到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._addGoldIter = setInterval(() => {
        this._goldNum += this._alterNum;
        this._goldNumTxt.text = this._goldNum.toString();
    }, 1000);

    const trigger = this.gameObject.getChildByName("触发器") as Trigger; 
    trigger.onEnter.add((other: GameObject) => { 
        if (other instanceof Character && player.character === other) { 
            ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum); 
            this._goldNum = 0;
            this._goldNumTxt.text = this._goldNum.toString();
        } 
    }); 

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

脚本完整代码

修改完毕最终 MailBox 脚本代码是这样的(点击展开)。
TypeScript
import { PlayerModuleC } from "../modules/player/PlayerModuleC";
import { PlayerData } from "../modules/player/PlayerData";
import HomeInfo from "./HomeInfo";

@Component
export default class MailBox extends Script {

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

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

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

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

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

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {
        // --------------------------------------客户端操作--------------------------------------
        if (SystemUtil.isClient()) {
            // 默认隐藏客户端的UI
            this.hideWorldUI();
        }

        // --------------------------------------服务端操作--------------------------------------
        if (SystemUtil.isServer()) {
            // 关闭碰撞先
            (this.gameObject as Model).setCollision(PropertyStatus.Off);

            /** 拿到自己的家园脚本 */
            const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;
            
            homeInfo.bindAction.add((isBind: boolean) => {
                // 绑定还是解绑
                if (isBind) {
                    this.gameObject.setVisibility(PropertyStatus.On);
                    // 关闭碰撞
                    (this.gameObject as Model).setCollision(PropertyStatus.On);
                    this.initClient(Player.getPlayer(homeInfo.ownerId));
                }else {
                    this.gameObject.setVisibility(PropertyStatus.Off);
                    // 关闭碰撞
                    (this.gameObject as Model).setCollision(PropertyStatus.Off);
                }
            })
        }
    }

    /** 隐藏客户端的UI */
    public async hideWorldUI() {
        //  等待这个模型在客户端加载好
        await this.gameObject.asyncReady();
        // 拿到世界UI
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 隐藏
        worldUI.setVisibility(PropertyStatus.Off);
    }

    @RemoteFunction(Client)
    public async initClient(player: Player) {
        //  等待这个模型在客户端加载好
        await this.gameObject.asyncReady();

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

        // 拿到世界UI
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 显示
        worldUI.setVisibility(PropertyStatus.On);
        // 拿到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._addGoldIter = setInterval(() => {
            this._goldNum += this._alterNum;
            this._goldNumTxt.text = this._goldNum.toString();
        }, 1000);

        const trigger = this.gameObject.getChildByName("触发器") as Trigger;
        trigger.onEnter.add((other: GameObject) => {
            if (other instanceof Character && player.character === other) {
                ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
                this._goldNum = 0;
                this._goldNumTxt.text = this._goldNum.toString();
            }
        });

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

    protected onDestroy(): void {
        // 销毁时,清除时器
        if (this._addGoldIter) {
            clearInterval(this._addGoldIter);
        }
    }
}
import { PlayerModuleC } from "../modules/player/PlayerModuleC";
import { PlayerData } from "../modules/player/PlayerData";
import HomeInfo from "./HomeInfo";

@Component
export default class MailBox extends Script {

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

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

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

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

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

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {
        // --------------------------------------客户端操作--------------------------------------
        if (SystemUtil.isClient()) {
            // 默认隐藏客户端的UI
            this.hideWorldUI();
        }

        // --------------------------------------服务端操作--------------------------------------
        if (SystemUtil.isServer()) {
            // 关闭碰撞先
            (this.gameObject as Model).setCollision(PropertyStatus.Off);

            /** 拿到自己的家园脚本 */
            const homeInfo = this.gameObject.parent.getScriptByName("HomeInfo") as HomeInfo;
            
            homeInfo.bindAction.add((isBind: boolean) => {
                // 绑定还是解绑
                if (isBind) {
                    this.gameObject.setVisibility(PropertyStatus.On);
                    // 关闭碰撞
                    (this.gameObject as Model).setCollision(PropertyStatus.On);
                    this.initClient(Player.getPlayer(homeInfo.ownerId));
                }else {
                    this.gameObject.setVisibility(PropertyStatus.Off);
                    // 关闭碰撞
                    (this.gameObject as Model).setCollision(PropertyStatus.Off);
                }
            })
        }
    }

    /** 隐藏客户端的UI */
    public async hideWorldUI() {
        //  等待这个模型在客户端加载好
        await this.gameObject.asyncReady();
        // 拿到世界UI
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 隐藏
        worldUI.setVisibility(PropertyStatus.Off);
    }

    @RemoteFunction(Client)
    public async initClient(player: Player) {
        //  等待这个模型在客户端加载好
        await this.gameObject.asyncReady();

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

        // 拿到世界UI
        const worldUI = this.gameObject.getChildByName("世界UI") as UIWidget;
        // 显示
        worldUI.setVisibility(PropertyStatus.On);
        // 拿到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._addGoldIter = setInterval(() => {
            this._goldNum += this._alterNum;
            this._goldNumTxt.text = this._goldNum.toString();
        }, 1000);

        const trigger = this.gameObject.getChildByName("触发器") as Trigger;
        trigger.onEnter.add((other: GameObject) => {
            if (other instanceof Character && player.character === other) {
                ModuleService.getModule(PlayerModuleC).changeGold(this._goldNum);
                this._goldNum = 0;
                this._goldNumTxt.text = this._goldNum.toString();
            }
        });

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

    protected onDestroy(): void {
        // 销毁时,清除时器
        if (this._addGoldIter) {
            clearInterval(this._addGoldIter);
        }
    }
}