TomLooman_ActionRoguelike_第三章玩法与物理碰撞

2023-08-09 18:40:23 来源:哔哩哔哩

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


(相关资料图)

教程参考:/tomlooman/ActionRoguelike

基于的项目实现:/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_07_25

输入和旋转,魔法攻击

旋转的部分内容在2023_07_24中提及。

如上设置,在编辑器内启动游戏后不需要先点击一下鼠标才能操作。

如上设置,每次打开编辑器后不再是默认的level,而是上次关闭前的level。

创建SMagicProjectile类

头文件与Scharacter类类似,但SMagic类继承自Actor而不是Character,这也意味着我们不能控制它(只有Pawn以及Pawn的子类例如Character才能控制)。

且注意#include ""要最后引入。

与ASMagic类相似,类名前有ACTIONROGUELIKE_API宏,将这个Sprojectile类从ACTIONROGUELIKE中暴露出去,能被其他模块调用。

因为是游戏性类,且能在世界中生成,所以类名有前缀A。

同样有GENERATED_BODY()生成代码模板,减少我们的工作量。

与ASCharacter一样有BeginPlay和Tick,但没有SetupPlayerInputComponent,因为ASMagicProjectile只是Actor,而不是Pawn,所以无法被控制,只能按照某种规则运行。

ASMagicProjectile头文件中新增的三个Component。

USphereComponent和SCharacter里使用的CapsuleComponent类似,都是处理运动与碰撞的,但是形状不同。

但是在ASCHaracter中看不到显式的碰撞组件成员,可能是在GENERATED_BODY()中生成的。

在源文件中我们对碰撞组件进行了默认实例化(CreateDefaultSubobject<组件所属的类名>("组件在UE编辑器中显示的名字")),并设置了我们自定义的碰撞(后续有相关内容),还将其设置为该Actor的RootComponent。

UProjectileMovementComponent(抛体组件)能够实现抛体运动,反弹效果,抛物曲线等功能,它继承自UMovementComponent。

它会在每个tick更新另一个组件的位置(这里可能就是碰撞组件)。如果被更新的组件开启了模拟物理,则只有非零的初始速度(方向向量和大小)对后续轨迹有影响,因为在初始时刻后,后续运动由物理模拟接管。

在源文件中我们对其进行默认实例化。

InitialSpeed设置初始速度。

bRotationFollowsVelocity为true,则抛体组件的rotation在每个frame随着速度方向更新。(bool变量前缀为b)

bInitialVelocityInLocalSpace为true,则抛体组件的初始速度向量是相对于局部空间的,而不是相对于世界空间的。若该bool变量为false,则Projectile始终向坐标系内的一个方向运动,不考虑我们的Character和Controller。

UParticleSystemComponent负责CPU端的粒子数据和更新逻辑,是一个SceneComponent,带有位置信息,这意味着这个Component可以被挂到任意Actor身上。

这里我们只简单地进行默认实例化,并attach到碰撞组件上。

对UParticleSystemComponent类型的对象,我们可以在蓝图子类中设置其粒子效果。

在中的void ASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)内添加

,并在ProjectSetting里添加。

实现将一个外部输入绑定到一个动作,再将一个动作绑定到一个成员函数。

BindAction()和BindAxis()的区别总结如下:

执行时机:BindAction() 绑定的函数会在输入事件发生时执行,如按键按下或抬起时执行一次;BindAxis() 绑定的函数每帧都会执行;

作用原理:BindAction() 用来监听外设是否到达某个状态,如某个按键被按下或者抬起;BindAxis() 监听的是外设状态的变化量;

函数参数:BindAction() 绑定的函数无参数;BindAxis() 绑定的函数有一个参数,该参数就是外设状态的变化量;

对按键的处理:当按键被按下或者抬起时,BindAction() 绑定的函数就会被触发;BindAxis() 绑定的函数会每帧都执行,但在按键被按下时收到的参数是 Scale(Axis Mapping 时设定的系数),在抬起时收到的参数是0。

ActionBind的函数接口和我们的调用如下

template<class UserClass> FInputActionBinding& BindAction( const FName ActionName, const EInputEvent KeyEvent, UserClass* Object, typename FInputActionHandlerSignature::TMethodPtr< UserClass > Func )

调用为

PlayerInputComponent->BindAction("PrimaryAttack", IE_Pressed, this, &ASCharacter::PrimaryAttack);

ActionName是我们在ProjectSetting设置的动作名称("PrimaryAttack"),KeyEvent是与该动作绑定的事件(例如按下、松开、双击)(IE_Pressed是枚举类型变量EInputEvent中的一个枚举成员的key,如下图),Object是函数作用的对象?(这里涉及到UE的委托机制),Func是与事件绑定的函数的指针(我们自定义的成员函数ASCharacter::PrimaryAttack)。

