斯坦福 UE4 C++ ActionRoguelike游戏实例教程 16.优化交互,实现看到物体时出现交互提示

发布时间 2023-04-24 20:20:20作者: 仇白

斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本篇文章对应Lecture 18 – Creating Buffs, World Interaction, 73节。本文将会重构以前实现过的SurInteractionComponent,实现在玩家注释可交互物体时,可以出现可交互提示,效果如下:

在文章的最后,我会放出所有相关的代码。

优化交互

在几十节课之前,我们学习过如何与场景中的物体进行交互。其中有一项就是定义了一个交互组件(SurInteractionComponent),它允许角色在按下交互键时,如果视线方向有可交互物体,即可与物体进行交互,触发物体的Interact函数。

本篇文章则要对这个组件进行一次升级,当我们注视可交互物体时,可以直接出现一个交互提示(UMG控件),提醒我们这个物体是可以交互的;当我们按下交互键时,即可对物体进行交互。具体怎么升级呢,让我们边做边说吧。


要实现这个功能,我们需要做到以下几点:

  1. 当我们注视一个Actor时,要获取这个Actor的引用;反之,当我们视线离开这个Actor时,取消对这个Actor的引用
  2. 当我们获得这个Actor的引用时,要生成交互提示控件,并在这个Actor周围显示这个控件;反之,隐藏这个控件
  3. 当我们按下交互键(根据自己的设置)时,判断我们是否拿到了目标Actor的引用(指针是否为空)。如果拿到了,则执行交互相关逻辑。

以上就是我们本文要实现的全部需求,为此,我们需要为我们的交互组件(SurInterationComponent)添加以下主要成员。

主要有

  1. 当前注视的Actor
  2. 要生成的控件类,这里选用之前实现的SurWorldUserWidget子类,它可以选定一个Actor,并将自己在视口中依附在Actor周围。
  3. 控件实例,根据第二点的控件类生成一个实例
//当前可交互的物体
UPROPERTY() //将其标记为UPEOPERTY, 监控其生命周期,避免空悬指针的发生
AActor* FocusActor;

//指定生成控件的类
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<USurWorldUserWidget> DefaultWidgetClass;

//当有可交互的物体时,会生成指定的控件
UPROPERTY()
USurWorldUserWidget* DefaultWidgetInstance;

public:	
void FindBestInteractable();

我们将执行交互(PrimaryInteract)和寻找交互对象(FindBestInteractable)分离了开来。这是因为我们是要通过“注视”来寻找交互物体,并且实时判断该物体能不能交互。因此,我们需要把之前PrimaryInteract的寻找交互对象的大部分逻辑移动过来,并对其进行一些修改,其最核心的逻辑如下:

  1. 在一开始时将FocusActor置为空。
  2. 做射线检测,如果命中了物体,并且物体实现了Interact接口,则将物体作为FocusActor
  3. 如果FocusActor不为空,则尝试生成控件。这里生成控件的方式有些类似于单例模式中的懒汉模式,只有当第一次需要生成控件的时候才实例化,而不是在构造函数中就实例化了这个控件。
  4. 如果FocusActor不为空,且拥有控件实例,则将其添加到视口中
  5. 如果FocusActor为空,则将控件实例从视口中移除。
