Skip to content
游戏UI

游戏UI

UI是游戏中必不可少的一部分,本节课我们需要制作一个UI,用来进行胜利提示、死亡倒计时、关卡进度。

1.创建UI

UI的制作包含两个步骤,分别是创建UI以及编写代码,下面我们就进行创建UI的步骤。

新建UI

在工程内容中点击 “UI”

点击 “新建UI”

将UI的名称修改为 “GameUI”

image-20230829154316894

制作胜利提示UI

创建好UI之后,我们双击UI就能进入UI编辑器,在这里需要根据游戏的实际需求来制作UI。

将图片控件拖动到UI上,并修改大小和颜色(不知道如何修改大小和颜色,可以观看视频)

将文本控件拖动到UI上,并修改大小和位置

将图片控件的名称修改为"mBG"

将文本控件的名称修改为"mTextBlock"

image-20231102135044951

注意

这里的控件名,决定着后面控制UI的代码是否生效。因为代码本质上是通过控件名来查找到对应的控件,如果控件名称和代码中的代码不一致,那么就有可能导致代码查找不到对应的UI控件,最终产生报错

2.导出UI_generate脚本

当我们使用UI的时候,前提是需要先用代码查找到对应的控件。UI编辑器提供了一个“导出所有脚本”的功能,能够将查找控件的代码导出到一个脚本中,这个功能大大提升了我们制作UI的效率。下面给大家展示导出UI脚本的步骤:

导出步骤

在UI编辑器中点击“导出所有脚本”(记得导出前先点击一下保存)

弹出导出完毕窗口就代表UI导出成功了

image-20231102140010069

导出结果

①如果是第一次进行UI脚本导出,就会在脚本下创建一个名为ui-generate的文件夹

②导出的UI脚本会以UI名+"_generate"来进行命名

③这部分代码就是用来查找到名字为 "mBG" 的控件的代码

④这部分代码就是用来查找到名字为 "mTextBlock" 的控件的代码

image-20231102140705952

3.编写UI脚本

有了UI_generate脚本之后,我们就可以创建UI脚本来对UI进行控制了。

创建UI脚本

在“脚本”下新建一个文件夹,命名为“UI”,这个文件夹用来专门存放UI脚本

点击“新建脚本”

将脚本的名称修改为"GameUI"

image-20230829164614478

编写UI脚本

GameUI脚本中编写如下内容:

下列内容主要是对GameUI_Generate脚本进行了继承,因此GameUI就能够通过this来访问到GameUI_Generate中的控件,从而实现了用代码控制UI的功能。

ts
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    protected onAwake(): void {
        this.mTextBlock.text = "胜利提示"
    }

}
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    protected onAwake(): void {
        this.mTextBlock.text = "胜利提示"
    }

}

为什么不直接使用UI_generate脚本?

因为当我们每次使用UI编辑器提供的“导出所有脚本”功能时,都会将UI_generate脚本重新覆盖一次。如果代码直接编写在UI_generate脚本中,这会导致我们编写的代码丢失。

4.尝试将UI进行显示

显示UI,编辑器提供了一套专门的API:UIService

GameStart脚本中添加一行代码:

这行代码的作用就是将UI脚本所代表的UI显示在游戏画面中

ts
import { LevelManager } from "./LevelManager";
import GameUI from "./UI/GameUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(GameUI) 
        }
    }

    /**
     * 周期函数 每帧执行
     * 此函数执行需要将this.useUpdate赋值为true
     * @param dt 当前帧与上一帧的延迟 / 秒
     */
    protected onUpdate(dt: number): void {

    }

    /** 脚本被销毁时最后一帧执行完调用此函数 */
    protected onDestroy(): void {

    }
}
import { LevelManager } from "./LevelManager";
import GameUI from "./UI/GameUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(GameUI) 
        }
    }

    /**
     * 周期函数 每帧执行
     * 此函数执行需要将this.useUpdate赋值为true
     * @param dt 当前帧与上一帧的延迟 / 秒
     */
    protected onUpdate(dt: number): void {

    }

    /** 脚本被销毁时最后一帧执行完调用此函数 */
    protected onDestroy(): void {

    }
}