对ASCharacter::PrimaryAttack我们声明与实现如下,

public:

从后往前解释。我们实际上要生成一个Projectile然后才能把它发射出去,在世界中生成Actor使用GetWorld()->SpawnActor<类的类型>。

SpawnActor的函数接口如下,

template< class T > T* SpawnActor(UClass* Class, FTransform const& Transform,const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters())

SpawnActor用于从给定的参数Transform和SpawnParameters生成Class类的Actor。

在调用SpawnActor时我们要传入要spawn的对象的类名,这里我们先在头文件中声明一个变量,然后在蓝图类中将该变量绑定到我们的ASMagicProjectile类。

UPROPERTY(EditAnywhere)这个说明符使该属性公开给编辑器并且可编辑。

TSubclassOf<类的类名>是提供UClass类型安全性的模板类。模板类告知编辑器的属性窗口,只显示派生自<>内类的类(在这里就是指显式派生自AActor的类)。同时,这个参数在代码中也只接受派生自<>内类的类。

这样一来,上面SpawnActor函数在世界中Spawn的都是AAactor的子类。

FVector,FTransform,FActorSpawnParameters等F开头的类名说明它们是纯C++类,可以用new来产生新对象。

另外前缀为U的可以用NewObject函数来产生对象,前缀为A可以用SpawnActor函数来产生对象(就是上面的GetWorld()->SpawnActor<AActor>)。

FTransform是个结构体,包含一个 Translation 矢量,一个 Rotation 四元数,和一个Scale3D 矢量(分别对应位置translation,旋转rotation和缩放scale),可以用以下代码创建一个FTransform对象。

FRotator是上面提过的旋转类,是个三维向量结构体,封装roll,yaw,pitch。FVector存储三个T泛型的变量。

我们的代码中只用了Rotation和Location初始化FTransform。

GetControlRotation()之前提过,返回Pawn的Controller的Rotation。GetActorLocation()返回Actor(这里是我们的Character)的Location。

FActorSpawnParameters是传递给SpawnActor的可选参数类的类型。

其中,FactorSpawnParameters::SpawnCollisionHandlingOverride是控制生成点(Spawn的位置)冲突的变量。

ESpawnActorCollisionHandlingMethod中定义了解决Spawn生成点冲突的可用策略,其中的AlwaysSpawn表示Actor总会Spawn在我们想要的位置,忽视Collision。

按照以下代码,我们的ASMagicProjectile类的Actor会Spawn在Character的碰撞体的中心,但我们实际上想要让Actor生成在Character的手上,因此做出修改。

唯一的不同是我们把GetActorLocation()换成RightHandLocation(),两者都是Fvector。

GetMesh()返回指向MeshComponent的指针,GetSocketLocation(Socket名称)得到指定名称的Socket的位置。

添加Socket

首先,我们的Character有一个Skeleton(骨架),这个Skeleton是由许多bones(骨头)组成的,我们可以在某块bones上添加Socket。

双击Mesh组件的SkeletalMesh后,选择Skeleton

左侧显式Character的bones和sockets,可以看到两者的标志不同。这里角色的右手上已经有了一个名为“Muzzle_01”的socket,所以我们可以在C++中直接调用。因此,我们获得RightHandLocation时传入GetSocketLocation的参数是"Muzzle_01"(编辑器中的目标socket的名字)。

现在我们能正确地从ASCharacter实例的手部Spawn出ASMagicProjectile实例,但是Spawn出的Projectile会穿过我们放置在世界中的StaticMesh物体,不会发生碰撞,所以我们需要对ASMagicProjectile类的collision做出修改。

我们有三种方法修改Collision。

1、可以在蓝图子类中选择碰撞组件然后修改Collision。

2、可以在ASMagicProjectile的构造函数中修改碰撞组件的属性

this->SphereComp->SetCollisionObjectType(ECC_WorldDynamic); this->SphereComp->SetCollisionResponseToAllChannels(ECR_Ignore); this->SphereComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);

SetCollisionObjectType设置该组件自身的碰撞属性(这里设置为ECC_WorldDynamic),SetCollisionResponseToAllChannels

设置该组件对所有通道的碰撞属性(这里设置为全部忽略ECR_Ignore),SetCollisionResponseToChannel

设置该组件对某个通道的碰撞属性(这里将该组件对ECC_Pawn的碰撞属性设置为ECR_Overlap)。

3、可以直接在ProjectSetting里创建一个Collision的Profile,然后在具体组件的Collision中应用(该方法可在多个组件中复用)

要应用上面创建的Profile,在C++中和在蓝图子类中都可以。但要注意的,C++中的设置类似于默认项,所以如果又在蓝图子类进行了修改,那么C++和蓝图子类中的Collision属性可能不同。

this->SphereComp->SetCollisionProfileName("Projectile");

