第二次引擎编程尝试

ronald4个月前职场1390

在这一节,我们尝试在游戏中创建一个物品并可以被角色拾取。

在游戏世界里面创建一个物品

创建物品对象

image-20220215095218512.pngimage-20220215095250889.pngimage-20220215095354687.png

    在创建对象的时候,需要我们选择对象的父类,选项卡中的父类对象他们各自有自己的适用范围,这里挑几个常用的进行说明:

None

    顾名思义,None表示此C++对象为纯自己定义的类,所有逻辑和与引擎的交互由开发人员自己实现。

Actor

    从前面来看,Actor是所有可以放置在游戏世界内的对象的类的基类。

Pawn

    Pawn是指所有由玩家或者AI控制的Actor的基类,是玩家或AI在游戏世界内的具体化的展现;默认的Pawn包含了DefaultPawnMovementComponentCollisionComponentStaticMeshComponent,分别控制Pawn对象的移动、碰撞;

其中DefaultPawnMovementComponent是对MovementComponent能力的扩充,他被设置为无重力可飞行的移动风格,还包括MaxSpeedaccelerationdeceleration等。

Character

    CharacterPawn能力的扩充,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

    PlayerControllerPawn和控制他玩家的接口,是游戏玩家和游戏内实体交互的渠道,通常输入处理或者其他功能放到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++的接口,也可以是另外一个蓝图。

    而我们拖拽蓝图实例到游戏场景中,可以理解是创建一个蓝图实例的操作,这个蓝图实例里面又实际创建一个类对象实例。

image-20220215095728297.pngimage-20220215095754336.pngimage-20220216111836421.png

让放置的对象可以被看见

    将蓝图拖拽到游戏场景之后,运行游戏可以看到,对象并不能被看到,我们需要添加一些代码,让创建的游戏对象可以在游戏世界被展现

首先,我们需要添加一些代码

#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的值,如下:

image-20220216114948269.png    设置对象的位置,并设置Static Mesh的材质信息image-20220216115044833.png    这样点击运行,这个对象在游戏里面就能看到了

image-20220216115226875.png

让对象“闪闪发光”

    前面内容中,我们已经在游戏场景内放置了我们自己创建的对象,现在我们希望给这个对象更加炫酷一点,所以这里我们给这个对象加点特效,前面文章中,我们已经展示了如何添加特效,这里直接展示我们添加代码后的结果:

#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);
}

    编译代码,并在编辑器内编辑:

image-20220216141738829.pngimage-20220216141758220.png

    设置好特效后,运行后可以看到

image-20220216142131617.png

让物品可以被玩家拾取

物品被玩家拾取分为两步:

    第一步是物品从游戏场景内消失;

    第二步是修改玩家数据。

    而行为模式,我们采取的机制是,玩家走进物品的时候自动拾取,我们分步进行阐述:

物品在合适的时机从场景内消失

    我们的行为模式是,玩家站到物品位置的时候将物品从场景内移除,但是默认情况下由于开启了碰撞,所以玩家实际是无法站到物品位置的,所以我们可以通过接口关闭碰撞,如下:

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 channelobject 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

image-20220216164421957.png    打开蓝图的编辑面板有:

image-20220216164542689.png

    我们可以这么理解这个蓝图的逻辑

    · 入口是Event Tick,是个定时执行的接口,他会在Delta Second执行,调用对应的接口

    · 空白位置右击就可以创建一个节点,这个节点可以是Print String,也可以是Append,这里是Print StringEvent Tick调用

    · 这个PrintString的输入是一个字符串类型的变量In

    · 这个字符串类型的变量又是Append接口的返回值Return Value

    · 而Append接口有两个参数AB,其中A我们填写了一个默认值carry,而B为IsCarryObject的返回值

    最终实现效果如下:

image-20220216163519544.png

参考资料

    Collision Filtering (unrealengine.com)

    https://zhuanlan.zhihu.com/p/69164560

    https://zhuanlan.zhihu.com/p/427716054


相关文章

LUA数据结构(三)

Lua数据结构userdata    Lua官方的介绍:userdata是一种用户自定义数据,用于表示一种由应用程序或者C/C++语言库创建的类型,可以将任意C/C++类型的数据(通常是struct、指针)存储到Lua变量中调用。    在实际应用过程中,C/C++接口调用LuaL_newuserdata就会分配指定大...

协程-无栈协程(下)

无栈协程库——protothread    ProtoThread源码如下所示:#define LC_INIT(s) s = 0; #define LC_RESUME(s) switch(s) { case 0: #define LC_SET(s)...

聊一聊面试(一)

聊一聊面试(一)

  前些天一些叔伯辈的小孩大学要毕业了,可能感觉计算机行业赚的有点多,一些不怕加班的也想进入这个行业,于是就辗转找到我,希望帮忙做一些这个方面的面试辅导;而我趁着这个机会,对过去的一些思考进行沉淀,分享出来和大家交流一下。    在我看来,面试主要考察能力项包括:专业能力、沟通能力、思维能力、学习能力、思考能力和抗压能力五大方面;投射到实...

Lua和so

Lua和so

    实际在应用开发过程中,会用到很多第三方的库;Lua由于其易嵌入的特性,不仅可以使用Lua编写的库,也可以将C++的库进行二次封装供Lua调用。这里我们以实现在Lua中解析xml文件格式场景,结合C++编写的“tinyxml2” 这个库为例进行讲解:库的准备及编译第一步,下载“tinyxml2”的源码下载链接:leethomason/tinyxml2:...

第四次引擎编程尝试

第四次引擎编程尝试

本节我们将尝试在游戏中创建AI,并针根据游戏内的事件做出对应的反应。创建自己的AI类· 按照介绍的,先创建一个我们的AI类,因为AI可以移动、碰撞等,所以我们选择其继承自Character· 创建好AI类之后,我们创建其蓝图类· 创建好蓝图我们进入AI的编辑界面从界面中,我们可以看到:    我们的FPSTpl2AIGuard继承自Character,拥有A...

Lua的Upvalue和闭包(二)

Lua的Upvalue和闭包(二)

Lua闭包和Upvalue的实现    前面文章介绍了Lua闭包和upvalue的概念,本文简单过一下Lua对于闭包和upvalue的实现以加深理解。Lua闭包结构    Lua在内存的结构如下所示:#define ClosureHeader \ CommonHeader; lu_by...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。