实现购买按钮上金币显示
预计阅读时间 15 分钟
本章我们会在购买按钮上方创建一个用于显示价格的世界UI,并使用代码来动态控制它的内容。
制作一个解锁建筑信息的 UI
- 右键工程内容中的 UI,创建一个名为 prefab 的文件夹
- 选中 UI 下新建的这个 prefab 文件夹,新建一个名为 UnlockInfo 的 UI
- 双击新建的这个 UI,进入 UI 设计器界面
- 制作一个解锁这个建筑所需信息的 UI,建筑名字、金币图标、建筑价格
- 鼠标放在设计器上时的几个控制操作
- 滚动鼠标滚轮 -> 放大或缩小画布
- 鼠标右键按住画布拖动 -> 移动画布
- 拖入一个文本控件,设置其 X 方向的大小为 350,然后双击控件,改名为“建筑名”
- 鼠标放在设计器上时的几个控制操作
- 将图片控件拖入到设计器中合适位置,在本地资源库中搜索金币,寻找一个合适金币图标(162901)下载,并将其拖入到图片控件中,设置图片控件 X 方向和 Y 方向的大小为 100
- 同理,拖入一个文本控件,设置其 X 方向的大小设置为 250,Y 方向的大小设置为 100,双击将其改名为 100
- 在对象管理器中,选中 Root 目录,设置其 X 方向大小为 350,Y 方向的大小为 200
- 在对象管理器中,选中 RootCanvas 画布,在对象属性中找到布局,开启自动布局,在新出现的属性选项中再勾选上网格布局
- 最后,将需要在代码中动态改变的控件对象名改变(小写字母开头),这里将建筑名文本控件对象名改为”buildNameTxt“,解锁建筑所需金币数量文本控件对象名改为”buildNeedsTxt“,最后保存
- 制作解锁按钮的世界 UI
- 进入到解锁按钮预制体中,右键解锁按钮模型,创建一个世界 UI(一定是 C 端的)的游戏功能对象
- 在世界 UI 的属性面板中设置其相对位置的 Z 轴大小为 2000,再找到 UI 类型,将其设置为头顶,再将刚才制作好的建筑信息 UI 与世界 UI 绑定
- 运行游戏,查看效果
编写世界 UI 的代码
打开 BuildInfo 脚本,我们继续补充代码,用代码来控制世界 UI 的显示。
- 首先添加一个新的属性
unlockPrice
表示解锁这个建筑需要的金额。参照之前的属性给它加上装饰器,方便我们在编辑器中修改。
typescript
@Property({ group: "基本信息", displayName: "解锁价格" })
public unlockPrice: number = 10;
@Property({ group: "基本信息", displayName: "解锁价格" })
public unlockPrice: number = 10;
接下来写一个客户端初始化世界 UI 信息的方法
initWorldUI
。根据服务端创建出来的解锁按钮的 AssetID,在客户端异步查找这个 AssetID 对应的物体。
客户端找到这个解锁按钮对象之后,再找到它的子物体:世界 UI 对象,将类型转换为 UIWidget。
获取世界 UI 的 targetUI,也就是我们之前拖到
世界UI
上的 UI 文件。根据 targetUI 获取对应的 UI 控件,buildNameTxt 和 buildNeedsTxt。
按需赋值,buildNameTxt 控件的文本为这个建筑的名字,buildNeedsTxt 为解锁这个建筑所需的金币。
TypeScript
/**
* 客户端初始化世界UI
* @param unlockBtnGuid 解锁按钮的 AssetID
*/
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 解锁按钮的 AssetID
*/
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
,接收一个解锁按钮 GameObjectID 的参数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的 GameObjectID 是解锁按钮预制体的id,第二个参数指资源类型,这儿因为是预制体的资源所以传递GameObjPoolSourceType.Prefab
this._unlockBtn = GameObjPool.spawn("D442F26A43DED08F57F592B57CC2B56E", GameObjPoolSourceType.Prefab);
// 初始化所有玩家的世界UI
this.initWorldUIAllPlayer(this._unlockBtn.gameObjectId);
下面代码略...
}
/**
* 初始化解锁建筑按钮
*/
protected initUnlockBtn() {
// 注意这儿spawn的 GameObjectID 是解锁按钮预制体的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 按钮GameObjectID
*/
@RemoteFunction(Client)
private initWorldUIOnlyOne(player: Player, unlockBtnGuid: string): void {
this.initWorldUI(unlockBtnGuid);
}
/**
* 初始化所有玩家的世界UI
* @param unlockBtnGuid 按钮GameObjectID
*/
@RemoteFunction(Client, Multicast)
private initWorldUIAllPlayer(unlockBtnGuid: string): void {
this.initWorldUI(unlockBtnGuid);
}
/**
* 客户端初始化世界UI
* @param unlockBtnGuid 解锁按钮的GameObjectID
*/
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的GameObjectID是解锁按钮预制体的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 按钮GameObjectID
*/
@RemoteFunction(Client)
private initWorldUIOnlyOne(player: Player, unlockBtnGuid: string): void {
this.initWorldUI(unlockBtnGuid);
}
/**
* 初始化所有玩家的世界UI
* @param unlockBtnGuid 按钮GameObjectID
*/
@RemoteFunction(Client, Multicast)
private initWorldUIAllPlayer(unlockBtnGuid: string): void {
this.initWorldUI(unlockBtnGuid);
}
/**
* 客户端初始化世界UI
* @param unlockBtnGuid 解锁按钮的GameObjectID
*/
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的GameObjectID是解锁按钮预制体的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();
}
}