添加了上面这一行后,运行游戏,就能看UI了,并且UI还显示出了我们设定的文本:

image-20231102142330027

4.胜利提示

有了GameUI以及GameUI脚本后,我们就需要开始编写具体的业务逻辑了。胜利提示功能就是当角色到达终点后,将GameUI显示出来,并把GameUI上的文本控件的内容修改为“胜利啦!”

制作这个功能的核心逻辑就是让GameUI脚本开启一个事件监听,然后当角色到达终点的逻辑里进行事件派发,通知GameUI展示出UI的内容。

所以首先要在GameUI脚本中开启一个本地事件监听

该脚本有如下几个要点:

在UI第一次显示的时候,将背景和文本隐藏

对本地事件"Victory"进行监听,事件触发时将背景和文本进行展示

在本地事件触发时,将mTextBlock控件的文本内容修改为“胜利啦!”

ts
import CheckPointTrigger from "../CheckPointTrigger";
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {
    
    protected onAwake(): void {
        // UI第一次显示的时候,将背景和文本隐藏 
        this.mBG.visibility = SlateVisibility.Hidden 
        this.mTextBlock.visibility = SlateVisibility.Hidden 

        Event.addLocalListener("Victory",()=>{ 
            this.mBG.visibility = SlateVisibility.Visible 
            this.mTextBlock.visibility = SlateVisibility.Visible 

            this.mTextBlock.text = "胜利啦!" 
        }) 
    }
    
}
import CheckPointTrigger from "../CheckPointTrigger";
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {
    
    protected onAwake(): void {
        // UI第一次显示的时候,将背景和文本隐藏 
        this.mBG.visibility = SlateVisibility.Hidden 
        this.mTextBlock.visibility = SlateVisibility.Hidden 

        Event.addLocalListener("Victory",()=>{ 
            this.mBG.visibility = SlateVisibility.Visible 
            this.mTextBlock.visibility = SlateVisibility.Visible 

            this.mTextBlock.text = "胜利啦!" 
        }) 
    }
    
}

事件通信

上述代码中提到的“本地事件监听”是属于“事件通信”中的一个知识点。

关于事件通信的详细介绍大家可以阅读这篇文档:事件通信 | 教程 (ark.online)

有了监听,对应就需要有派发,我们需要在角色到达终点时派发Victory事件。

CheckPointTrigger脚本中进行事件派发:

本次新增的逻辑:

在判断到角色进入终点触发器时,如果进入的角色是当前客户端,就派发"Victory"事件,从而让GameUI展示出UI内容。

ts
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({displayName:"序号"})
    public pointNumber:number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other:GameObject)=>{
            // 进入的物体是否是角色
            if(other instanceof Character){

                // 进入的角色 是否是 当前客户端角色
                if(other == Player.localPlayer.character){
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint",this) 
                    // 播放特效
                    EffectService.playAtPosition("89097",this.gameObject.worldTransform.position)                                     
                    // 播放音效
                    SoundService.playSound("38193")
                }
                
                if(this.pointNumber==-1){
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750",this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if(other == Player.localPlayer.character){ 
                        Event.dispatchToLocal("Victory") 
                    } 
                }
            }
        })
    }
}
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({displayName:"序号"})
    public pointNumber:number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other:GameObject)=>{
            // 进入的物体是否是角色
            if(other instanceof Character){

                // 进入的角色 是否是 当前客户端角色
                if(other == Player.localPlayer.character){
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint",this) 
                    // 播放特效
                    EffectService.playAtPosition("89097",this.gameObject.worldTransform.position)                                     
                    // 播放音效
                    SoundService.playSound("38193")
                }
                
                if(this.pointNumber==-1){
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750",this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if(other == Player.localPlayer.character){ 
                        Event.dispatchToLocal("Victory") 
                    } 
                }
            }
        })
    }
}

效果演示

当角色进入终点,就会展示出GameUI

