Skip to content
游戏UI

游戏UI

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

1.创建UI

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

新建UI

在工程内容中点击 “UI”

点击 “新建UI”

将UI的名称修改为 “GameUI”

image-20230829154316894

制作UI

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

将控件拖动到画布中进行编辑。这里主要使用到了图片控件、文本控件、进度条控件

修改控件的名称,将需要用代码控制的控件名以小写字母开头进行命名

image-20230829154924566

2.导出UI_generate脚本

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

导出步骤

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

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

image-20230829155610638

导出结果

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

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

③脚本会查找UI中以小写字母开头命名的控件

image-20230829160013831

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 {

    }

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


export default class GameUI extends GameUI_Generate {

    protected onAwake(): void {

    }

}

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

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

4.胜利提示

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

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

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

该脚本有如下几个要点:

onAwake函数,会在UI被创建的时候进行调用一次

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

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

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 = "胜利啦!"
        })
    }
}

有了监听,对应就需要有派发,我们需要在角色到达终点时派发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){
                    // 播放特效
                    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){
                    // 播放特效
                    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") 
                    } 
                }
            }
        })
    }
}

UI逻辑编写好之后,我们需要让这些逻辑正常直接,就还要使用代码将GameUI动态创建出来。

我们在LevelManager脚本中对GameUI进行创建:

本次新增的逻辑:

在关卡管理器init函数中,使用UIService提供的show函数来将GameUI进行展示。

UIService.show()

这句代码在不同情况下的作用不同。当UI未被创建时调用show,UIService会先去createUI,然后再将UI显示出来;如果UI已经被创建了,那么UI就会直接进行显示。

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

/**
 * 关卡管理器
 */
export class LevelManager {
    
	// 省略代码
    ......

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


        UIService.show(GameUI)  
    }

    // 省略代码
    ......
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
    
	// 省略代码
    ......

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


        UIService.show(GameUI)  
    }

    // 省略代码
    ......
}

效果演示

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

20230829-171537

5.死亡倒计时

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

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

本次新增的逻辑:

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

ts
import CheckPointTrigger from "../CheckPointTrigger";
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 CheckPointTrigger from "../CheckPointTrigger";
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) 
        }) 
    }
}

效果演示

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

20230829-180414

6.关卡进度

6.1.确定最大关卡数

想要知道进度,就得提前知道最大关卡数,我们可以让关卡管理器来对全部检查点进行管理,也就能获取到最大关卡数了。

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

本次新增的逻辑:

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

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

/**
 * 关卡管理器
 */
export class LevelManager{
    // 省略代码
    ......

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

    public async init(){
		// 省略代码
        ......
    }

	// 省略代码
	......
}
import CheckPointTrigger from "./CheckPointTrigger"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager{
    // 省略代码
    ......

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

    public async init(){
		// 省略代码
        ......
    }

	// 省略代码
	......
}

CheckPointTrigger脚本执行onStart函数时,将自己添加到关卡管理器的checkPointMap中

本此新增的逻辑:

将自己set到关卡管理器的checkPointMap中

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);
	
        // 省略代码
        ......
    }
}
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);
	
        // 省略代码
        ......
    }
}

6.2.将最大关卡数传递给GameUI

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

所以现在我们就需要将这个参数传递给GameUI,我们可以通过show函数来传递参数。

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

本此新增的逻辑:

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

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

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

ts
// 省略代码
......
export class LevelManager{
	// 省略代码
    ......

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

    public async init(){
        // 省略代码
        ......

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

	// 省略代码
	......
}
// 省略代码
......
export class LevelManager{
	// 省略代码
    ......

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

    public async init(){
        // 省略代码
        ......

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

	// 省略代码
	......
}

GameUI脚本在onShow函数中接收关卡信息,并将信息刷新到UI上:

本此新增的逻辑:

向GameUI脚本中添加了两个成员变量,maxLevelNum和nowLevelNum

添加了一个onShow函数,并接收关卡管理器传来的最大关卡数和当前关卡数

编写了一个刷新函数freshProgress,它能够刷新进度条的内容

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 
    
    // 省略代码
    ......


    onShow(maxLevelNum: number, nowLevelNum: number) { 
        // 最大关卡数 
        this.maxLevelNum = maxLevelNum 
 
        // 当前关卡数 
        this.nowLevelNum = nowLevelNum 
        // 刷新UI信息 
        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 
    
    // 省略代码
    ......


    onShow(maxLevelNum: number, nowLevelNum: number) { 
        // 最大关卡数 
        this.maxLevelNum = maxLevelNum 
 
        // 当前关卡数 
        this.nowLevelNum = nowLevelNum 
        // 刷新UI信息 
        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.经过检查点刷新进度

除了UI第一次显示的时候需要刷新进度,我们还需要当角色通过检查点的时候刷新进度。

“当”角色通过检查点的时候,写代码时可以有一个简单的技巧,就是出现“当”这种逻辑时,我们就可以考虑使用事件监听来完成该逻辑。

所以我们需要在GameUI脚本中开启一个监听,监听角色进入检查点的事件

本此新增的逻辑:

在onAwake里对事件“checkPoint”进行监听,该事件触发时会将具体是碰到了哪个检查点传递过来,然后根据这个检查点的关卡号来刷新关卡进度

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 {
		// 省略代码
        ......

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

}
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 {
		// 省略代码
        ......

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

}

有了监听就应该有派发,下面就需要在CheckPointTrigger脚本中派发事件

本此新增的逻辑:

当当前客户端进入检查点时,派发"CheckPoint"事件,并将自己作为参数传递出去

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")
                    }
                }
            }
        })
    }
}

效果演示

每当角色经过检查点,就会刷新关卡进度

20230829-180615