游戏UI
UI是游戏中必不可少的一部分,本节课我们需要制作一个UI,用来进行胜利提示、死亡倒计时、关卡进度。
1.创建UI
UI的制作包含两个步骤,分别是创建UI以及编写代码,下面我们就进行创建UI的步骤。
新建UI
①在工程内容中点击 “UI”
②点击 “新建UI”
③将UI的名称修改为 “GameUI”
制作UI
创建好UI之后,我们双击UI就能进入UI编辑器,在这里需要根据游戏的实际需求来制作UI。
①将控件拖动到画布中进行编辑。这里主要使用到了图片控件、文本控件、进度条控件
②修改控件的名称,将需要用代码控制的控件名以小写字母开头进行命名
2.导出UI_generate脚本
当我们使用UI的时候,前提是需要先用代码查找到对应的控件。UI编辑器提供了一个“导出所有脚本”的功能,能够将查找控件的代码导出到一个脚本中,这个功能大大提升了我们制作UI的效率。下面给大家展示导出UI脚本的步骤:
导出步骤
①在UI编辑器中点击“导出所有脚本”(记得导出前先点击一下保存)
②弹出导出完毕窗口就代表UI导出成功了
导出结果
①如果是第一次进行UI脚本导出,就会在脚本下创建一个名为ui-generate的文件夹
②导出的UI脚本会以UI名+"_generate"来进行命名
③脚本会查找UI中以小写字母开头命名的控件
3.编写UI脚本
有了UI_generate脚本之后,我们就可以创建UI脚本来对UI进行控制了。
创建UI脚本
①在“脚本”下新建一个文件夹,命名为“UI”,这个文件夹用来专门存放UI脚本
②点击“新建脚本”
③将脚本的名称修改为"GameUI"
编写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
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)
})
}
}
效果演示
角色进入死亡区域就会触发死亡倒计时
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")
}
}
}
})
}
}
效果演示
每当角色经过检查点,就会刷新关卡进度