20230829-171537

5.死亡倒计时

死亡倒计时的制作逻辑和胜利提示类似,都是通过一个事件来进行触发。

所以可以在GameUI脚本中开启一个对死亡事件的监听:

本次新增的逻辑:

在onAwake函数中对本地事件“Death”进行监听,监听到事件之后,先将背景和文本控件进行显示。然后开启一个间隔函数,每隔一秒更新一次文本,倒计时结束时就关闭间隔函数并隐藏背景和文本控件。

ts
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {
    protected onAwake(): void {

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory", () => {
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death", () => { 
            this.mBG.visibility = SlateVisibility.Visible 
            this.mTextBlock.visibility = SlateVisibility.Visible 
 
            let time = 3; 
            this.mTextBlock.text = time.toString() 
            let handle = setInterval(() => { 
                if (time == 1) { 
                    clearInterval(handle) 
                    this.mBG.visibility = SlateVisibility.Hidden 
                    this.mTextBlock.visibility = SlateVisibility.Hidden 
                } 
                time-- 
                this.mTextBlock.text = time.toString() 
            }, 1000) 
        }) 
    }
}
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {
    protected onAwake(): void {

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory", () => {
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death", () => { 
            this.mBG.visibility = SlateVisibility.Visible 
            this.mTextBlock.visibility = SlateVisibility.Visible 
 
            let time = 3; 
            this.mTextBlock.text = time.toString() 
            let handle = setInterval(() => { 
                if (time == 1) { 
                    clearInterval(handle) 
                    this.mBG.visibility = SlateVisibility.Hidden 
                    this.mTextBlock.visibility = SlateVisibility.Hidden 
                } 
                time-- 
                this.mTextBlock.text = time.toString() 
            }, 1000) 
        }) 
    }
}

接着,我们还需要在角色死亡的时候,派发这个死亡事件。

LevelManager脚本中添加如下代码:

ts
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)


    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){ 
            Event.dispatchToLocal("Death") 
        } 
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)


    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){ 
            Event.dispatchToLocal("Death") 
        } 
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}

效果演示

角色进入死亡区域就会触发死亡倒计时

20230829-180414

6.关卡进度

6.1.制作关卡进度UI

还是在GameUI的基础上进行制作,主要是在GameUI上添加了一个进度条和一个进度提示文本

① 拖入一个进度条控件,并调整大小和位置

② 拖入一个文本控件,并调整大小和位置

③ 将进度条控件的名称修改为 "mLevelProgress"

④ 将文本控件的名称修改为 "mLevelText"

⑤ 点击“保存”

⑥ 点击“导出所有脚本”

image-20231102150907991

帮你一波

如果大家不想自己制作UI,可以移步至7.4 我为大家提供好了UI文件

6.2.onShow函数

onShow函数是属于UI脚本的生命周期函数,onShow会在UI被显示的时候进行执行。

我们目前的需求是需要GameUI显示出关卡进度,那么首要任务就是得让GameUI能够接收到最大关卡数和当前关卡数。

所以现在需要在GameUI脚本中,通过onShow来接收到最大关卡数和当前关卡数,在第一次显示UI的时候就更新进度条的进度以及进度文本的内容。

除了第一次显示GameUI的时候需要更新进度条,在玩家每一次进入新的关卡的时候,也需要更新进度条,我们可以通过对本地事件CheckPoint进行监听,来实现进度更新。

将上述逻辑添加到GameUI脚本中:

ts
import CheckPointTrigger from "../CheckPointTrigger"; 
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    /**最大关卡数 */ 
    maxLevelNum:number = 0 
    /**当前关卡数 */ 
    nowLevelNum:number = 0 

    protected onAwake(): void {
        

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            let time = 3;
            this.mTextBlock.text = time.toString()
            let handle =  setInterval(()=>{
                if(time==1){
                    clearInterval(handle)
                    this.mBG.visibility = SlateVisibility.Hidden
                    this.mTextBlock.visibility = SlateVisibility.Hidden
                }
                time--
                this.mTextBlock.text = time.toString()                
            },1000)
        })

        Event.addLocalListener("CheckPoint",(chekPoint:CheckPointTrigger)=>{ 
            this.nowLevelNum = chekPoint.pointNumber 
            // 更新进度条 
            this.freshProgress() 
        }) 
        
    }

    onShow(maxLevelNum:number,nowLevelNum:number){ 
        // 最大关卡数 
        this.maxLevelNum = maxLevelNum 
 
        // 当前关卡数 
        this.nowLevelNum = nowLevelNum 
 
        this.freshProgress() 
        
    } 

    private freshProgress(){ 
        if(this.nowLevelNum == -1){ 
            this.mLevelText.text = "第"+this.maxLevelNum+"关" 
 
            this.mLevelProgress.currentValue = 1 
            return 
        } 
 
        this.mLevelText.text = "第"+this.nowLevelNum+"关" 
 
        this.mLevelProgress.currentValue = this.nowLevelNum / this.maxLevelNum 
    } 
    
}
import CheckPointTrigger from "../CheckPointTrigger"; 
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    /**最大关卡数 */ 
    maxLevelNum:number = 0 
    /**当前关卡数 */ 
    nowLevelNum:number = 0 

    protected onAwake(): void {
        

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            let time = 3;
            this.mTextBlock.text = time.toString()
            let handle =  setInterval(()=>{
                if(time==1){
                    clearInterval(handle)
                    this.mBG.visibility = SlateVisibility.Hidden
                    this.mTextBlock.visibility = SlateVisibility.Hidden
                }
                time--
                this.mTextBlock.text = time.toString()                
            },1000)
        })

        Event.addLocalListener("CheckPoint",(chekPoint:CheckPointTrigger)=>{ 
            this.nowLevelNum = chekPoint.pointNumber 
            // 更新进度条 
            this.freshProgress() 
        }) 
        
    }

    onShow(maxLevelNum:number,nowLevelNum:number){ 
        // 最大关卡数 
        this.maxLevelNum = maxLevelNum 
 
        // 当前关卡数 
        this.nowLevelNum = nowLevelNum 
 
        this.freshProgress() 
        
    } 

    private freshProgress(){ 
        if(this.nowLevelNum == -1){ 
            this.mLevelText.text = "第"+this.maxLevelNum+"关" 
 
            this.mLevelProgress.currentValue = 1 
            return 
        } 
 
        this.mLevelText.text = "第"+this.nowLevelNum+"关" 
 
        this.mLevelProgress.currentValue = this.nowLevelNum / this.maxLevelNum 
    } 
    
}

6.3.用Map来管理所有的检查点

在上一点,我们编写了逻辑,来根据获取到的最大关卡号以及当前关卡号对进度进行更新。

但是我们现在还没有获取最大关卡号的逻辑,所以我们可以在LevelManager脚本中,添加一个Map,用于存储所有的检查点脚本。

LevelManager脚本中添加一个Map,用于存储所有的检查点:

向关卡管理器中添加了一个成员变量checkPointMap,这个变量是一个Map类型,其中Map的key为number类型,用来填入检查点的序号;Map的value为CheckPointTrigger类型

ts
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)

    /**所有的检查点脚本 */ 
    public checkPointMap:Map<number,CheckPointTrigger> = new Map() 

    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)

    /**所有的检查点脚本 */ 
    public checkPointMap:Map<number,CheckPointTrigger> = new Map() 

    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}

6.4 让Map获取到所有的检查点

上一点我们在LevelManger脚本中添加了一个checkPointMap,所以现在就需要让每个CheckPointTrigger脚本,在启动的时候,将自己设置到checkPointMap中。

CheckPointTrigger脚本中添加一行代码:

