虚幻引擎C++开发学习(二)下

24 篇文章 5 订阅
4 篇文章 7 订阅

由于篇幅原因,这里继续上一章的内容。

一、增加拾取道具

这一章,我们增加一个道具的拾取,将其放在触发体积的区域内,让门能保持开启。

我们想做的事情:

  • 我们在场景中,添加一个基础的网格体,如椎体或球体。我们希望玩家可以抓取这个物体。
  • 我们希望为玩家添加一个组件。而且这个玩家是出现在游戏中的,在游戏开始前还没出现。

关于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;
}

 好的,那关于本章的内容就此结束。关于场景优化等内容,这里不会涉及。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值