void USurInteractionComponent::FindBestInteractable()
{
   bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread();
   
   ...//
   ...//射线检测部分代码
   ...//
       
   FocusActor = nullptr;
   AActor* HitActor = Hit.GetActor();
   if(HitActor)
   {
      //判断碰撞体是否实现了我们需要的接口
      if(HitActor->Implements<USurGameInterface>())
      {
         FocusActor = HitActor;
         if(bDebugDraw)
         {
            //用于Debug
            DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 32, FColor::Green, false, 3);
         }
      }
   }
   else
   {
      if(bDebugDraw)
      {
         DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 32, FColor::Red, false, 3);
      }
   }

   //如果当前有聚焦ACTOR,就生成控件
   if(FocusActor)
   {
      if(DefaultWidgetInstance == nullptr && ensure(DefaultWidgetClass))
      {
         DefaultWidgetInstance = CreateWidget<USurWorldUserWidget>(GetWorld(), DefaultWidgetClass);
      }

      if(DefaultWidgetInstance)
      {
         DefaultWidgetInstance->AttachedActor = FocusActor;
         if(!DefaultWidgetInstance->IsInViewport())
         {
            DefaultWidgetInstance->AddToViewport();
         }
      }
   }
   else
   {
      if(DefaultWidgetInstance)
      {
         DefaultWidgetInstance->RemoveFromParent();
      }
   }
}

可以看到,核心逻辑基本围绕FocusActor在运行,也只有注释可交互物体时,我们才能获得FocusActor。

当然,以上操作需要时刻运行。值得一提的是,课程里将FindBestInteractable放在TickComponent并不是最好的做法。如果一个游戏有60帧的话,那么我们一秒钟就得运行该函数60次,实际上很多时候我们并不需要执行那么多次,也许我们可以设置一个定时器,让其间隔更长的时间,也可以修改Component类的TickInterval,这样可以提高程序运行的效率。

void USurInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
   Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

   //在每一Tick都查找可交互物体,这个做法比较消耗资源
   //更好的做法是使用一个定时器,每隔一段时间检测一次,效率会相对高一些
   FindBestInteractable();
}


接下来创建要显示的交互提示控件,继承自USurWorldUserWidget, 起名为DefaultWidgetInstance。这部分的操作我们已经做过很多次了,这里就不赘述了。

image-20230423192830917

控件结构

image-20230423192606145

ParentSizeBox属性

image-20230423192816477

设置文本字体等属性

有闲情逸致的话还可以给他加个小动画。


设置好之后,将其设置到InteractionComponent,这样实例化的控件就是这个类了。

image-20230423193127658

设置DefaultWidgetClass

最后进入游戏,可以看到当视线转移到可交互物体上时,就会出现提示控件。

总结

本节课我们升级了很久以前实现的交互组件,成功让其在注视可交互物体时出现提示信息。

到目前为止,我们已经学习了制作单机游戏的大部分能力(入门),事实上,我们已经可以利用我们掌握的这些能力,制作出一个精巧的小游戏来了,至少,在面对一个新的游戏需求时,我们可以拥有某一方面的思路,并将其付诸实践。

在接下来的课程中,课程老师将会带领我们走入多人游戏的大门,后面的东西难度确实有点大啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊阿

全部代码

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SurInteractionComponent.generated.h"


