欢迎光临散文网 会员登陆 & 注册

TomLooman_ActionRoguelike_第七章UMG和Player属性

2023-08-10 08:17 作者:别叫我小红  | 我要投稿

该专栏用于保存对TomLooman的ActionRoguelike项目的学习笔记,学习过程中的思考与记录不一定准确。


教程参考:https://github.com/tomlooman/ActionRoguelike

基于UE5.0的项目实现:https://github.com/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_08_05

UMG和Player属性:属性组件,带有数据绑定的UMG,基于事件的UMG,用属性进行伤害

 

我们希望用一个东西来表示角色和物体的血量之类的属性,最直接的方法是将这些属性作为角色类的成员变量,但是由此而来的一个问题是角色类将会变得非常大,难以维护,而且这些属性只能用在某一个类中,复用性差,所以我们将这些属性抽象成一个属性类。

这个属性组件类继承自Actor组件,和交互组件类一样。BlueprintSpawnableComponent是类元数据说明符,表示组件类可由蓝图生成。

 

在类中,我们声明一个成员变量表示生命值,并想要通过一个成员函数修改它。

定义了属性组件类之后,我们要将其作为角色类的一个成员变量,这样一来,角色类通过属性组件类的成员变量,相当于拥有了属性组件类中定义的那些属性。

 

目前为止,还没有东西能触发属性组件类中修改血量函数的执行。我们希望在角色被子弹击中时改变角色血量,所以我们在子弹类中定义overlap事件。

注意,OnComponentBeginOverlap相当于蓝图中的Event(事件),与组件相关,当有东西和这个组件overlap时触发(be called)。OnActorOverlap是类的成员函数,与整个类的实例相关。AddDynamic将UObject类的实例(SphereComp)和类成员函数(OnActorOverlap)通过动态多播代理(OnComponentBeginOverlap)绑定到一起。所以这一行代码表示,一旦触发了SphereComp->OnComponentBeginOverlap这一事件,就执行函数ASMagicProjectile::OnActorOverlap。

 

触发事件后执行的成员函数定义如下,这里的函数形式继承自Actor基类。UFUNTCION()让类的成员函数变为UFunction类型,UFunction是一种C++函数,可以被UE的反射系统识别。

在函数的实现中,我们首先判断了触发碰撞组件overlap事件的Actor非空,且不是发出子弹的角色(这一点解决了之前向右移动时发出子弹,子弹会在角色身上explode的问题)。

然后我们获取触发事件的Actor上的属性组件成员,通过GetComponentByClass函数实现,其函数原型为UActorComponent* GetComponentByClass(TSubclassOf<UActorComponent> ComponentClass) const,因此需要一个类作为函数参数,可通过类名::StaticClass()获得一个类的引用。同时,因为GetComponentByClass得到的UActorComponent*类型的对象,而不是我们想要的属性组件类对象这里需要进行以此类型转换,用Cast实现。

再然后我们判断触发事件额Actor是否有属性组件成员,如果有的话,执行该属性组件成员中修改血量的成员函数。

为什么这里要定义碰撞组件的overlap事件,而不是hit事件?因为比方在游戏射击游戏中,我们不想让子弹对友方也造成伤害,但是友方和敌方的碰撞属性又一般是同样的,也就无法通过碰撞属性区分,此时就可以对友方和敌方都视为overlap,但在overlap事件中判断OtherActor是友方还是敌方。

另外,我们要修改子弹与世界中其他物体的碰撞属性,将我们希望子弹能对它造成伤害的类型的碰撞属性设为overlap。否则,如果还是block,会出现教程中所说的,角色发射子弹时,角色会产生一个位移,因为此时角色和子弹是block的,会发生hit事件,所以角色会被子弹推动。

 

有了角色的属性之后,我们自然地想将属性,比如血量,显示在屏幕上,这里就要用到UMG,之前做准心时使用过。

这里我们放置了一个Text表示属性值,一个ProgressBar对属性值进行可视化,还用了一个HorizontalBox,这样我们就不用自己布局进行对其了。

我们可以将Text绑定到某个函数上,将函数返回值作为Text显示的值。需要注意的是,这个绑定的函数是在每个Tick都会执行的,所以可能开销较大。

该函数的蓝图表示如下,

GetOwningPlayer的函数原型是virtual APlayerController * GetOwningPlayer() const,获得的是PlayerController。GetOwningPlayerPawn的函数原型是APawn * GetOwningPlayerPawn() const,获得的是玩家控制的Character。

这里想要获得角色的属性组件成员的血量成员,还有一种方法,利用Cast将APawn*转换为玩家控制的Character的类型,然后直接访问成员变量。因为GetComponentByClass的做法需要遍历Actor的组件,所以可能效率低一些。

另外,一开始对Character是否为空的判定是有用的,因为游戏一开始时角色可能还没生成。

 

我们之前提到了这与Text绑定的函数会在每个Tick都执行,所以它除了低效之外,还无法告知我们属性变化的时间信息。如果我们想要在属性变化,比如受伤减血时,显示一些动画,比如屏幕闪烁红光,就需要一个类似Event事件的东西来触发。