tsx
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({ displayName: "序号" })
    public pointNumber: number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        LevelManager.instance.checkPointMap.set(this.pointNumber, this); 

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other: GameObject) => {
            // 进入的物体是否是角色
            if (other instanceof Character) {

                // 进入的角色 是否是 当前客户端角色
                if (other == Player.localPlayer.character) {
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint", this)
                    // 播放特效
                    EffectService.playAtPosition("89097", this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("38193")
                }

                if (this.pointNumber == -1) {
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750", this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if (other == Player.localPlayer.character) {
                        Event.dispatchToLocal("Victory")
                    }
                }
            }
        })
    }


}
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({ displayName: "序号" })
    public pointNumber: number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        LevelManager.instance.checkPointMap.set(this.pointNumber, this); 

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other: GameObject) => {
            // 进入的物体是否是角色
            if (other instanceof Character) {

                // 进入的角色 是否是 当前客户端角色
                if (other == Player.localPlayer.character) {
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint", this)
                    // 播放特效
                    EffectService.playAtPosition("89097", this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("38193")
                }

                if (this.pointNumber == -1) {
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750", this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if (other == Player.localPlayer.character) {
                        Event.dispatchToLocal("Victory")
                    }
                }
            }
        })
    }


}

6.5.调整GameUI显示的时机

由于现在GameUI需要在显示的时候接收到最大关卡号,所以就不能在GameStart脚本中调用UIService来对其进行显示了,因为GameStart脚本启动的比较快,可能会出现LevelManager还没有获取到所有检查点的时候,GameStart脚本就已经将GameUI显示出来了,这就会导致GameUI获取到错误的最大关卡号。

所以我们首先需要将GameStart脚本中显示GameUI的逻辑删除:

ts
import { LevelManager } from "./LevelManager";
import GameUI from "./UI/GameUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()
			
            UIService.show(GameUI) 
        }
    }

    /**
     * 周期函数 每帧执行
     * 此函数执行需要将this.useUpdate赋值为true
     * @param dt 当前帧与上一帧的延迟 / 秒
     */
    protected onUpdate(dt: number): void {

    }

    /** 脚本被销毁时最后一帧执行完调用此函数 */
    protected onDestroy(): void {

    }
}
import { LevelManager } from "./LevelManager";
import GameUI from "./UI/GameUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()
			
            UIService.show(GameUI) 
        }
    }

    /**
     * 周期函数 每帧执行
     * 此函数执行需要将this.useUpdate赋值为true
     * @param dt 当前帧与上一帧的延迟 / 秒
     */
    protected onUpdate(dt: number): void {

    }

    /** 脚本被销毁时最后一帧执行完调用此函数 */
    protected onDestroy(): void {

    }
}

在6.4.中,我们将各个检查点都添加到了关卡管理器的checkPointMap中,因此我们只需要获取map的count即可获得最大关卡数。

所以现在我们就需要在LevelManger脚本中,延迟2000毫秒,等待所有检查点获取完毕之后,再调用显示GameUI的逻辑,并且将最大关卡数传递给GameUI脚本。

LevelManager脚本向GameUI发送最大关卡数:

本此新增的逻辑:

UIService.show这个API的第二个参数是一个变长参数,可以传递任意长度的数值进去

通过UIService.show将最大关卡数和当前关卡数传递给了GameUI

显示UI的逻辑被延迟了2000毫秒。(这是为了防止检查点启动时序比关卡管理器慢,导致关卡管理器不能够在传递参数前获取到正确的关卡管理器数量)

ts
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI" 

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)

    /**所有的检查点脚本 */
    public checkPointMap:Map<number,CheckPointTrigger> = new Map()

    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

        setTimeout(() => { 
            UIService.show(GameUI,this.checkPointMap.size,0)     
        }, 2000); 
    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI" 

/**
 * 关卡管理器
 */
