avatar
罗传月武(YueWu)@罗传月武(yuewu)
技术
新闻

UE4基于栈状态机的UI管理(框架设计)

ue4 stack ui

前言

三流程序写UI,但即便是UI这种一般程序员比较排斥去做的事情,想把它写好写的优雅,也是需要点考究的。

我虽然不知道什么是好代码,但我知道什么是菜代码。浏览过多数商城的UI系统,查阅过网上无数的UMG“最佳实践”,很可惜,大多数都没有采用一种优雅的方法进行实现,没有统一的UI管理,做法也是千奇百怪,有在PlayerController里通过Enum来切换不同的Widget的,也有我想显示个UI就立即创建的,也有GetAllWidgetOfClass关掉所有UI然后再开启自己想要的。甚至还有看起来花里胡哨的“One widget rules all!”(然后各种WidgetSwitcher切换,利用接口夸UI调用。)

以上全菜。

如果你了解栈的概念,和状态机的概念,请继续往下看(栈和状态机是个做游戏的都知道,不适合浪费太多文字解释)

思路

栈状态机才是解决该类问题的杀手锏(但不是解决问题的唯一思路),各位看官回忆一下自己玩过的游戏。

  1. 打开菜单:按Tab就弹出暂停菜单(层1)进入视口,暂停菜单里有继续游戏,游戏设置,退出游戏几个按钮,当你点击游戏设置的时候,又会弹出一个新的游戏设置菜单(层2)进入视口,盖住了暂停菜单,游戏设置菜单下又有音效设置,图形设置等,当你点击图形设置,又会弹出一个图形设置菜单(层3)。
  2. 关闭菜单:每一个菜单右上角有一个X可以关闭UI,当你关闭图形设置菜单(层3)后,图形设置菜单(层3)从视口移除,游戏设置菜单(层2)重回视口,当你关闭层2,暂停菜单(层1)重回视口。
  3. 一次关闭所有菜单:比如玩家按Esc,就直接回到了游戏界面,这一操作本质上就是自动关闭了层321。

好,各位发现没。

上述的打开关闭菜单的操作是否就像一个栈:一个菜单进入视口(入栈Push,进入栈顶);一个菜单退出视口(出栈Pop,从最顶层离开);顶层菜单被新打开的菜单覆盖从而不可操作(在栈中往下走了一层,Push,离开最顶层);因为顶层菜单退出视口,下面被覆盖的菜单又重新回到最顶层(在栈中往上走了一层,Pop,重新进入栈顶)。按Esc关闭所有菜单,其实就是把菜单从栈顶一个个弹出。

同时它还像一个状态机:栈中的每一个菜单都是一个状态机中的状态,一个状态机同时只有一个激活状态,就是栈顶的那一个状态(CurrentState),菜单进入/退出栈顶就是状态的Enter和Exit。

实现通用状态机

既然UI操作是有规律可言的,且可以用栈状态机进行模拟,那咱们首先就在虚幻里实现一个通用的栈状态机,然后再基于栈状态机实现一个UI栈状态机(因为栈状态机又不是只能拿来做UI管理的。)

首先每个状态都可以进入,退出,和更新。即便是状态机也是如此。而处于栈中的状态还可以被Push和Pop。以下是具体实现。

类型定义

ue4 stack ui

新建一个枚举和会用到的委托声明,放在任意头文件里,比如StateMachineTypes.h 截图按代码顺序来的。

栈状态接口

ue4 stack ui

再新建一个虚幻接口叫StackStateInterface.h,该接口定义了状态的共有行为。 截图按代码顺序来的。

栈状态

ue4 stack ui

然后新建一个栈状态StackState.h实现StackStateInterface。 截图按代码顺序来的。


ue4 stack ui

StackState.cpp

栈状态机定义

ue4 stack ui

现在有了状态了,再实现一个栈状态机,为了复用,做成组件。 StackStateMachineComponent.h 截图按代码顺序来的。

栈状态机也实现了StackStateInterface,同时可以PushState和PopState,以及Pop指定State和Pop所有State


ue4 stack ui

这一部分主要是提供给蓝图的回调,确保灵活。


ue4 stack ui

