由于篇幅原因,这里继续上一章的内容。
一、增加拾取道具
这一章,我们增加一个道具的拾取,将其放在触发体积的区域内,让门能保持开启。
我们想做的事情:
- 我们在场景中,添加一个基础的网格体,如椎体或球体。我们希望玩家可以抓取这个物体。
- 我们希望为玩家添加一个组件。而且这个玩家是出现在游戏中的,在游戏开始前还没出现。
关于GameMode:
- 玩家从什么库存物品开始
- 有多少生命可用
- 玩家需要达到多少分数才能结束游戏
这些都属于GameMode,这些都是整个游戏的参数和规则。
关于GameMode的更多基础内容,可以查看官方文档的说明:
https://docs.unrealengine.com/4.27/en-US/InteractiveExperiences/Framework/GameMode/
1.1 替换默认的DefaultPawn
我们先选中DefaultPawn,然后点击蓝图:
接着我们添加一个新的Actor组件的c++类——Grabber。然后我们进入蓝图中,添加Grabber组件:
这样我们再将其拖入场景中,就可以看到:
这样再我们运行游戏时,就有一个默认的DefaultPawn和带有新组件的DefaultPawn_BluePrint,这样我们可以替换之前的默认(最后别忘了把场景中的我们拖入测试的蓝图删除)。
接着我们找到游戏的GameModeBase,并创建蓝图类。
然后我们到项目设置中的默认游戏设置中,选中我们刚刚创建的BP:
同样的,我们也把DefaultPawn修改为,DefaultPawn_BluePrint。
然后我们回到Grabber中输入代码进行测试:
UE_LOG(LogTemp,Warning,TEXT("work well"));
再编译运行,能在输出日志中看到,证明我们成功完成替换。
1.2 获得Player Viewpoint
我们可以通过查找官方文档来了解。
我们先尝试在每帧输出玩家的位置信息和旋转信息:
FVector PlayerViewPointLocation;
FRotator PlayerViewPointRotation;
GetWorld()->GetFirstPlayerController()->GetPlayerViewPoint(
OUT PlayerViewPointLocation,
OUT PlayerViewPointRotation);
UE_LOG(LogTemp,Warning,TEXT("Our playerviewpoint Location is %s , and our Rotation is %s")
,*PlayerViewPointLocation.ToString()
,*PlayerViewPointRotation.ToString());
这样编译我们就可以在输出日志看到测试的输出结果。
二、进一步获得物体信息
2.1 在虚幻中画出表现方向和长度的线
为了能拿起物体,我们除了要知道自身在物理世界里的位置和旋转信息,我们还要知道物体的信息,以及玩家到物体的距离。所以我们接下来尝试画一条线,从玩家到物体的线。
我们应该都知道向量的加法是什么:
那在虚幻中,怎么计算呢?有下面的示意图:
在弄清在游戏中Default pawn的方向和它到目标物体的距离是多少之前,我们先尝试在虚幻中画出我们想要看到的线。这需要使用DrawDebugLine(关于所需要的头文件,之后不会再)。
https://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/DrawDebugLine/
void DrawDebugLine
(
const UWorld * InWorld,
FVector const & LineStart,
FVector const & LineEnd,
FColor const & Color,
bool bPersistentLines,
float LifeTime,
uint8 DepthPriority,
float Thickness
)
我们输入对应的参数:
FVector LineTraceEnd = PlayerViewPointLocation + FVector(0.f, 0.f, 100.f);
DrawDebugLine(
GetWorld(),
PlayerViewPointLocation,
LineTraceEnd,
FColor(0, 0, 255),
false,
0.f,
0,
5.f
);
100.f是meter长,然后color是对应着RGB的颜色值,然后现在的方向是指向头顶。我们在虚幻中编译查看结果:
接下来我们对方向进行一个修正,我们要表现出角色的朝向。我们修改为:
FVector LineTraceEnd = PlayerViewPointLocation + PlayerViewPointRotation.Vector() * Reach;
现在的表现:
阶段性问题:
<1>How would a re-usable component access the transform (position, rotation, scale) of the object it is attached to?
- GetOwner()->GetTransform()
<2>How do we get the player controller?
- GetWorld()->GetFirstPlayerController()
<3>If we pause the game will GetTimeSeconds() continue to count up? Do consult the Unreal docs if you like!
- No
官方文档的说明:
Returns time in seconds since world was brought up for play, adjusted by time dilation and IS stopped when game pauses.
Target is Gameplay Statics.
<4>Why do we need to create a new Game Mode for the grabbing system?
- So that we can easily specify our modified Default Pawn.
2.2 判断距离内的物体
这里我们要用到LineTraceSingleByObjectType。
Trace a ray against the world using object types and return the first blocking hit
bool LineTraceSingleByObjectType
(
struct FHitResult & OutHit,
const FVector & Start,
const FVector & End,
const FCollisionObjectQueryParams & ObjectQueryParams,
const FCollisionQueryParams & Params
) const
我们有下面的代码:
FHitResult Hit;
//关于第一个参数,我们暂时将其设为空白的
//关于最后一个参数,我们需要忽视我们自己,因为dubug线,首先碰到的是玩家自己
FCollisionQueryParams TraceParams(FName(TEXT(" ")), false, GetOwner());
GetWorld()->LineTraceSingleByObjectType(
OUT Hit,
PlayerViewPointLocation,
LineTraceEnd,
FCollisionObjectQueryParams(ECollisionChannel::ECC_PhysicsBody),
TraceParams
);
接下来我们要进行测试,测试我们debug线碰到的物体,并将物体的名字输出出来。
AActor* ActorHit = Hit.GetActor();
if (ActorHit)
{
UE_LOG(LogTemp, Error, TEXT("The Trace object is %s"),*(ActorHit->GetName()))
}
我们编译并进行测试:
可以成功的看到输出日志中的显示。
三、 尝试对物体进行移动
3.1 PhysicsHandle
我们打开DefaultPawn蓝图,在添加组件中可以找到(这正是我们想要的),我们添加这个组件。
我们返回VS code,为了使用Physic Handle,我们仍需查阅文档。
这样我们可以定义:
UPhysicsHandleComponent* PhysicsHandle = nullptr;
接着我们想要检查Physicshandle,在游戏开始时。那这个应该怎么办?
我们可以在BeginPlay输入
PhysicsHandle = GetOwner()->FindComponentByClass<UPhysicsHandleComponent>();
并做出判断,当PhysicsHandle不存在时,输出错误。我们可以先保留Physic handle,然后再删除进行测试,看是否会输出报错。
3.2 操作映射
接下来我们添加操作映射,很简单的操作:
接下来我们添加代码,如果按键被按下,E或者是鼠标右键,都会调用Grab函数。所以这一步我们将用户输入,映射到函数调用。
InputComponent = GetOwner()->FindComponentByClass<UInputComponent>();
if (InputComponent)
{
InputComponent->BindAction("Grab",IE_Pressed,this,&UGrabber::Grab);
}
我们可以在Grab函数中测试一下,现阶段是否正确。
void UGrabber::Grab(){
UE_LOG(LogTemp, Warning, TEXT("Grabber Pressed!"));
}
我们在测试中,发现E键和移动的控制键重合了,所以我们可以删除绑定的E键,只保留鼠标右键。同样的,我们也可以测试当按键松开时的情况。
如果测试成功的话,我们可以在输出日志中,看到按键按下和松开的输出。
我们可以花时间对代码进行重构,增加可读性(这部分就不放进来了)
3.3 移动物体
我们首先做的是,如果我们触碰到了物体,我们将attach Physics handle。
在我们重构了代码后,我们将对应代码放入到了新的函数ComponentToGrab中,它会返回一个
FHitResult Hit;
我们首先判断是否触碰到物体,如果是,我们利用Physics Handle将其拿起:
FHitResult HitResult = GetFirstPhysicBodyInReach();
UPrimitiveComponent* ComponentToGrab = HitResult.GetComponent();
if (HitResult.GetActor())
PhysicsHandle->GrabComponentAtLocation
(
ComponentToGrab,
NAME_None,
LineTraceEnd
);
其次,我们在TickComponent中,如果Physics Handle已经拿起,我们使用SetTargetLocation:
if (PhysicsHandle->GrabbedComponent)
{
PhysicsHandle->SetTargetLocation(LineTraceEnd);
}
我们进入测试,按右键已经可以抓取物体了:
接下来我们要做的是放下物体,也很简单:
PhysicsHandle->ReleaseComponent();
好的!现在我们可以在场景中,抓起和放下物体了。然后我们需要对代码进行重构,重新整理代码,并删除之前的测试部分。
四、让物体和触发体积交互
4.1 物体触发交互
我们首先回到OpenDoor中,在之前我们的做法是:
if (PressurePlate && PressurePlate->IsOverlappingActor(ActorThatOpen))
现在我们要将其改为:
if (TotalMassofActors() > 50.f)
然后我们定义这个函数为:
float TotalMassofActors() const;
我们暂时先不对函数做过多操作。
float UOpenDoor::TotalMassofActors() const
{
float TotalMass = 0.f;
return TotalMass;
}
接下来我们通过判断overlapping,来对TotalMass的值进行修改。
TArray<AActor*> OverLappingActors;
PressurePlate->GetOverlappingActors(OverLappingActors);
接着我们可以在引擎中修改一些DefaultPawn的一些参数,保证玩家不能在场景中飞来飞去:
我们还需要选中物体的碰撞-生成重叠事件。物体的重量和玩家的重量也需要设置。
然后我们修改函数:
float UOpenDoor::TotalMassofActors() const
{
float TotalMass = 0.f;
TArray<AActor*> OverLappingActors;
PressurePlate->GetOverlappingActors(OverLappingActors);
for (AActor* Actor : OverLappingActors)
{
TotalMass += Actor->FindComponentByClass<UPrimitiveComponent>()->GetMass();
}
return TotalMass;
}
最后别忘了,在上面的判断处的50.f,我们还需要将其设为在引擎内可以修改的参数。
这样我们再进行测试,我们现在可以把物体放在触发体积处,保证门的开启了!
4.2 添加音效
我们可以选中门,然后添加组件——音频组件。
我们可以先导入我们想要使用的音频(可以直接拖入),然后将其分配到对应组件上。
我们可以创建函数:
void UOpenDoor::FindAudioComponent()
{
AudioComponent = GetOwner()->FindComponentByClass<UAudioComponent>();
if (!AudioComponent)
{
UE_LOG(LogTemp,Error,TEXT("Lost Audio Component"));
}
}
并在BeginPlay里:
FindAudioComponent();
在OpenDoor和CloseDoor里:
AudioComponent->Play();
这样我们进行测试,会发现声音在不断的播放,我们需要对这个问题进行改进。
首先我们要取消Audio组件的自动启用:
接着我们可以定义两个bool类型的变量来解决这个问题。
bool OpenDoorSound = false;
bool CloseDoorSound = true;
然后在CloseDoor设置(同理OpenDoor):
OpenDoorSound = false;
if(!AudioComponent){return;}
if (!CloseDoorSound)
{
AudioComponent->Play();
CloseDoorSound = true;
}
CloseDoorSound = false;
if(!AudioComponent){return;}
if (!OpenDoorSound)
{
AudioComponent->Play();
OpenDoorSound = true;
}
好的,那关于本章的内容就此结束。关于场景优化等内容,这里不会涉及。