export class LevelManager{
    // 单例模式
    private static _instacne:LevelManager
    public static get instance():LevelManager{
        if(LevelManager._instacne==null){
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger:Trigger

    /**复活位置 */
    private _rebornPosition:Vector = new Vector(10,0,420)

    /**所有的检查点脚本 */
    public checkPointMap:Map<number,CheckPointTrigger> = new Map()

    public async init(){
        this._deathTrigger =  await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other:GameObject)=>{
            // 当进入的物体是角色类型
            if(other instanceof Character){
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint",(checkPointTrigger:CheckPointTrigger)=>{
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

        setTimeout(() => { 
            UIService.show(GameUI,this.checkPointMap.size,0)     
        }, 2000); 
    }

    /**让角色死亡 */
    private charDeath(char:Character){
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421",char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if(char==Player.localPlayer.character){
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    private charReborn(char:Character){
        if(char == Player.localPlayer.character){
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}

6.6.效果演示

前面的步骤如果没有问题,那么现在运行游戏,每当角色经过检查点,就会刷新关卡进度。

20230829-180615

7.本节完整代码

7.1.GameUI

ts
import CheckPointTrigger from "../CheckPointTrigger";
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    /**最大关卡数 */
    maxLevelNum:number = 0
    /**当前关卡数 */
    nowLevelNum:number = 0

    protected onAwake(): void {
        

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            let time = 3;
            this.mTextBlock.text = time.toString()
            let handle =  setInterval(()=>{
                if(time==1){
                    clearInterval(handle)
                    this.mBG.visibility = SlateVisibility.Hidden
                    this.mTextBlock.visibility = SlateVisibility.Hidden
                }
                time--
                this.mTextBlock.text = time.toString()                
            },1000)
        })

        Event.addLocalListener("CheckPoint",(chekPoint:CheckPointTrigger)=>{
            this.nowLevelNum = chekPoint.pointNumber
            // 更新进度条
            this.freshProgress()
        })
    }

    onShow(maxLevelNum:number,nowLevelNum:number){
        // 最大关卡数
        this.maxLevelNum = maxLevelNum

        // 当前关卡数
        this.nowLevelNum = nowLevelNum

        this.freshProgress()
       
    }

    private freshProgress(){
        if(this.nowLevelNum == -1){
            this.mLevelText.text = "第"+this.maxLevelNum+"关"

            this.mLevelProgress.currentValue = 1
            return
        }

        this.mLevelText.text = "第"+this.nowLevelNum+"关"

        this.mLevelProgress.currentValue = this.nowLevelNum / this.maxLevelNum
    }
}
import CheckPointTrigger from "../CheckPointTrigger";
import GameUI_Generate from "../ui-generate/GameUI_generate";


export default class GameUI extends GameUI_Generate {

    /**最大关卡数 */
    maxLevelNum:number = 0
    /**当前关卡数 */
    nowLevelNum:number = 0

    protected onAwake(): void {
        

        this.mBG.visibility = SlateVisibility.Hidden
        this.mTextBlock.visibility = SlateVisibility.Hidden

        Event.addLocalListener("Victory",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            this.mTextBlock.text = "胜利啦!"
        })

        Event.addLocalListener("Death",()=>{
            this.mBG.visibility = SlateVisibility.Visible
            this.mTextBlock.visibility = SlateVisibility.Visible

            let time = 3;
            this.mTextBlock.text = time.toString()
            let handle =  setInterval(()=>{
                if(time==1){
                    clearInterval(handle)
                    this.mBG.visibility = SlateVisibility.Hidden
                    this.mTextBlock.visibility = SlateVisibility.Hidden
                }
                time--
                this.mTextBlock.text = time.toString()                
            },1000)
        })

        Event.addLocalListener("CheckPoint",(chekPoint:CheckPointTrigger)=>{
            this.nowLevelNum = chekPoint.pointNumber
            // 更新进度条
            this.freshProgress()
        })
    }

    onShow(maxLevelNum:number,nowLevelNum:number){
        // 最大关卡数
        this.maxLevelNum = maxLevelNum

        // 当前关卡数
        this.nowLevelNum = nowLevelNum

        this.freshProgress()
       
    }

    private freshProgress(){
        if(this.nowLevelNum == -1){
            this.mLevelText.text = "第"+this.maxLevelNum+"关"

            this.mLevelProgress.currentValue = 1
            return
        }

        this.mLevelText.text = "第"+this.nowLevelNum+"关"

        this.mLevelProgress.currentValue = this.nowLevelNum / this.maxLevelNum
    }
}

7.2.CheckPointTrigger

ts
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({displayName:"序号"})
    public pointNumber:number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        LevelManager.instance.checkPointMap.set(this.pointNumber,this);

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other:GameObject)=>{
            // 进入的物体是否是角色
            if(other instanceof Character){

                // 进入的角色 是否是 当前客户端角色
                if(other == Player.localPlayer.character){
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint",this)   
                    // 播放特效
                    EffectService.playAtPosition("89097",this.gameObject.worldTransform.position)                                     
                    // 播放音效
                    SoundService.playSound("38193")
                }
                
                if(this.pointNumber==-1){
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750",this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if(other == Player.localPlayer.character){
                        Event.dispatchToLocal("Victory")
                    }
                }
            }
        })
    }
}
import { LevelManager } from "./LevelManager"