需要注意的是,只有发生碰撞的双方都认为对方是Block的对象,才会发生Block(需要双向奔赴)。例如,在A的Collision属性中B是要被Block的,但在B的Collision属性中A是被Ignore的,那么A和B实际不会发生碰撞,只有在B的Collision属性中A也是被Block的,A与B之间的碰撞才会发生。

且最终结果会是双方Collision属性中更轻的那个,例如block和overlap则是overlap,overlap和ignore则是ignore。如下图,player会被wall阻挡,因为二者对对方的预设都是block。但不会被shrub阻挡:虽然player对worldStatic的预设是block,但shrub对pawn的预设是overlap,所以取了较轻的overlap判定。

UE中的Collision属性包括ignore、overlap和block。Block下会发生阻挡(两人见面时停下),ignore和overlap下都不会阻挡(两人擦肩而过)。Ignore和overlap的区别在于,overlap会出发overlap事件(两人都知道与对方擦肩而过),ignore则不会触发时间(两人根本没看见对方)。

实现ASMagicProjectile与WorldStatic碰撞效果后(否则魔法飞弹会直接穿过白色方块)

SimulatePhysics

若为true,则该对象会模拟物理行为,比如被推动、从高处落下、抛物运动等,同时对象Transform的Mobility属性应设置为movable。若为false,则该对象会固定在原地或按指示移动。

要实现物理模拟,对SkeletalMeshComponent,需要设置物理资产。对StaticMeshComponent,需要设置碰撞。

Actor的Mobility属性主要应用于StaticMesh和Light,包括三种状态Static、Stationary和Movable。

Mobility为Static的Actor无法进行任何的移动或改变。

Mobility为Stationary的Actor可以改变,但不能移动。

Mobility为Movable的Actor可以进行任何的添加、移动和改变。

2023_07_28

Assignment 1:爆炸桶,跳跃

首先创建C++类。因为爆炸桶不需要被Control,所以不需要继承Pawn及其子类,继承Actor就足够。以下是头文件中声明的爆炸桶组件的成员变量,以及源文件构造函数中对成员变量的实例化和设置。

这里声明组件的方式与之前有所不同,比较在Scharacter中声明摄像机组件,与这里声明网格组件,

两者都是指针,但声明CameraComp时用的是C++的原始指针,声明MeshComp时用的是TObjectPtr类。TObjectPtr类模板可用于替换原始指针,用法与其它类模板相同,为TObjectPtr<类名>。

在实例化MeshComp时,除了默认实例化和将其作为RootComponent,我们还开启了MeshComp的物理模拟。

需要注意的是,我们在蓝图中对物理模拟打钩后,Component的CollisionPresets会自动变成PhysicsActor。但是当我们在C++中执行SetSimulatePhysics(true)后,Component的CollisionPresets的变化需要显式地进行,否则不会变化。在这里会保持WorldDynamic,因为我们的SMagicProjectile对WorldDynamic的Collision是Overlap的,所以两者无法发生正确的碰撞。

ForceComp所属的URadialForceComponent类(径向力组件)会向径向方向对Physics和Destructible类别的Object施加力。

在实例化ForceComp时,我们设置了该径向力的范围和强度。

其中有一个bImpulseVelChangeBool类型变量,若为true,则该组件发出的径向力忽视Object的重量,否则Object重量越重,径向力对Object的影响越小。

ForceComp->AddCollisionChannelToAffect(ECC_WorldDynamic)和SMagicProjectile中使用过的SphereComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap)类似,都是对某个通道设置某种碰撞属性。

可通过ForceComp->SetAutoActivate()函数设置该组件的径向力是否自动开启,若为true,则径向力从该组件所依附的对象被创造时就存在(但是试验中没有效果)。因为我们需要用子弹激活这个径向力来模拟爆炸效果,所以设置为false。

在头文件中我们重载了Actor类的PostInitializeComponents函数,该函数是实例化Actor时的一环,

我们在源文件中对该函数的定义如下

Super::PostInitializeComponents();执行父类(Actor类)的同名函数。

OnComponentHit组件将会在击中或被击中时触发。

这里的AddDynamic涉及UE的委托机制,作用大致是将SexplosiveBarrel对象与函数ASExplosiveBarrel::OnActorHit绑定,当触发一些事件时激活(运行)函数ASExplosiveBarrel::OnActorHit。   

BindAction进行两个绑定:将外部输入绑定到事件(ProjectSetting中完成),将事件绑定到函数(C++,SetupPlayerInputComponent成员函数中完成)。

因为ACharacter自带跳跃函数,所以实现较为简单。

如果把IE_Pressed换成IE_Release,角色就会在松开空格后起跳,而不是在按下空格时起跳(可以实现类似蓄力跳的效果)。

关键词:

推荐内容