Skip to content
实现购买按钮上金币显示

实现购买按钮上金币显示

预计阅读时间 15 分钟

本章我们会在购买按钮上方创建一个用于显示价格的世界UI,并使用代码来动态控制它的内容。

制作一个解锁建筑信息的 UI

  • 右键工程内容中的 UI,创建一个名为 prefab 的文件夹

img

  • 选中 UI 下新建的这个 prefab 文件夹,新建一个名为 UnlockInfo 的 UI

img

  • 双击新建的这个 UI,进入 UI 设计器界面

img

  • 制作一个解锁这个建筑所需信息的 UI,建筑名字、金币图标、建筑价格
    • 鼠标放在设计器上时的几个控制操作
      • 滚动鼠标滚轮 -> 放大或缩小画布
      • 鼠标右键按住画布拖动 -> 移动画布
    • 拖入一个文本控件,设置其 X 方向的大小为 350,然后双击控件,改名为“建筑名”

  • 将图片控件拖入到设计器中合适位置,在本地资源库中搜索金币,寻找一个合适金币图标(162901)下载,并将其拖入到图片控件中,设置图片控件 X 方向和 Y 方向的大小为 100

  • 同理,拖入一个文本控件,设置其 X 方向的大小设置为 250,Y 方向的大小设置为 100,双击将其改名为 100

img

  • 在对象管理器中,选中 Root 目录,设置其 X 方向大小为 350,Y 方向的大小为 200

img

  • 在对象管理器中,选中 RootCanvas 画布,在对象属性中找到布局,开启自动布局,在新出现的属性选项中再勾选上网格布局

  • 最后,将需要在代码中动态改变的控件对象名改变(小写字母开头),这里将建筑名文本控件对象名改为”buildNameTxt“,解锁建筑所需金币数量文本控件对象名改为”buildNeedsTxt“,最后保存

img

  • 制作解锁按钮的世界 UI
    • 进入到解锁按钮预制体中,右键解锁按钮模型,创建一个世界 UI(一定是 C 端的)的游戏功能对象

  • 在世界 UI 的属性面板中设置其相对位置的 Z 轴大小为 2000,再找到 UI 类型,将其设置为头顶,再将刚才制作好的建筑信息 UI 与世界 UI 绑定

  • 运行游戏,查看效果

img

编写世界 UI 的代码

打开 BuildInfo 脚本,我们继续补充代码,用代码来控制世界 UI 的显示。

  • 首先添加一个新的属性unlockPrice 表示解锁这个建筑需要的金额。参照之前的属性给它加上装饰器,方便我们在编辑器中修改。
typescript
@Property({ group: "基本信息", displayName: "解锁价格" })
public unlockPrice: number = 10;
@Property({ group: "基本信息", displayName: "解锁价格" })
public unlockPrice: number = 10;
  • 接下来写一个客户端初始化世界 UI 信息的方法 initWorldUI

    1. 根据服务端创建出来的解锁按钮的 GUID,在客户端异步查找这个 GUID 对应的物体。

    2. 客户端找到这个解锁按钮对象之后,再找到它的子物体:世界 UI 对象,将类型转换为 UIWidget。

    3. 获取世界 UI 的 targetUI,也就是我们之前拖到世界UI上的 UI 文件。

    4. 根据 targetUI 获取对应的 UI 控件,buildNameTxt 和 buildNeedsTxt。

    5. 按需赋值,buildNameTxt 控件的文本为这个建筑的名字,buildNeedsTxt 为解锁这个建筑所需的金币。

TypeScript
/**
 * 客户端初始化世界UI
 * @param unlockBtnGuid 解锁按钮的guid
 */
private async initWorldUI(unlockBtnGuid: string) {
    // 异步的去查找这个解锁按钮,直接查找的时候有可能解锁按钮在客户端还没有创建成功
    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 + "";
}
/**
 * 客户端初始化世界UI
 * @param unlockBtnGuid 解锁按钮的guid
 */
private async initWorldUI(unlockBtnGuid: string) {
    // 异步的去查找这个解锁按钮,直接查找的时候有可能解锁按钮在客户端还没有创建成功
    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 + "";
}
  • 初始化函数编写好后,我们需要在服务端初始化解锁按钮时,让所有客户端同步世界 UI 信息。因为 UI 都是只存在于客户端的,所以我们要编写一个客户端函数,但是要在服务端调用。这时候就可以使用装饰器让函数支持 RPC 。
    • 装饰器 @RemoteFunction(Client, Multicast),被其修饰的方法可被服务端调用,并且广播给所有客户端。
    • 定义一个客户端方法 initWorldUIAllPlayer,接收一个解锁按钮 GUID 的参数 unlockBtnGuid
TypeScript
/** 
 * 初始化所有玩家的世界UI
 */
@RemoteFunction(Client, Multicast)
protected initWorldUIAllPlayer(unlockBtnGuid: string) {
    this.initWorldUI(unlockBtnGuid);
}
/** 
 * 初始化所有玩家的世界UI
 */
@RemoteFunction(Client, Multicast)
protected initWorldUIAllPlayer(unlockBtnGuid: string) {
    this.initWorldUI(unlockBtnGuid);
}
  • 在服务端初始化解锁按钮时调用这个方法。