@Component
export default class CheckPointTrigger extends Script {

    @Property({displayName:"序号"})
    public pointNumber:number = 0

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected onStart(): void {

        LevelManager.instance.checkPointMap.set(this.pointNumber,this);

        let trigger = this.gameObject as Trigger
        trigger.onEnter.add((other:GameObject)=>{
            // 进入的物体是否是角色
            if(other instanceof Character){

                // 进入的角色 是否是 当前客户端角色
                if(other == Player.localPlayer.character){
                    // 本地事件通信(派发)
                    Event.dispatchToLocal("CheckPoint",this)   
                    // 播放特效
                    EffectService.playAtPosition("89097",this.gameObject.worldTransform.position)                                     
                    // 播放音效
                    SoundService.playSound("38193")
                }
                
                if(this.pointNumber==-1){
                    // 播放动作
                    other.loadAnimation("14509").play()
                    // 播放特效
                    EffectService.playAtPosition("142750",this.gameObject.worldTransform.position)
                    // 播放音效
                    SoundService.playSound("47425")

                    if(other == Player.localPlayer.character){
                        Event.dispatchToLocal("Victory")
                    }
                }
            }
        })
    }
}

7.3.LevelManager

tsx
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
    // 单例模式
    private static _instacne: LevelManager
    public static get instance(): LevelManager {
        if (LevelManager._instacne == null) {
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger: Trigger

    /**复活位置 */
    private _rebornPosition: Vector = new Vector(10, 0, 420)

    /**所有的检查点脚本 */
    public checkPointMap: Map<number, CheckPointTrigger> = new Map()

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

        setTimeout(() => {
            UIService.show(GameUI, this.checkPointMap.size, 0)
        }, 2000);
    }

    /**让角色死亡 */
    public charDeath(char: Character) {
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421", char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if (char == Player.localPlayer.character) {
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    public charReborn(char: Character) {
        if (char == Player.localPlayer.character) {
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
    // 单例模式
    private static _instacne: LevelManager
    public static get instance(): LevelManager {
        if (LevelManager._instacne == null) {
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger: Trigger

    /**复活位置 */
    private _rebornPosition: Vector = new Vector(10, 0, 420)

    /**所有的检查点脚本 */
    public checkPointMap: Map<number, CheckPointTrigger> = new Map()

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })


        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {
            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()
        })

        setTimeout(() => {
            UIService.show(GameUI, this.checkPointMap.size, 0)
        }, 2000);
    }

    /**让角色死亡 */
    public charDeath(char: Character) {
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421", char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if (char == Player.localPlayer.character) {
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    public charReborn(char: Character) {
        if (char == Player.localPlayer.character) {
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }
}

7.4.GameUI文件

如果大家懒得拼UI的话,可以直接下载这个文件:

https://arkimg.ark.online/GameUI.rar

什么?你问我怎么用?

① 点击UI

② 随便选中一个UI,然后右键,点击“打开文件所在位置”

③ 将压缩包解压到上一步打开的文件夹中,然后回到游戏即可看见UI

④ 有了UI后,大家回到导出UI的介绍中继续制作即可

image-20231031104159685