优化显示效果
预计阅读时间 25 分钟
本章我们要使用 Tween 来优化家具显示效果,让游戏更加生动。并将建筑物分组,实现解锁完前置家具才能接受后续家具的逻辑。
使用 Tween 优化动画显示效果
打开上一章编写的 BuildInfo
脚本:
- onUpdate 方法默认是关闭的,我们可以在 onStart 方法中将 useUpdate 设置位 true 来开启,开启之后编辑器就会自动每帧调用 onUpdate 函数了。
TypeScript
// 开启服务端的 onUpdate 函数开关
this.useUpdate = true;
// 开启服务端的 onUpdate 函数开关
this.useUpdate = true;
- 接着在 onUpdate 方法中开启 Tween 更新
TypeScript
protected onUpdate(DeltaTime: number): void {
TweenUtil.TWEEN.update();
}
protected onUpdate(DeltaTime: number): void {
TweenUtil.TWEEN.update();
}
- 新增 showBuild 方法,用来显示建筑和实现 tween 动画。我们想让地板由小变大,所以要改变的是
scale
属性。
TypeScript
/**
* 显示建筑
*/
protected showBuild() {
// 定义一个tween, 要变动的值是scale缩放系数,设置初始值为 {x: 0, y: 0, z: 0}
// 设置好初始值后,物体在 Tween 启动时会 scale 设置为初始值
const tween = new Tween({ scale: Vector.zero });
// 更改这个物体的缩放系数,并设置用 500 毫秒完成更改,to 函数里的参数指的是最终结果的大小
// 这里也就是设置为物体原本的大小
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.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale; })
// 动画完成时关闭 useUpdate 减少无用函数调用
tween.onComplete(() => { this.useUpdate = false });
// 启动这个 tween 动画
tween.start();
}
/**
* 显示建筑
*/
protected showBuild() {
// 定义一个tween, 要变动的值是scale缩放系数,设置初始值为 {x: 0, y: 0, z: 0}
// 设置好初始值后,物体在 Tween 启动时会 scale 设置为初始值
const tween = new Tween({ scale: Vector.zero });
// 更改这个物体的缩放系数,并设置用 500 毫秒完成更改,to 函数里的参数指的是最终结果的大小
// 这里也就是设置为物体原本的大小
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.onUpdate(t => { this.gameObject.worldTransform.scale = t.scale; })
// 动画完成时关闭 useUpdate 减少无用函数调用
tween.onComplete(() => { this.useUpdate = false });
// 启动这个 tween 动画
tween.start();
}
- 新的显示家具函数编写好之后,我们修改原来的
_unlockBuildFun
方法,注释掉显示模型的代码逻辑,改为调用showBuild
方法显示建筑
TypeScript
this._unlockBuildFun = (other: GameObject) => {
// 判断进入的对象是 Character 才执行
if (other instanceof Character) {
... 代码略
// 对象池回收对象
GameObjPool.despawn(this._unlockBtn);
// 显示建筑模型
// this.gameObject.setVisibility(PropertyStatus.On);
this.showBuild();
}
}
this._unlockBuildFun = (other: GameObject) => {
// 判断进入的对象是 Character 才执行
if (other instanceof Character) {
... 代码略
// 对象池回收对象
GameObjPool.despawn(this._unlockBtn);
// 显示建筑模型
// this.gameObject.setVisibility(PropertyStatus.On);
this.showBuild();
}
}
- 查看效果
将建筑分组
接下来将 BuildInfo 脚本拖入到每个需要解锁的家具下,并适当调节各个解锁按钮的相对位置,效果如下:
所有家具家具的解锁按钮正常显示后,接下来我们实现以下效果:
- 逐个显示家具的解锁按钮。
- 解锁完当前组所有家具,显示下一组解锁按钮。
打开
BuildInfo
脚本,添加一个新属性_listener
用来保存解锁事件的监听器。
typescript
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
- 添加一个新方法
ensureNeeds
,用来判断解锁按钮是否满足显示条件
typescript
/**
* 验证是否满足显示解锁按钮的需求
*/
protected ensureNeeds() {
// 满足需求,显示解锁按钮
if (++this._curPro >= this.needs) {
this.initUnlockBtn();
// 注销listener
this._listener.disconnect();
}
}
/**
* 验证是否满足显示解锁按钮的需求
*/
protected ensureNeeds() {
// 满足需求,显示解锁按钮
if (++this._curPro >= this.needs) {
this.initUnlockBtn();
// 注销listener
this._listener.disconnect();
}
}
- 在
onStart
方法中,添加对 groupId 不 0 的处理逻辑
typescript
protected onStart(): void {
// 如果是客户端就直接返回
if (SystemUtil.isClient()) return;
// 开启服务端的onUpdate
this.useUpdate = true;
// 默认隐藏,并显示第0组解锁建筑按钮
this.gameObject.setVisibility(PropertyStatus.Off);
// 关闭碰撞
(this.gameObject as Model).setCollision(PropertyStatus.Off);
// 显示组ID是0的组
if (this.groupId === 0) {
this.initUnlockBtn();
} else {
// 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
}
protected onStart(): void {
// 如果是客户端就直接返回
if (SystemUtil.isClient()) return;
// 开启服务端的onUpdate
this.useUpdate = true;
// 默认隐藏,并显示第0组解锁建筑按钮
this.gameObject.setVisibility(PropertyStatus.Off);
// 关闭碰撞
(this.gameObject as Model).setCollision(PropertyStatus.Off);
// 显示组ID是0的组
if (this.groupId === 0) {
this.initUnlockBtn();
} else {
// 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
}
- 事件监听方法编写完成后,我们需要一个发送事件的方法,找到
showBuild
函数,我们在它动画播放完成后发送一条事件,让程序判断是否显示下一个解锁按钮
typescript
protected showBuild() {
// 定义一个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.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
this.useUpdate = false;
});
// 启动这个tween动画
tween.start();
}
protected showBuild() {
// 定义一个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.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
this.useUpdate = false;
});
// 启动这个tween动画
tween.start();
}
- 脚本编写完成后,将解锁建筑分组,在各自的属性面板中设置好其 groupId,以及 needs 属性。
- 这里将地板的 groupId 设置为 0。
- 三面木墙的 groupId 设置为 1。
- 屋顶的 groupId 设置为 2,needs 设置为 3 (意思是上一组解锁了 3 个才能显示这个按钮,也就是说必须将墙壁全部解锁了才显示屋顶解锁按钮)。
- 灯的 groupId 设置为 3,床、凳子、桌子、垫子的 groupId 设置为 4。
- 运行游戏,看最终效果
脚本完整代码
现在 BuildInfo 中完整代码如下 (点击展开)
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;
/** 当前显示解锁按钮组进度 */
private _curPro: number = 0;
/** 解锁按钮 */
private _unlockBtn: GameObject;
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
/** 解锁建筑的方法 */
private _unlockBuildFun;
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
// 如果是客户端就直接返回
if (SystemUtil.isClient()) return;
// 开启服务端的onUpdate
this.useUpdate = true;
// 默认隐藏,并显示第0组解锁建筑按钮
this.gameObject.setVisibility(PropertyStatus.Off);
// 关闭碰撞
(this.gameObject as Model).setCollision(PropertyStatus.Off);
// 显示组ID是0的组
if (this.groupId === 0) {
this.initUnlockBtn();
} else {
// 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
}
/**
* 验证是否满足显示解锁按钮的需求
*/
protected ensureNeeds() {
// 满足需求,显示解锁按钮
if (++this._curPro >= this.needs) {
this.initUnlockBtn();
// 注销listener
this._listener.disconnect();
}
}
/**
* 初始化解锁建筑按钮
*/
protected async initUnlockBtn() {
// 注意这儿spawn的AssetID是解锁按钮预制体的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;
this._unlockBuildFun = (other: GameObject) => {
// 判断进入的对象是一个Character实例才创建
if (other instanceof Character) {
// 用完了就先取消绑定
trigger.onEnter.remove(this._unlockBuildFun);
// 对象池回收解锁按钮
GameObjPool.despawn(this._unlockBtn);
this.showBuild();
}
}
// 拿到解锁按钮预制体下面的触发器
const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
// 绑定触发器的进入事件
trigger.onEnter.add(this._unlockBuildFun);
}
/**
* 显示建筑
*/
protected showBuild() {
// 定义一个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.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
this.useUpdate = false;
});
// 启动这个tween动画
tween.start();
}
protected onUpdate(DeltaTime: number): void {
TweenUtil.TWEEN.update();
}
}
@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;
/** 当前显示解锁按钮组进度 */
private _curPro: number = 0;
/** 解锁按钮 */
private _unlockBtn: GameObject;
/** 事件监听器,需在解锁按钮回收时注销 */
private _listener: EventListener;
/** 解锁建筑的方法 */
private _unlockBuildFun;
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
// 如果是客户端就直接返回
if (SystemUtil.isClient()) return;
// 开启服务端的onUpdate
this.useUpdate = true;
// 默认隐藏,并显示第0组解锁建筑按钮
this.gameObject.setVisibility(PropertyStatus.Off);
// 关闭碰撞
(this.gameObject as Model).setCollision(PropertyStatus.Off);
// 显示组ID是0的组
if (this.groupId === 0) {
this.initUnlockBtn();
} else {
// 监听是否显示解锁建筑按钮事件,事件名是 "Show_Unlock_Button" + 组号
this._listener = Event.addLocalListener("Show_Unlock_Button" + this.groupId, this.ensureNeeds.bind(this));
}
}
/**
* 验证是否满足显示解锁按钮的需求
*/
protected ensureNeeds() {
// 满足需求,显示解锁按钮
if (++this._curPro >= this.needs) {
this.initUnlockBtn();
// 注销listener
this._listener.disconnect();
}
}
/**
* 初始化解锁建筑按钮
*/
protected async initUnlockBtn() {
// 注意这儿spawn的AssetID是解锁按钮预制体的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;
this._unlockBuildFun = (other: GameObject) => {
// 判断进入的对象是一个Character实例才创建
if (other instanceof Character) {
// 用完了就先取消绑定
trigger.onEnter.remove(this._unlockBuildFun);
// 对象池回收解锁按钮
GameObjPool.despawn(this._unlockBtn);
this.showBuild();
}
}
// 拿到解锁按钮预制体下面的触发器
const trigger = this._unlockBtn.getChildByName("触发器") as Trigger;
// 绑定触发器的进入事件
trigger.onEnter.add(this._unlockBuildFun);
}
/**
* 显示建筑
*/
protected showBuild() {
// 定义一个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.dispatchToLocal("Show_Unlock_Button" + (this.groupId + 1));
this.useUpdate = false;
});
// 启动这个tween动画
tween.start();
}
protected onUpdate(DeltaTime: number): void {
TweenUtil.TWEEN.update();
}
}