实现家园系统(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的 GameObjectID 是解锁按钮预制体的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的 GameObjectID 是解锁按钮预制体的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 解锁按钮的 GameObjectID
*/
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的GameObjectID是解锁按钮预制体的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的id");
}
}
/**
* 显示建筑(服务端)
*/
@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 解锁按钮的 GameObjectID
*/
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的GameObjectID是解锁按钮预制体的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的id");
}
}
/**
* 显示建筑(服务端)
*/
@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);
}
}
}