上一章的内容都还是基础知识,游戏逻辑较为简单,语法也比价简单,主要的目的还是基础,所以没有过多的记录。
这一章会构建一个小游戏(关于关卡构建的部分这里省略掉了,是一个简单的房屋构建和光照布置,这些内容在之前的博客中有所介绍),所以会尽量记录有关c++的内容。
这一章的一章的涉及内容有:
关于输入绑定和关卡设计相关的在前面的文章中都有所提到。
正文:
我们重新创建一个不含初学者包的C++项目。
当引擎能显示界面时,创建成功了
我们可以在项目列表中,找到Source-Escape-Escape.cpp
先介绍下指针:
Pointers are memory address.
pointer syntax(指针语法):
上面的三种方式都可以,我们举例子来说明,假设我们有:
AActor* SomeActor;
AActor class 有一个方法GetName(),后面会用到。
我们可以用下面的方式使用(基础):
SomeActor->GetName();
关于inheritance(继承)
在虚幻中:
举例:Character “is a” Pawn,Pawn “is an” Actor
相同的例子:Dog “is a” Mammal,Mamm “is an” Animal.
关于Components(组件):
组件非常适合共享共同的行为或特性。Actor可以拥有自定义组件(上一章中有提到)。
那怎样创建一个World position component。
在那之前,我们可以进入类查看器:
在里面搜索pawn,可以看到:
回到组价,我们可以随意拖入一个球体,然后选中它,添加组件,新建c++组件:
对其进行命名并且创建,我们可以在vs code的目录结构中看到:
可以注意到在文件的最上方有一句:
// Fill out your copyright notice in the Description page of Project Settings.
我们可以在项目设置中,对版权声明进行修改。
放在这部分的代码会在每帧执行:
void UWorldPosition::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// ...
}
UE_LOG(Category, Verbosity, TEXT("Message"));
UE_LOG(LogTemp, Warning, TEXT("Hello!"));
我们可以在vs code中,在BeginPlay输入:
UE_LOG(LogTemp, Warning, TEXT("This is a warning"));
然后我们可以编译,并打开输出日志,运行游戏,就可以看到(输出两次是因为我这里有两个object):
我们可以先查看官方文档:
https://docs.unrealengine.com/4.27/en-US/API/Runtime/Core/Math/FVector/https://docs.unrealengine.com/4.27/en-US/API/Runtime/Core/Math/FVector/定义是:A vector in 3-D space composed of components (X, Y, Z) with floating point precision.
我们也可以在里面找到此类的函数(如果我们需要的话):
一个简单的方式获取位置信息:
FString ObjectName = GetOwner()->GetName();
FString ObjectPosition = GetOwner()->GetActorLocation().ToString();
UE_LOG(LogTemp, Warning,TEXT("%s Location in world is : %s"),*ObjectName,*ObjectPosition);
关于这一阶段的问题:
1 ObjectName is an FString. Why do we have to use *ObjectName in our UE_LOG rather than just ObjectName?
UE_LOG is expecting a TCHAR array,and the *effectively converts the string to this type.
关于如何用蓝图使门进行碰撞检测,并开启的部分,在前面的文章中有介绍过。
我们首先为门,创建一个新的c++组件,需要注意的是,我们需要将门修改为可移动类型。因为如果一直是静态对象,是不能在游戏中修改的。
我们可以用下面的句子获取位置信息:
GetOwner()->GetActorRotation()
要使用GetOwner,别忘了加上:
#include "GameFramework/Actor.h"
但是这个会给我们一个FRotator,这样我们需要查找一下文档。
这样我们就能获取信息,但是我们想要做的是设置旋转,这就需要SetActorRotation()。
//FRotator CurrentRotation = GetOwner()->GetActorRotation();
//CurrentRotation.Yaw = 90.f;
FRotator OpenDoor ={0.f,90.f,0.f};
GetOwner()->SetActorRotation(OpenDoor );
我们进入游戏,门就被正常打开了
不过这样只会在我们进入游戏时,将门打开,不能在游戏中看到门打开的过程。
我们不将代码放在BeginPlay中,这次放在TickComponent中。
我们首先将最终的目标Yaw值(90度)定义成私有数据TargetYaw,然后获得当前的旋转值,和上面一样:
float CurrentYaw = GetOwner()->GetActorRotation().Yaw;
然后定义一个OpenDoor(这里的定义会在下面遭到修改):
FRotator OpenDoor(0.f,TargetYaw,0.f);
我们使用Fmath::Lerp来修改OpenDoor:
OpenDoor.Yaw = FMath::Lerp(CurrentYaw, TargetYaw,0.02f);
它的用法,在官方文档中有:
但是我们这里用的线性插值,它有一些问题。这里的门角度会一直接近90度,但是不会到达90度。而且和电脑性能有关的,如果你的电脑性能足够好,可以在一秒内跑很多帧,那么门关上的速度会更快。
那怎样获得理想的关门效果?
我们使用Fmath::FInterpConstantTo4
OpenDoor.Yaw = FMath::FInterpConstantTo(CurrentYaw, TargetYaw,DeltaTime,45);
要实现这个,我们要取消之前对TargetYaw的赋值,重新定义:
float InitialYaw;
float CurrentYaw;
float TargetYaw;
然后我们对BeginPlay函数和TickComponent函数进行简单的修改:
void UOpenDoor::BeginPlay()
{
Super::BeginPlay();
InitialYaw = GetOwner()->GetActorRotation().Yaw;
CurrentYaw = InitialYaw;
TargetYaw = InitialYaw + 90.f;
}
void UOpenDoor::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
CurrentYaw = FMath::Lerp(CurrentYaw,TargetYaw,DeltaTime * 1.f);
FRotator DoorRotator = GetOwner()->GetActorRotation();
DoorRotator.Yaw = CurrentYaw;
GetOwner()->SetActorRotation(DoorRotator);
}
我们可以从虚幻引擎中,复制一个相同的门,然后编译c++代码。
在游戏开始时,就可以看到两个门同时缓慢打开了。
但是有个问题是,我们每次要修改旋转值,还要在代码中修改。而且不能对单个门的旋转角度进行修改。我们需要对这个问题改进。
有个很简单的方法,我们在TargetYaw定义的地方输入:
UPROPERTY(EditAnywhere, Category = "Damage")
这样在编译后,我们选择门的OpenDoor组件,可以看到:
这样我们可以对每个门进行设置,并将代码改为:
TargetYaw += InitialYaw;
设置一个90度,一个为50度:
我们对开门这一动作进一步细化,我们希望玩家在达成一定条件后,门才能开启。
我们将使用触发体积,Trigger volume来实现。首先先在引擎中创建一个触发体积,然后我们回到VS code中。
我们加入新的头文件,并保证OpenDoor在最下方:
#include "Engine/TriggerVolume.h"
然后添加
UPROPERTY(EditAnywhere, Category = "Trigger")
ATriggerVolume* PressurePlate;
这样我们可以在选中组件,设置PressurePlate为我们刚刚在虚幻引擎中创建的触发体积。
然后我们需要保证,当玩家进入触发体积时,有对应操作。
我们还需要创建一个:
UPROPERTY(EditAnywhere, Category = "Open")
AActor* ActorThatOpen;
我们需要为其分配,但是DefaultPawn只在运行的时候出现,我们没办法在未运行时选中。
所以我们需要先运行,然后弹出,再选中对应组件(为了进行测试):
接着我们回到代码中,并对代码进行重构。我们需要创建一个新的函数OpenDoor,并将之前的部分操作移动到新的函数中。
void OpenDoor(float DeltaTime);
然后我们加入判断:
if (PressurePlate->IsOverlappingActor(ActorThatOpen))
{
OpenDoor(DeltaTime);
}
然后编译,进行测试。用上面的操作添加DefaultPawn,然后走进触发体积,门是可以打开的。
但是我们在游戏中不能按照这个方式来开门关门,所以我们要进行修改。
首先在进入游戏BeginPlay时,加入判断,防止出现PressurePlate未被分配的情况。
if(!PressurePlate)
{
//如果,没有在选项中分配PressurePlate
UE_LOG(LogTemp, Error, TEXT("%s Has the OpenDoor component on it , but no pressureplate set"),*GetOwner()->GetName());
}
其次,我们要加入两个新的头文件:
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
关于FirstPlayerController,如果我们的游戏未设置为本地多人游戏,则每个客户端上将只有一个 PlayerController。
我们在BeginPlay的判断下,加入:
ActorThatOpen = GetWorld()->GetFirstPlayerController()->GetPawn();
现在我们就可以正常和触发体积互动,开门了。
至于关门,就很简单了,这里就不多赘述。可以自己尝试。
我们对功能进行扩充,如果玩家在一定时间内没有关闭们,我们就自动关门。我们可以使用GetTimeSecond。
它会返回:time in seconds since world was brought up for play
重新定义两个变量:
float DoorLastOpen = 0.f;
float DoorCloseDelay = 2.f;
然后加入新的判断:
if (PressurePlate && PressurePlate->IsOverlappingActor(ActorThatOpen))
{
OpenDoor(DeltaTime);
DoorLastOpen = GetWorld()->GetTimeSeconds();
}
else
{
if (GetWorld()->GetTimeSeconds() - DoorLastOpen > DoorCloseDelay)
{
CloseDoor(DeltaTime);
}
}
由于篇幅过多,本章将分成两部分。剩余的内容在下一部分继续介绍。