第二次引擎编程尝试
在这一节,我们尝试在游戏中创建一个物品并可以被角色拾取。
在游戏世界里面创建一个物品
创建物品对象
在创建对象的时候,需要我们选择对象的父类,选项卡中的父类对象他们各自有自己的适用范围,这里挑几个常用的进行说明:
None
顾名思义,None表示此C++对象为纯自己定义的类,所有逻辑和与引擎的交互由开发人员自己实现。
Actor
从前面来看,Actor
是所有可以放置在游戏世界内的对象的类的基类。
Pawn
Pawn
是指所有由玩家或者AI控制的Actor的基类,是玩家或AI在游戏世界内的具体化的展现;默认的Pawn
包含了DefaultPawnMovementComponent
、CollisionComponent
、StaticMeshComponent
,分别控制Pawn
对象的移动、碰撞;
其中DefaultPawnMovementComponent
是对MovementComponent
能力的扩充,他被设置为无重力可飞行的移动风格,还包括MaxSpeed
、acceleration
、deceleration
等。
Character
Character
是Pawn
能力的扩充,Character
用于代表垂直站立的玩家(可以理解为一种特殊的Pawn
),可以在场景中行走、跑、跳等;从实现上看,他还包括了:
(1)胶囊体组件CapsuleComponent
,用于运动碰撞检测
(2)角色移动组件CharacterMovementComponent
,用于对象移动,并扩展了重力、摩擦力、速度等的能力
(3)骨骼网格体组件SkeletalMeshComponent
,用于骨骼高级动画、以及将骨架网格体添加到Character
子类对象中
总结:Actor
->Pawn
->Character
是一个父类->子类的继承关系,功能也越来越复杂:表示任意一个可以放置在游戏世界的对象->表示一个游戏世界内移动的对象->表示一个游戏世界内的角色;结果上来看:Actor可以表示游戏内的物品->Pawn表示一个游戏世界内NPC->Character表示游戏世界内的角色
Controller
控制器是一种可以控制Pawn
的非实体Actor
,可以理解Controller
也是一种Actor
,但是需要需要和Pawn
对象(或者其子类Character
对象)绑定,达到控制Pawn
及其子类对象动作的目的。
PlayerController
PlayerController
是Pawn
和控制他玩家的接口,是游戏玩家和游戏内实体交互的渠道,通常输入处理或者其他功能放到PlayerController
中,游戏场景中Pawn
可能是临时存在的,但是Player Controller
则是在游戏过程中一直存在。
Game Mode & Game State
GameMode
用于描述游戏规则,这些规则包括:参与游戏人数、是否可以被暂停、关卡之间的切换等;而游戏过程中Game Mode
有需要同步给各个玩家,则通过Game State
进行存储和同步;也就是说Game Mode
仅存在于服务器,任务是定义和实现游戏规则;而Game State
存在于CS两端,用于存储游戏状态和同步其他玩家数据。
AGameModeBase
是所有Game Mode
的基类
Player State
Game State
用于启用客户端监控游戏状态,但这个状态是仅限于游戏层面属性,需要被其他玩家关注的属性;属于玩家独有,且更全面的信息
HUD
HUD
,头显,指的是游戏期间在屏幕上覆盖的状态和信息;用于告知当前游戏状态,即分数、生命值、剩余时间等;HUD
是不可互动的,即玩家只能读不能改HUD的元素
物品放入游戏场景
生成所创建对象的源码
点击确定后,我们看到生成的代码如下:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "MyActor.generated.h" UCLASS() class FPSTPL2_API AMyActor : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AMyActor(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; };
但是这只是生成了,游戏内物品类,实际游戏运行时,我们并没有创建对应的物品对象并放置到游戏场景中。
将对象放置到场景中
为了在游戏中创建我们编写的游戏对象,我们可以通过创建蓝图去继承我们定义的C++类,并将这个蓝图对象拖拽到游戏场景中实现场景中对象的创建。功能上看,蓝图是一个流程框架的描述,描述一个执行框架,走到哪一步执行什么操作,至于这个操作可以是一个C++的接口,也可以是另外一个蓝图。
而我们拖拽蓝图实例到游戏场景中,可以理解是创建一个蓝图实例的操作,这个蓝图实例里面又实际创建一个类对象实例。
让放置的对象可以被看见
将蓝图拖拽到游戏场景之后,运行游戏可以看到,对象并不能被看到,我们需要添加一些代码,让创建的游戏对象可以在游戏世界被展现
首先,我们需要添加一些代码
#include "MyActor.h" #include "Components/SphereComponent.h" #include "Components/MeshComponent.h" // Sets default values AMyActor::AMyActor() { PrimaryActorTick.bCanEverTick = true; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); RootComponent = MeshComp; } // Called when the game starts or when spawned void AMyActor::BeginPlay() { Super::BeginPlay(); } // Called every frame void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); }
我们为AMyActor
类添加了UStaticMeshComponent
类型组件MeshComp
,并在蓝图中编辑这个AMyActor
对象并设置MeshComp
的值,如下:
设置对象的位置,并设置Static Mesh
的材质信息这样点击运行,这个对象在游戏里面就能看到了
让对象“闪闪发光”
前面内容中,我们已经在游戏场景内放置了我们自己创建的对象,现在我们希望给这个对象更加炫酷一点,所以这里我们给这个对象加点特效,前面文章中,我们已经展示了如何添加特效,这里直接展示我们添加代码后的结果:
#include "MyActor.h" #include "Components/SphereComponent.h" #include "Components/MeshComponent.h" #include "GameFramework/ProjectileMovementComponent.h" #include "Kismet/GameplayStatics.h" // Sets default values AMyActor::AMyActor() { PrimaryActorTick.bCanEverTick = true; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); RootComponent = MeshComp; } // Called when the game starts or when spawned void AMyActor::BeginPlay() { Super::BeginPlay(); UGameplayStatics::SpawnEmitterAtLocation(this,ExplosionEffect, GetActorLocation()); } // Called every frame void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); }
编译代码,并在编辑器内编辑:
设置好特效后,运行后可以看到
让物品可以被玩家拾取
物品被玩家拾取分为两步:
第一步是物品从游戏场景内消失;
第二步是修改玩家数据。
而行为模式,我们采取的机制是,玩家走进物品的时候自动拾取,我们分步进行阐述:
物品在合适的时机从场景内消失
我们的行为模式是,玩家站到物品位置的时候将物品从场景内移除,但是默认情况下由于开启了碰撞,所以玩家实际是无法站到物品位置的,所以我们可以通过接口关闭碰撞,如下:
AMyActor::AMyActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); RootComponent = MeshComp; MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
通过SetCollisionEnabled
将物品的碰撞关闭,这样玩家可以直接站到物品位置处,但是仅站到物品处是不够的,物品需要感知到玩家站上去了,并对对应的事件进行处理,如下:
// Sets default values AMyActor::AMyActor() { PrimaryActorTick.bCanEverTick = true; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); RootComponent = MeshComp; MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly); MeshComp->SetCollisionResponseToAllChannels(ECR_Ignore); MeshComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); } void AMyActor::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, GetActorLocation()); Destroy(); }
在解释这段之前,我们需要先了解UE4的碰撞检测机制。
UE4的碰撞检测
在UE4中,物体之间的碰撞分为三种:
(1)Block,即物体所在区域是物体独占,不允许其他对象占用;当其他对象在移动到物体所占范围时,是会被阻挡住
(2)Overlap,即物体所在区域非物体独占,其他对象在位置上可以与此物体占用相同位置,但是会触发事件
(3)Ignore,即位置非物体独占,且出现位置重叠也不会有事件触发
针对前面的前两种碰撞:Block和Overlap,会分别会触发Hit事件和Overlap事件,既然有事件抛出来就得有关注这个事件的对象要处理这个事件,UE4采取的是channel
机制,这里分为trace channel
和object channel
,从应用上看,二者区别在于:
(1)trace channel:主要用于射线类的碰撞检测
(2)object channel:主要用于对象移动的碰撞检测
而对于每个Actor
对象,其实都有带有自己的channel
,去关注对应事件的处理,UE4提供接口SetCollisionResponseToChannel
去注册关注的事件,并提供了一系列的回调去处理对应事件发生的处理逻辑
本例中的实现
在本例中,我们希望角色走到物体旁边就拾取了这个物品,即触发了overlap
事件,并进行处理:
(1)SetCollisionEnabled(ECollisionEnabled::QueryOnly)
,首先设置碰撞类型为仅支持查询,即不阻挡玩家但是会触发事件
(2)SetCollisionResponseToAllChannels(ECR_Ignore);
(3)SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
①触发事件之后,事件会丢给各个channel
进行处理,这里的含义即将自己的channel
注册到自己关注的事件上
②然后重写基类的回调NotifyActorBeginOverlap
进行处理
修改玩家数据
在这一步,当我们让游戏场景内物品消失之后,我们希望玩家身上的数据状态发生改变,并且通过屏幕内打印的方式展示这种转变,分为两步来看:
修改玩家的数据状态
修改玩家状态分为两步,首先是在角色类中添加字段记录对应的状态,其次是在对应的接口里面修改相关的状态,如下:
void AMyActor::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, GetActorLocation()); AFPSTpl2Character* MyCharacter = Cast<AFPSTpl2Character>(OtherActor); if (MyCharacter) { MyCharacter->bIsCarryObject = true; } Destroy(); }
在这里,当AMyActor
对象被overlap
的时候,会调用被overlap
对象的NotifyActorBeginOverlap
接口,这里面记录了发生碰撞的对象OtherActor
,在这里首先对OtherActor
进行类型转换Cast
,然后进行对应字段的设置bIsCarryObject
UE的类型转换
UE的Cast
,UE通过cast
方法实现类型转换,当类型之间无法进行类型转换的时候,会返回一个nullptr
展示数据修改的结果
通过上述的逻辑,我们修改了玩家的数据,但是我们希望展示出这种数据的变化,这里采取最简单的方式,即字符串打印的形式(用蓝图进行制作):
双击角色的蓝图类FirstPersonCharacter
打开蓝图的编辑面板有:
我们可以这么理解这个蓝图的逻辑
· 入口是Event Tick
,是个定时执行的接口,他会在Delta Second
执行,调用对应的接口
· 空白位置右击就可以创建一个节点,这个节点可以是Print String
,也可以是Append
,这里是Print String
给Event Tick
调用
· 这个PrintString
的输入是一个字符串类型的变量In
· 这个字符串类型的变量又是Append
接口的返回值Return Value
· 而Append
接口有两个参数A
和B
,其中A
我们填写了一个默认值carry
,而B为IsCarryObject
的返回值
最终实现效果如下:
参考资料
Collision Filtering (unrealengine.com)
https://zhuanlan.zhihu.com/p/69164560
https://zhuanlan.zhihu.com/p/427716054