class USurWorldUserWidget;
//交互组件,附加在Actor上允许其与其他物体交互
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class FPSPROJECT_API USurInteractionComponent : public UActorComponent
{
   GENERATED_BODY()

public:    
   // Sets default values for this component's properties
   USurInteractionComponent();

   void FindBestInteractable();
protected:
   // Called when the game starts
   virtual void BeginPlay() override;

   //当前可交互的物体
   UPROPERTY() //将其标记为UPEOPERTY, 监控其生命周期,避免空悬指针的发生
   AActor* FocusActor;

   //指定生成控件的类
   UPROPERTY(EditDefaultsOnly, Category = "UI")
   TSubclassOf<USurWorldUserWidget> DefaultWidgetClass;

   //当有可交互的物体时,会生成指定的控件
   UPROPERTY()
   USurWorldUserWidget* DefaultWidgetInstance;

   UPROPERTY(EditDefaultsOnly, Category = "Trace")
   float TraceDistance;
   
   UPROPERTY(EditDefaultsOnly, Category = "Trace")
   float TraceRadius;

   UPROPERTY(EditDefaultsOnly, Category = "Trace")
   TEnumAsByte<ECollisionChannel> CollisionChannel;
public:    
   // Called every frame
   virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

   void PrimaryInteract();
      
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SurInteractionComponent.h"

#include "DrawDebugHelpers.h"
#include "SurGameInterface.h"
#include "SurWorldUserWidget.h"
#include "GameFramework/Character.h"

static  TAutoConsoleVariable<bool> CVarDebugDrawInteraction(TEXT("su.InteractionDebugDeaw"), false, TEXT("Enable Debug Line for Interact Component."), ECVF_Cheat);


// Sets default values for this component's properties
USurInteractionComponent::USurInteractionComponent()
{
   PrimaryComponentTick.bCanEverTick = true;

   TraceDistance = 1000.f;
   TraceRadius = 50.f;

   CollisionChannel = ECC_WorldDynamic;
}

void USurInteractionComponent::FindBestInteractable()
{
   bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread();
   
   FHitResult Hit;

   FVector CtrlerLocation;//控制器的位置
   FRotator CtrlerRotation;
   
   APawn* MyOnwer = Cast<APawn>(GetOwner());
   if(!MyOnwer) return;
   
   CtrlerLocation = MyOnwer->GetPawnViewLocation();
   CtrlerRotation = MyOnwer->GetControlRotation();
   FVector End = CtrlerLocation + (CtrlerRotation.Vector() * TraceDistance);

   FCollisionObjectQueryParams ObjectQueryParams;//查询参数
   ObjectQueryParams.AddObjectTypesToQuery(CollisionChannel);//选择查询WorldDynamic类型的对象

   FCollisionShape Shape;
   Shape.SetSphere(TraceRadius);
   GetWorld()->SweepSingleByObjectType(Hit, CtrlerLocation, End, FQuat::Identity ,ObjectQueryParams, Shape);

   FocusActor = nullptr;
   AActor* HitActor = Hit.GetActor();
   if(HitActor)
   {
      //判断碰撞体是否实现了我们需要的接口
      if(HitActor->Implements<USurGameInterface>())
      {
         FocusActor = HitActor;
         if(bDebugDraw)
         {
            //用于Debug
            DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 32, FColor::Green, false, 3);
         }
      }
   }
   else
   {
      if(bDebugDraw)
      {
         DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 32, FColor::Red, false, 3);
      }
   }

   //如果当前有聚焦ACTOR,就生成控件
   if(FocusActor)
   {
      if(DefaultWidgetInstance == nullptr && ensure(DefaultWidgetClass))
      {
         DefaultWidgetInstance = CreateWidget<USurWorldUserWidget>(GetWorld(), DefaultWidgetClass);
      }

      if(DefaultWidgetInstance)
      {
         DefaultWidgetInstance->AttachedActor = FocusActor;
         if(!DefaultWidgetInstance->IsInViewport())
         {
            DefaultWidgetInstance->AddToViewport();
         }
      }
   }
   else
   {
      if(DefaultWidgetInstance)
      {
         DefaultWidgetInstance->RemoveFromParent();
      }
   }
}


// Called when the game starts
void USurInteractionComponent::BeginPlay()
{
   Super::BeginPlay();

   // ...
   
}


// Called every frame
void USurInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
   Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

   //在每一Tick都查找可交互物体,这个做法比较消耗资源
   //更好的做法是使用一个定时器,每隔一段时间检测一次,效率会相对高一些
   FindBestInteractable();
}

void USurInteractionComponent::PrimaryInteract()
{
   if(!FocusActor)
   {
      UE_LOG(LogTemp, Warning, TEXT("No FocusActor to Interact."));
      return;
   }
   APawn* MyPawn = Cast<APawn>(GetOwner());
   if(ensure(MyPawn))
   {
      ISurGameInterface::Execute_Interact(FocusActor, MyPawn);
   }
}

image-20230423201537101

控件属性和动画

image-20230423201556994

事件构建并不等同与构造函数,他将在转换层级的时候反复触发