GetterSetter,供外部访问。

栈状态机实现


ue4 stack ui

状态入栈实现

ue4 stack ui

状态弹栈实现


ue4 stack ui

按指定数量弹栈或清空栈。


ue4 stack ui

确保不断更新栈中的当前状态


ue4 stack ui

同时,状态机本身也是个状态哦

ue4 stack ui

我们需要知道状态的Push和Pop,比如打个log让我们知道栈中都发生了什么。

实现UI栈状态机

UI栈状态

现在咱们实现了一个通用栈状态机,已经可以完成很多事情,甚至可以拓展,所以接下来我要拓展栈状态机,进行UI管理,UE4的UI都是UserWidget,所以咱们实现一个UIState继承自UserWidget,其功能跟上面的UStackState没什么区别,除了继承的父类不一样。但两者都实现了IStackStateInterface。这样,我们游戏里创建的UserWidget都是一个可以被栈状态机管理的栈状态了。

ue4 stack ui

UIState.h,和StackState.h相似,只是供蓝图使用的接口现在有Native实现。


ue4 stack ui

UIState.cpp,跟StackState.cpp相似


ue4 stack ui

当一个UI状态以Push/Pop形式进入/退出顶层

ue4 stack ui

Update暂无实现,供蓝图实现。

UI栈状态机


ue4 stack ui

UIManagerComponent.h,关于GetCurrentUIState,用到了DeterminesOutputType = StateClass和DynamicOutputParam = OutState 这两个meta

UI栈状态机用法

现在有了通用的栈状态和栈状态机,我们也添加了UI栈状态和UI栈状态机(前面的UUiMnanagerComponent)。那么这个Manager一般放在PlayerController下。所有游戏中的需要作为“界面”使用的UMG都应该继承自UIState。

1.在PlayerController中。


ue4 stack ui

2.暂停菜单 UMG_PauseState

ue4 stack ui


ue4 stack ui

3.设置菜单 UMG_SettingState


ue4 stack ui


ue4 stack ui

最佳实践/设计理念/答疑

1.为什么代码要截图而不是粘贴?

因为你懒。你敲了才是你的,你不敲,还是我的。

2.前面不是说要用到这个UI的时候才创建不好吗,为什么你的截图中还是要用的时候才创建?

是不好,尤其是在那个UI依赖的东西多的情况下,我这里只是出于演示目的,实际上我得做法是在UIManager里维护一个可配置的TMap<FName,TSubclassof<UUIState>> States,然后在UIManager的Beginplay里一次创建好所有UI并存入TMap<FName,UUIState*> StateInstances里,然后写一个Helper函数如 PushNamedState(FName UIName);

3.假如每个界面的开启关闭我需要动画过渡呢?

你可以继承一个AnimatedUIState,写一堆过渡效果(透明度lerp,color等)提供一些接口函数如 GetTransitionTarget(让蓝图可以重载告知过渡效果的应用目标),然后在状态Enter/Exit执行相关过渡效果。如果要用Umg里的动画,很遗憾UMG动画不能继承,但只是过渡的话代码实现即可。

4.假如我的UI不是一层一层的呢,比如说一个界面上有多个Tab。

这时候你可以用WidgetSwitcher确保这个UI都在这一层。只是说这一层复杂点而已。

5.游戏的InGameUI怎么实现呢,比如玩家的血条那些的。

一般我的UIManager在游戏过程中,都会加一个默认状态叫UMG_PlayingState,玩家游戏过程中的UI都显示在上面,而且还能通过UICount进行一些逻辑判断,比如我只有UICount=1(也就是只有一个Playing状态的时候),我按Tab才能弹出暂停菜单。而且我这个PlayingState还可以做很多比如玩家捡到东西的屏幕提示等(ShowInfo(FText Text))。

6.由于UIManager一般放在PlayerController下,但PlayerController会存在于服务端(特指DedicatedServer,)而服务端没有UMG。这种多人联机情况下怎么办(2020.6.1日新增)?

对UIManager操作的时候,先GetPlayerController,调用上面的IsLocalPlayerController。

确保UI的操作都发生在本地玩家上。

7.还有别的疑问,欢迎评论区提出。