TypeScript
/**
 * 初始化解锁建筑按钮
 */
protected initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = GameObjPool.spawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);
    // 初始化所有玩家的世界UI
    this.initWorldUIAllPlayer(this._unlockBtn.gameObjectId);
    下面代码略...
}
/**
 * 初始化解锁建筑按钮
 */
protected initUnlockBtn() {
    // 注意这儿spawn的guid是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
    this._unlockBtn = GameObjPool.spawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);
    // 初始化所有玩家的世界UI
    this.initWorldUIAllPlayer(this._unlockBtn.gameObjectId);
    下面代码略...
}
  • 在玩家刚进入房间时,只给当前该玩家同步解锁按钮的世界 UI 信息
    • 装饰器 @RemoteFunction(Client),被其修饰的方法可被服务端调用,这个方法的第一个数必须是一个玩家 Player,服务端才能准确找到调用这个玩家的这个方法
    • 定义一个客户端方法 initWorldUIOnlyOne,参数是 player: Player, unlockBtnGuid: string
TypeScript
/** 初始化单个玩家的世界UI */
@RemoteFunction(Client)
protected initWorldUIOnlyOne(player: Player, unlockBtnGuid: string) {
    this.initWorldUI(unlockBtnGuid);
}
/** 初始化单个玩家的世界UI */
@RemoteFunction(Client)
protected initWorldUIOnlyOne(player: Player, unlockBtnGuid: string) {
    this.initWorldUI(unlockBtnGuid);
}
  • 定义一个方法 onPlayerEnter,接收参数 player: Player,并判断当前如果有解锁按钮,且这个建筑未解锁。这时就初始化这个玩家的世界 UI
TypeScript
/**
 * 玩家进入房间,初始化已经显示出来的世界UI
 * @param player 上线的玩家
 */
protected onPlayerEnter(player: Player) {
    // 当前建筑按钮显示且当前建筑隐藏
    if (this._unlockBtn && !this.gameObject.getVisibility()) {
        this.initWorldUIOnlyOne(player, this._unlockBtn.gameObjectId);
    }
}
/**
 * 玩家进入房间,初始化已经显示出来的世界UI
 * @param player 上线的玩家
 */
protected onPlayerEnter(player: Player) {
    // 当前建筑按钮显示且当前建筑隐藏
    if (this._unlockBtn && !this.gameObject.getVisibility()) {
        this.initWorldUIOnlyOne(player, this._unlockBtn.gameObjectId);
    }
}
  • Player.onPlayerJoin 事件,在服务端监听玩家上线,每当有玩家进入房间就会收到回调,在 onStart 中调用该方法,并与 onPlayerEnter 方法绑定
TypeScript
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
    // 监听玩家进入房间
    Player.onPlayerJoin.add(this.onPlayerEnter.bind(this));
}
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
    // 监听玩家进入房间
    Player.onPlayerJoin.add(this.onPlayerEnter.bind(this));
}
  • 运行游戏查看效果

完整代码

当前脚本完整代码(点击展开)
typescript
@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; 

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

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

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

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

    protected onStart(): void {

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

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

        // 关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        // 关闭显示
        this.gameObject.setVisibility(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));
        }

        // 监听玩家进入房间事件 
        Player.onPlayerJoin.add(this.onPlayerEnter.bind(this)); 
    }

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

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

    /** 
     * 玩家上线时处理 
     * @param player 当前上线的玩家 
     */ 
    private onPlayerEnter(player: Player): void { 
        // 判断当前有解锁按钮 且 建筑没有解锁 
        if (this._unlockBtn && !this.gameObject.getVisibility()) { 
            this.initWorldUIOnlyOne(player, this._unlockBtn.gameObjectId); 
        } 
    } 

    /** 
     * 给指定玩家初始化 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) {
                // 解绑触发器进入事件
                trigger.onEnter.remove(this._unlockbuildFun);
                // 回收按钮模型
                GameObjPool.despawn(this._unlockBtn);
                // 使用动画 显示建筑模型 & 开启碰撞 
                this.showBuild();
            }
        }
        // 绑定到触发器进入事件
        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();
    }

}
@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; 

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

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

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

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

    protected onStart(): void {

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

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

        // 关闭碰撞
        (this.gameObject as Model).setCollision(PropertyStatus.Off);
        // 关闭显示
        this.gameObject.setVisibility(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));
        }

        // 监听玩家进入房间事件 
        Player.onPlayerJoin.add(this.onPlayerEnter.bind(this)); 
    }

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

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

    /** 
     * 玩家上线时处理 
     * @param player 当前上线的玩家 
     */ 
    private onPlayerEnter(player: Player): void { 
        // 判断当前有解锁按钮 且 建筑没有解锁 
        if (this._unlockBtn && !this.gameObject.getVisibility()) { 
            this.initWorldUIOnlyOne(player, this._unlockBtn.gameObjectId); 
        } 
    } 

    /** 
     * 给指定玩家初始化 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) {
                // 解绑触发器进入事件
                trigger.onEnter.remove(this._unlockbuildFun);
                // 回收按钮模型
                GameObjPool.despawn(this._unlockBtn);
                // 使用动画 显示建筑模型 & 开启碰撞 
                this.showBuild();
            }
        }
        // 绑定到触发器进入事件
        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();
    }

}