就像子弹类中做的那样,触发OnActorOverlap事件后,执行被overlap的对象中的某个函数。但是现有的事件中并没有能够表示“角色的属性组件成员中的血量成员发生变化”的事件,所以我们要自定义一个“事件”(C++中的委托在概念好像可以这么解释)。



 

这个委托(Delegate)不一定要定义在属性组件类中,当委托被多个类使用时,也可以定义在一个单独的头文件中,然后在要使用该委托的文件中include即可。

(1)


从名字可以看出这里声明了一个有四个参数的动态多播委托。第一个值是委托的名字,之后是参数。注意,和函数的参数列表不同的是,委托中的参数类型和参数用逗号隔开。

 

类似于函数类,我们声明之后还需要实例化。我们在属性组件类中将其实例化为一个成员变量。

同时,从它使用UPROPERTY中也可以看出,委托并不是一个函数,而是更像一个类。

BlueprintCallable让我们在UI中也可以调用这个事件。

(2)

 

目前为止,还无人触发这个事件。因为这个事件是通知其它对象“角色血量改变”这一事实的,所以我们应该在改变血量的函数中触发该事件,这里的“触发”对于委托来说就是“广播”。

可以看出,委托的广播与函数调用的形式类似,或者说类似于调用类的成员函数,需要传入委托声明时要求的参数。

这里的InstigatorActor暂时为空,后续会补充。触发该事件的就是这个属性组件本身,所以第二个参数为this。

(4)


此时我们可以发现,就像碰撞组件有OnComponentHit那些事件一样,属性组件中出现了我们自定义的OnHealthChanged事件。

我们也可以生成事件的蓝图节点,参数就是我们广播传入的参数。当触发该事件后,就进行事件节点后的蓝图执行流。

 

当我们把“角色血量变化”构建为一个事件,我们就可以在之前的UI中调用了。

首先看以下的蓝图部分,在这里我们先获取玩家控制的Character,然后获取Character中的属性组件成员变量,因为属性组件成员变量有我们想调用的FOnHealthChanged委托的示例OnHealthChanged。再然后是关键的一步,我们将这个委托绑定到另一个事件上,那么当委托OnHealthChanged被触发时,就会触发所绑定的事件。

(3)

OnHealthChanged委托绑定的事件就是要修改我们血条和血量文字。以下是用ProcessBar实现血条时的蓝图。

 

上面涉及到了UI中的EventConstruct和EventPreConstruct,两者的区别在于EventConstruct在我们运行游戏时才运行,而EventPreConstruct在设计时就运行。例如,如果我们在EventConstruct修改Text的文字,只有游戏运行时我们才能看到Text的变化,而如果我们在EventPreConstruct,我们在设计页面就能看到Text的变化。

 

关于UE的委托(Delegate),最重要的就是上面内容中的(1)(2)(3)(4),其中(3)和(4)的出现顺序并没有反。

根据这个例子,总结委托的使用步骤如下,

1. **在头文件中声明委托**:在您的类的头文件中,使用 `DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams` 宏来声明您的委托类型。例如:

 

```cpp

DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChanged, AActor*, InstigatorActor, USAttributeComponent*, OwningComp, float, NewHealth, float, Delta);

```

 

2. **声明委托实例**:在您的类中,声明一个具体的委托实例。这将成为您在需要时绑定和触发事件的实例。例如:

 

```cpp

UCLASS()

class YourClass : public UObject

{

    GENERATED_BODY()

 

public:

    FOnHealthChanged OnHealthChangedDelegate;

};

```

 

3. **绑定函数到委托**:在适当的时候,您可以将函数绑定到委托实例上。这些函数将在委托被触发时执行。(在上面的例子中,我们没有绑定函数,而是绑定了一个事件,下面会有绑定函数的例子)例如:

 

```cpp

OnHealthChangedDelegate.AddDynamic(this, &YourClass::HealthChangedFunction);

```

 

4. **触发委托**:在适当的时候,调用委托实例的触发函数,以执行已绑定的函数。例如:

 

```cpp

OnHealthChangedDelegate.Broadcast(InstigatorActor, OwningComp, NewHealth, Delta);

```

 

在上述步骤中,`HealthChangedFunction` 是您要执行的函数,而 `InstigatorActor`、`OwningComp`、`NewHealth` 和 `Delta` 是函数的参数。

 

通过这些步骤,您可以使用声明的委托类型来实现事件触发、回调和监听机制,以响应特定的事件情况。请注意,这只是一个基本的使用示例,具体的实现可能会根据您的需求而有所不同。

 

我们现在想给血量Text的UI做个动画。

在设计界面可以方便地给某个Widget添加动画的track,在track中可以选择某个时间点,然后插入该时间点的Widget。例如在0时刻有Text的默认形态,我们在1时刻插入了Text的放大形态(修改Transform的Scale),然后动画系统会进行类似插值的操作,最终我们就会看到从0时刻到1时刻Text逐渐变大的动画效果。

然后我们可以非常方便地在蓝图中用PlayAnimation调用这个动画,


TomLooman_ActionRoguelike_第七章UMG和Player属性的评论 (共 条)

分享到微博请遵守国家法律