UEC++——Class2_DungeonEscape

0 阅读52分钟

UEC++——Class2_DungeonEscape

在本期课程案例地牢逃脱中,主要使用c++类实现了使用触发器绑定一个物体,随后使其移动,并且还学习了按键交互功能引入了(增强输入系统,Enhanced Input),设置指定按键,当其被触发时执行指定逻辑,以及使用了射线、形状检测以及单通道扫描检测。具体的实现过程让我们一起通过以下内容进行深入了解。

首先我们在本期课程中想要实现的所有功能如下:

  1. 当玩家或特定物体处于触发器检测范围时,特定物体将进行移动。
  2. 玩家按下指定按键实现交互,交互是发射一个形状检测的射线,对特定物体进行拾取和放置。
  3. 存在一种可收集物品可供玩家拾取。
  4. 存在一种类似机关的物品,当玩家使用形状射线检测时,如果拾取了特定物体,该机关将会被启动。
  5. 个人额外添加:触发器也可以选择进行移动。
  6. 个人额外添加:手电筒、游戏提示文本以及退出文本的使用和显示。
  7. 个人额外添加:主菜单、背景音乐、场景切换、胜利文本。

以上就是需要实现的主要功能,当然在这些实现的过程中也将会了解并学习很多其他与之相关知识。好的,那我们先从第一个功能实现进行讲解。

  • 功能1:当玩家或特定物体处于触发器检测范围时,特定物体将进行移动。

    • 首先我们很明确的需要一个触发器组件,那么就直接创建一个BoxComponent类的C++,在这个类中给其添加触发碰撞函数,初始工作就基本完成。可能会有个疑惑为什么不直接创建一个TriggerBox类或者Trigger类呢?因为后续我们需要基于这个c++类创建蓝图,然后在蓝图中添加一些场景物体,如果是基于Trigger类创建的,那么这些添加的场景物体就会默认看不见,这是Trigger类的默认设置,所以不建议基于Trigger类进行创建。

    • 随后的难点在于我们怎么让其控制一个特定物体进行移动呢?其实很简单我们创建一个AActor类的对象指针并设置为EditAnywhere,就可以在引擎中自行选择我们需要触发哪个物体移动了。

    • 但该如何设计其怎么移动呢,其实就是上节课的移动方式,但在这次的移动中我们使用了一个VInterpConstantTo()的函数进行实现,它将以一个恒定的速度插值从一个向量到另一个向量进行移动。随后为了方便我们把这个移动功能重新创建一个ActorComponent的c++类进行实现,随后把其挂载到我们需要移动的物体上,这样就十分方便我们对不同的移动物体进行移动速度和方向的更改。

      VInterpConstantTo():用于以恒定速度插值,将一个向量逐步过渡到目标向量。返回值是一个新的FVector,表示经过恒定速度插值后的向量值。
      
      函数参数
      const FVector& Current:当前向量(位置)
      const FVector& Target:目标向量(位置)
      float DeltaTime:帧时间差
      float InterpSpeed:恒定速度,单位:单位/秒 
      
    • 然后我们回到BoxComponent的触发函数中,我们需要OnOverlapBegin()和OnOverlapEnd()函数来分别检测当前玩家或者特定物体是否触发检测了,在OnOverlapBegin()中,当开始检测时我们对触发的Actor先进行标签(Tag)判断,是否是我们希望进行检测的对象,如果是则对我们绑定的指定移动对象进行移动的设置(先获取其身上挂载的移动组件,如果有则进行相关移动设置),从而实现指定物体移动;当离开检测时则让其移动到原始位置。

    • 但是这里还有一个问题,那就是如果这个触发器如果检测到了两个及以上的可触发的对象时,其中一个离开了检测范围但另一个还在,这时它也会执行让物体移动到原始位置的逻辑。这时我们只需引入一个计数器的变量,在开始检测时进行计数,离开时依次删除计数,并判断计数是否为零,为零才执行相关逻辑即可。

    以下为移动组件和触发组件的完整代码和效果图

    • Mover.h

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "Components/ActorComponent.h"
      #include "Mover.generated.h"
      
      //BlueprintSpawnableComponent:蓝图可生成组件,让我们可以在蓝图里使用这个组件,也就是编辑中能添加使用这个c++组件
      UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
      class DUNGEON_ESCAPE2_API UMover : public UActorComponent
      {
      	GENERATED_BODY()
      
      public:
      	// Sets default values for this component's properties
      	UMover();
      
      protected:
      	// Called when the game starts
      	virtual void BeginPlay() override;
      
      public:
      	// Called every frame
      	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
      	bool GetShouldMove();//获取私有变量ShouldMove的值
      	void SetShouldMove(bool NewShouldMove);//设置私有变量ShouldMove的值
      
      	UPROPERTY(EditAnywhere)
      	FVector MoveOffset;//移动偏移量
      
      	UPROPERTY(EditAnywhere)
      	float MoveTime=5.0f;//移动时间
      
      	UPROPERTY(VisibleAnywhere)
      	float Speed;//移动速度
      
      	UPROPERTY(VisibleAnywhere)
      	bool ReachedTarget = false;//是否抵达目标位置
      
      	FVector StartLocation;//初始位置
      	FVector TargetLocation;//目标位置
      
      private:
      	UPROPERTY(VisibleAnywhere)
      	bool ShouldMove = false;//是否应该(朝向哪里)移动
      };
      
    • Mover.cpp

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #include "Mover.h"
      #include "Math/UnrealMathUtility.h" //FMath函数访问路径
      
      // Sets default values for this component's properties
      UMover::UMover()  //构造函数
      {
      	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
      	// off to improve performance if you don't need them.
      	PrimaryComponentTick.bCanEverTick = true;
      
      }
      
      bool UMover::GetShouldMove()//获取私有变量ShouldMove的值
      {
      	return ShouldMove;
      }
      
      void UMover::SetShouldMove(bool NewShouldMove)//设置私有变量ShouldMove的值并更新移动目标位置
      {
      	ShouldMove = NewShouldMove;
      	if (ShouldMove)//判断是否应该朝向哪里移动
      	{
      		//目标位置=起始位置+移动偏移量(移动距离)
      		TargetLocation = StartLocation + MoveOffset;
      	}
      	else
      	{
      		//目标位置=起始位置
      		TargetLocation = StartLocation;
      	}
      }
      
      // Called when the game starts
      void UMover::BeginPlay()
      {
      	Super::BeginPlay();
      
      	StartLocation = GetOwner()->GetActorLocation();//获取actor初始位置并赋值
      	SetShouldMove(false);//更新ShouldMove值和移动目标位置
      	Speed = MoveOffset.Length() / MoveTime;//获取移动速度
      }
      
      // Called every frame
      void UMover::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
      {
      	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
      
      	//获取actor当前位置;
      	FVector CurrentLocation = GetOwner()->GetActorLocation();
      
      	//与目标位置进行比较,判断是否抵达目标位置
      	ReachedTarget = CurrentLocation.Equals(TargetLocation);
      
      	if (!ReachedTarget)//如果没有抵达则继续移动
      	{
      		//VInterpConstantTo():用于以恒定速度插值,将一个向量逐步过渡到目标向量。
      	//const FVector& Current:当前向量(位置)
      	//const FVector& Target:目标向量(位置)
      	//float DeltaTime:帧时间差
      	//float InterpSpeed:恒定速度,单位:单位/秒 
      	//返回值是一个新的FVector,表示经过恒定速度插值后的向量值。
      		FVector NewLocation = FMath::VInterpConstantTo(CurrentLocation, TargetLocation, DeltaTime, Speed);
      
      		GetOwner()->SetActorLocation(NewLocation);//更新actor位置
      
      	}
      }
      
    • Trigger.h

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "Components/BoxComponent.h"//盒状组件
      #include "Mover.h"
      #include "Trigger.generated.h"  //把对应头文件写在这个之前,不然会出问题
      
      /**
       *
       */
       
       //ClassGroup:用于在编辑器的各类资产选择器中,将你的类归入一个指定的分组
      //DisplayName:是一个元数据说明符(Meta Specifier),用于覆盖类在蓝图编辑器中的显示名称。
      //meta:这是一个至关重要的元数据说明符,它允许蓝图(Blueprint)生成(Spawn)这个组件类的实例。
      UCLASS(ClassGroup = (Custom), DisplayName="Cpp_TriggerComponent",meta = (BlueprintSpawnableComponent))
      class DUNGEON_ESCAPE2_API UTrigger : public UBoxComponent
      {
      	GENERATED_BODY()
      
      public:
      	// Sets default values for this component's properties
      	UTrigger();//构造函数
      
      protected:
      	// Called when the game starts
      	virtual void BeginPlay() override;
      
      public:
      	// Called every frame
      	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
      
      	void Set_Trigger(bool NewTriggerValue);//更新物体移动状态
      	void ChangeTriggerMove(bool NewTriggerMove);//更新触发器移动状态
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	AActor* MoverActor;//设置触发器移动对象指针
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	bool IsPressurePlate = false;//是否为压力板选项
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	bool IsSelfMove=false;//触发器本身是否移动
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	float SelfMoveTime = 3.0f;//自身移动时间
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	FVector TriggerMoveOffset;//触发器本身移动偏移量
      
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VALUE")
      	bool PlayerCanTrigger = false;//判断角色是否可以触发
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	bool IsTriggered=false;//判断触发器是否触发
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	int32 ActivatorCount = 0;//触发检测计数器
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	FVector SelfStart;//自身起始位置
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	float SelfMoveSpeed;//触发器移动速度
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	FVector SelfTarget;//自身目标位置
      
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VALUE")
      	bool SelfIsReached=false;//判断触发器自身是否到达目标位置
      
      	UMover* Mover;//创建一个UMover指针成员变量
      
      	/** 当某对象进入球体组件时调用 */
      	UFUNCTION() //把以下函数注册到虚幻引擎里
      	void OnOverlapBegin( UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
      		int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
      
      	/** 当某对象离开球体组件时调用 */
      	UFUNCTION()
      	void OnOverlapEnd( UPrimitiveComponent* OverlappedComp,  AActor* OtherActor,
      		UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
      };
      
    • Trigger.cpp

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #include "Trigger.h"
      
      UTrigger::UTrigger()//构造函数 在编辑器中加载的时候也会被调用,而不是只在游戏加载的时候被调用
      {
      	PrimaryComponentTick.bCanEverTick = true;//开启tick组件相关功能
      }
      
      void UTrigger::BeginPlay()
      {
      	//Super关键字是用来引用父类的
      	//正常情况下,继承关系的这些类都应该进行super的调用
      	//这里是用于调用父类的begin函数,
      	//因为在父类在BeginPlay以及下面TickComponent函数中也有部分需要运行的代码
      	Super::BeginPlay();
      	if (MoverActor)//判断移动对象指针是否为空
      	{
      		Mover = MoverActor->FindComponentByClass<UMover>();//获取UMover组件
      		if (Mover)//判断Mover组件是否为空
      		{
      			UE_LOG(LogTemp, Display, TEXT("Successfully Found the Mover Component!!!"));
      		}
      		else
      		{
      			UE_LOG(LogTemp, Warning, TEXT("Failed to find Mover Component!!!"));
      		}
      	}
      	else
      	{
      			UE_LOG(LogTemp, Warning, TEXT("MoverActor is nullptr!!!"));
      	}
      
      	if (IsPressurePlate)//判断是否为压力板
      	{
      
      		//UE_LOG(LogTemp, Display, TEXT("isPressure!!!"));
      		//某个物体与之重叠时调用该事件
      		OnComponentBeginOverlap.AddDynamic(this, &UTrigger::OnOverlapBegin);//添加开始重叠委托事件
      		//某个物体与之结束重叠时调用该事件
      		OnComponentEndOverlap.AddDynamic(this, &UTrigger::OnOverlapEnd);//添加结束重叠委托事件
      	}
      	if (IsSelfMove)//如果触发器自身要移动
      	{
      		SelfStart = GetOwner()->GetActorLocation();//先获取自身初始位置
      		//UE_LOG(LogTemp, Warning, TEXT("OwnerName is %s"),*GetOwner()->GetActorNameOrLabel());
      		ChangeTriggerMove(false);//游戏刚开始先设置触发器目标位置
      		SelfMoveSpeed = TriggerMoveOffset.Length() / SelfMoveTime;//获取移动速度
      	}
      
      }
      
      void UTrigger::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction * ThisTickFunction)
      {
      	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
      	if (IsSelfMove)//如果触发器自身移动
      	{
      		FVector SelfCurrentLocation = GetOwner()->GetActorLocation();//实时获取触发器当前位置
      		SelfIsReached = SelfCurrentLocation.Equals(SelfTarget);//判断是否抵达目标位置
      		if (!SelfIsReached)
      		{
      			//未抵达目标位置,继续移动
      			FVector NewTriggerLocation = FMath::VInterpConstantTo(SelfCurrentLocation, SelfTarget, GetWorld()->GetDeltaSeconds(), SelfMoveSpeed);
      			GetOwner()->SetActorLocation(NewTriggerLocation);
      		}
      	}
      }
      void UTrigger::Set_Trigger(bool NewTriggerValue)//更新触发器状态和物体移动状态
      {
      	IsTriggered = NewTriggerValue;//更新触发器状态
      	if (Mover)
      	{
      		//更新移动性
      		Mover->SetShouldMove(IsTriggered);
      		if (IsSelfMove)//如果触发器自身会移动
      		{
      			ChangeTriggerMove(IsTriggered);//更新触发器移动状态
      		}
      	}
      	else
      	{
      		UE_LOG(LogTemp, Warning, TEXT("%s does not find Mover!"),*GetOwner()->GetActorNameOrLabel());
      	}
      }
      
      void UTrigger::ChangeTriggerMove(bool NewTriggerMove)//触发器移动逻辑
      {
      	if (NewTriggerMove)
      	{
      		SelfTarget = SelfStart + TriggerMoveOffset;//更新目标位置
      	}
      	else
      	{
      		SelfTarget = SelfStart;//目标位置初始位置
      	}
      }
      
      //开始重叠
      void UTrigger::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
      {
      	if (OtherActor && OtherActor->ActorHasTag("Player")&&PlayerCanTrigger)
      	{
      		//UE_LOG(LogTemp, Display, TEXT("Add Before ActivatorCount = %d "), ActivatorCount);
      		ActivatorCount++;//当有目标物体激活触发器时,触发计数增加,记录当前触发器内目标物体数量
      		if (!IsTriggered)//如果触发器未触发
      		{
      			Set_Trigger(true);//更新触发器状态和物体移动状态
      		}
      		//UE_LOG(LogTemp, Display, TEXT("Add After ActivatorCount = %d "), ActivatorCount);
      	}
      	else if (OtherActor&&OtherActor->ActorHasTag("PressureActivator"))
      	{
      		//UE_LOG(LogTemp, Display, TEXT("Add Before ActivatorCount = %d "), ActivatorCount);
      		ActivatorCount++;//当有目标物体激活触发器时,触发计数增加,记录当前触发器内目标物体数量
      		if (!IsTriggered)//如果触发器未触发
      		{
      			Set_Trigger(true);//更新触发器状态和物体移动状态
      		}
      		//UE_LOG(LogTemp, Display, TEXT("Add After ActivatorCount = %d "), ActivatorCount);
      	}
      }
      //结束重叠
      void UTrigger::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
      {
      	if (OtherActor && OtherActor->ActorHasTag("Player") && PlayerCanTrigger)
      	{
      		ActivatorCount--;//当有目标物体离开触发器时,触发计数减少,记录当前触发器内目标物体数量
      		if (!ActivatorCount && IsTriggered)//如果触发器已触发并且触发计数器为0
      		{
      			Set_Trigger(false);//更新触发器状态和物体移动状态
      		}
      		//UE_LOG(LogTemp, Display, TEXT("EndOverlap ActivatorCount = %d "), ActivatorCount);
      	}
      	else if (OtherActor&&OtherActor->ActorHasTag("PressureActivator"))
      	{
      		ActivatorCount--;//当有目标物体离开触发器时,触发计数减少,记录当前触发器内目标物体数量
      		if (!ActivatorCount&&IsTriggered)//如果触发器已触发并且触发计数器为0
      		{
      			Set_Trigger(false);//更新触发器状态和物体移动状态
      		}
      		//UE_LOG(LogTemp, Display, TEXT("EndOverlap ActivatorCount = %d "), ActivatorCount);
      	}
      }
      
    • 效果图

      GIF 2026-10-27 11-53-58.gif

  • 功能2:玩家按下指定按键实现交互,交互是发射一个形状检测的射线,对特定物体进行拾取和放置。

    • 实现这个功能之前需要先引入增强输入系统(Enhanced Input System)。

      UE5 的增强输入系统(Enhanced Input)是一套以 Input Action、Input Mapping Context、Input Modifier、Input Trigger 为核心的数据驱动输入框架,提供运行时切换输入方案、复杂触发逻辑(长按、双击、组合键)、多设备映射等高级功能,并自 UE5.1 起成为默认输入系统。

    • 了解增强输入系统后,我们就可以为需要绑定的指定按键进行设置了。这里我们可以在项目自带的Input\Action文件中看到许多系统已经设计好的输入操作数据资产有移动跳跃等等,而我们只需何其一样创建一个交互数据资产即可。随后回到Input文件中找到IMC_Default输入上下文映射数据资产,进去其中把刚刚创建的交互输入操作资产添加到其中并为其绑定好按键。

    • 我们进入到系统Character.h文件中在其的变量设置中找到分类为Input的地方,复制粘贴一份把名字更改一下(InteractAction),在这之后我们再添加一个交互逻辑的函数,以方便给这个输入按键绑定触发逻辑,这里只需声明一下这个交互函数即可,具体的实现方式我们在Character.cpp中进行实现。

    • 随后进入Character.cpp中为这个新添加的输入映射操作进行绑定和触发逻辑的实现。在其中找到一个名为SetupPlayerInputComponent的函数,这就是对输入操作进行相关触发逻辑绑定的地方,复制粘贴一份后修改其中的相关变量、触发逻辑以及逻辑事件即可。

    • 之后我们便可以来到交互函数中进行逻辑设计,当按键被触发我们就会发射一个形状检测的射线来进行检测,这里将使用使用一个SweepSingleChannle()的函数来进行实现,这个函数的作用为在世界中扫掠一个指定形状(如盒体、球体或胶囊体),并使用特定的碰撞通道(Trace Channel)来检测(这个通道主要是为了选择我们想检测对象进行检测,不想检测场景中的所有角色,所以这里设置为全部忽略,因为我们需要检测的对象其实很少,需要检测的对象自行设置碰撞预设即可), 返回第一个阻挡碰撞(Blocking Hit)的信息。所以这个函数刚好能实现我们想要的功能。

      SweepSingleByChannel:在世界中扫掠一个指定形状(如盒体、球体或胶囊体),并使用特定的碰撞通道(Trace Channel)来检测,返回第一个阻挡碰撞(Blocking Hit)的信息。
      
      函数参数
      struct FHitResult& OutHit:输出参数。如果发生碰撞,此结构体将包含第一个阻挡碰撞的详细信息(如碰撞点、法线、命中的Actor等)
      const FVector& Start:扫掠形状的起始位置(世界坐标)
      const FVector& End:扫掠形状的结束位置(世界坐标)
      const FQuat& Rot:扫掠形状的旋转(四元数)。如果形状不需要旋转,可传入 FQuat::Identity。
      ECollisionChannel TraceChannel:碰撞通道。用于决定本次查询会与哪些组件发生碰撞,例如 ECC_WorldStatic、ECC_Pawn 或自定义通道。(需先在引擎中的“项目设置\碰撞”中进行创建,随后在DefaultEngin配置文件中查找)
      const FCollisionShape& CollisionShape:要扫掠的几何形状。支持 Box、Sphere 和 Capsule。例如,可用 FCollisionShape::MakeBox(Extent) 创建一个盒体。
      const FCollisionQueryParams& Params:额外查询参数。可用于忽略特定Actor、指定是否返回复杂碰撞信息等。
      const FCollisionResponseParams& ResponseParam:响应容器。用于更精细地控制查询与不同碰撞通道的响应方式(阻挡、重叠或忽略)。
      
    • 但射线发射的起始位置和发射距离该如果设计呢?这里由于我们游戏为第一人称模式,所以我们直接使用角色的摄像机位置作为射线发射起始点,而发射距离则直接设置为变量,通过初始位置加上发射距离,直接计算出最后的终点位置,这样也方便后续更改,这样我们的具体射线检测逻辑已经完整。

    • 而对物体的拾取和放置这里采用的是简易的实现方式,如果射线碰撞检测到了可收集物品和需要使用物品的物体,那么就通过数组的形式把物体的特定信息存进去并删除检测的物体,随后在使用时则对数组中存在的特定信息进行查找,如果找到则使用物体并删除其在数组中的相关信息。

    • 在这之后我们需要回到引擎中,进入系统角色蓝图中并在其细节面板的输入属性中找到我们在Character.h中添加的输入操作InteractAction,把我们创建的交互输入操作的数据资产添加上去即可。

    以下为完整的Character.h和Character.cpp的完整代码和效果图(其中包含后续我自己添加的额外功能)。

    • Character.h

      // Copyright Epic Games, Inc. All Rights Reserved.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "GameFramework/Character.h"
      #include "Logging/LogMacros.h"
      #include "Components/SpotLightComponent.h"  //聚光灯光组件
      #include "Dungeon_Escape2Character.generated.h"
      
      class UInputComponent;
      class USkeletalMeshComponent;
      class UCameraComponent;
      class UInputAction;
      struct FInputActionValue;
      
      DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
      
      /**
       *  A basic first person character
       */
      UCLASS(abstract)
      class ADungeon_Escape2Character : public ACharacter
      {
      	GENERATED_BODY()
      
      	/** Pawn mesh: first person view (arms; seen only by self) */
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
      	USkeletalMeshComponent* FirstPersonMesh;
      
      	/** First person camera */
      	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
      	UCameraComponent* FirstPersonCameraComponent;
      
      protected:
      
      	/** Jump Input Action */
      	UPROPERTY(EditAnywhere, Category ="Input")
      	UInputAction* JumpAction;
      
      	/** Move Input Action */
      	UPROPERTY(EditAnywhere, Category ="Input")
      	UInputAction* MoveAction;
      
      	/** Look Input Action */
      	UPROPERTY(EditAnywhere, Category ="Input")
      	class UInputAction* LookAction;
      
      	/** Mouse Look Input Action */
      	UPROPERTY(EditAnywhere, Category ="Input")
      	class UInputAction* MouseLookAction;
      
      	UPROPERTY(EditAnywhere, Category = "Input")//创建一个交互输入动作
      	class UInputAction* InteractAction;
      
      	UPROPERTY(EditAnywhere, Category = "Input")//创建一个手电筒按键
      	class UInputAction* FlashAction;
      
      	UPROPERTY(EditAnywhere, Category = "Input")//创建一个游戏提示按键
      	class UInputAction* TipsAction;
      
      	UPROPERTY(EditAnywhere, Category = "Input")//创建一个游戏退出按键
      	class UInputAction* ExitAction;
      public:
      	ADungeon_Escape2Character();
      
      protected:
      
      	/** Called from Input Actions for movement input */
      	void MoveInput(const FInputActionValue& Value);
      
      	/** Called from Input Actions for looking input */
      	void LookInput(const FInputActionValue& Value);
      
      	/** Handles aim inputs from either controls or UI interfaces */
      	UFUNCTION(BlueprintCallable, Category="Input")
      	virtual void DoAim(float Yaw, float Pitch);
      
      	/** Handles move inputs from either controls or UI interfaces */
      	UFUNCTION(BlueprintCallable, Category="Input")
      	virtual void DoMove(float Right, float Forward);
      
      	/** Handles jump start inputs from either controls or UI interfaces */
      	UFUNCTION(BlueprintCallable, Category="Input")
      	virtual void DoJumpStart();
      
      	/** Handles jump end inputs from either controls or UI interfaces */
      	UFUNCTION(BlueprintCallable, Category="Input")
      	virtual void DoJumpEnd();
      
      protected:
      
      	/** Set up input action bindings */
      	virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
      
      	// Called when the game starts
      	virtual void BeginPlay() override;
      
      public:
      
      	/** Returns the first person mesh **/
      	USkeletalMeshComponent* GetFirstPersonMesh() const { return FirstPersonMesh; }
      
      	/** Returns first person camera component **/
      	UCameraComponent* GetFirstPersonCameraComponent() const { return FirstPersonCameraComponent; }
      
      	void Interact();//交互逻辑函数
      	void FlashInteract();//手电筒交互函数
      	void ExitInteract();//游戏退出交互函数
      	void TipsInteract();//提示文本交互函数
      
      	UFUNCTION(BlueprintCallable)//让蓝图可以调用该函数
      	void ShowTipsWidget();//显示提示文本
      
      	UFUNCTION(BlueprintCallable)//让蓝图可以调用该函数
      	void HiddenTipsWidget();//关闭提示文本
      
      	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "TEXT_Trigger")
      	UUserWidget* TipsWidght;//游戏提示UI
      
      	UPROPERTY(EditAnywhere, Category = "TEXT_Trigger")
      	UUserWidget* ExitWidght;//退出提示UI
      
      	UPROPERTY(EditAnywhere,CateGory="Interact")
      	float MaxInteractionDistance = 300.0f;//最大交互距离
      
      	UPROPERTY(EditAnywhere, CateGory = "Interact")
      	float InteractionSphereRadius = 10.0f;//球形交互半径
      
      	UPROPERTY(VisibleAnywhere)
      	USpotLightComponent* FlashLight;//手电筒
      	UPROPERTY(VisibleAnywhere)
      	bool IsFlash = false;//手电筒是否处于使用状态
      
      	UPROPERTY(VisibleAnywhere,Category = "TEXT_Trigger")
      	bool IsShowTips =true;//提示文本是否处于显示状态
      
      private:
      	UPROPERTY(VisibleAnywhere, CateGory = "Interact")
      	TArray<FString>ItemList;//存储拾取到的物品名
      
      };
      
    • Character.cpp

      // Copyright Epic Games, Inc. All Rights Reserved.
      
      #include "Dungeon_Escape2Character.h"
      #include "Animation/AnimInstance.h"
      #include "Camera/CameraComponent.h"
      #include "Components/CapsuleComponent.h"
      #include "Components/SkeletalMeshComponent.h"
      #include "EnhancedInputComponent.h"
      #include "InputActionValue.h"
      #include "GameFramework/CharacterMovementComponent.h"
      #include "Dungeon_Escape2.h"
      #include "Lock.h" //锁组件
      #include "CollectableItem.h"//可收集物品组件
      #include "Components/SpotLightComponent.h"  //聚光灯光组件
      #include "Blueprint/UserWidget.h"//用户控件组件
      
      ADungeon_Escape2Character::ADungeon_Escape2Character()
      {
      	// Set size for collision capsule
      	GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f);
      	
      	// Create the first person mesh that will be viewed only by this character's owner
      	FirstPersonMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("First Person Mesh"));
      
      	FirstPersonMesh->SetupAttachment(GetMesh());
      	FirstPersonMesh->SetOnlyOwnerSee(true);
      	FirstPersonMesh->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::FirstPerson;
      	FirstPersonMesh->SetCollisionProfileName(FName("NoCollision"));
      
      	// Create the Camera Component	
      	FirstPersonCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("First Person Camera"));
      	FirstPersonCameraComponent->SetupAttachment(FirstPersonMesh, FName("head"));
      	FirstPersonCameraComponent->SetRelativeLocationAndRotation(FVector(-2.8f, 5.89f, 0.0f), FRotator(0.0f, 90.0f, -90.0f));
      	FirstPersonCameraComponent->bUsePawnControlRotation = true;
      	FirstPersonCameraComponent->bEnableFirstPersonFieldOfView = true;
      	FirstPersonCameraComponent->bEnableFirstPersonScale = true;
      	FirstPersonCameraComponent->FirstPersonFieldOfView = 70.0f;
      	FirstPersonCameraComponent->FirstPersonScale = 0.6f;
      
      	// configure the character comps
      	GetMesh()->SetOwnerNoSee(true);
      	GetMesh()->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::WorldSpaceRepresentation;
      
      	GetCapsuleComponent()->SetCapsuleSize(34.0f, 96.0f);
      
      	// Configure character movement
      	GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
      	GetCharacterMovement()->AirControl = 0.5f;
      
      	//创建光源组件
      	FlashLight = CreateDefaultSubobject<USpotLightComponent>(TEXT("Flash"));
      	FlashLight->SetupAttachment(FirstPersonCameraComponent);//附加到摄像机位置
      	FlashLight->SetVisibility(false);//游戏开始默认关闭手电筒
      }
      
      void ADungeon_Escape2Character::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
      {	
      	// Set up action bindings
      	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
      	{
      		// Jumping
      		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ADungeon_Escape2Character::DoJumpStart);
      		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ADungeon_Escape2Character::DoJumpEnd);
      
      		// Moving
      		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ADungeon_Escape2Character::MoveInput);
      
      		// Looking/Aiming
      		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ADungeon_Escape2Character::LookInput);
      		EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &ADungeon_Escape2Character::LookInput);
      
      		//Interact  
      		//ETriggerEvent::Started: 按键按下时触发一次
      		//ETriggerEvent::Completed:按键松开时触发
      		//ETriggerEvent::Triggered:按键只要不松开就一直触发
      		EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ADungeon_Escape2Character::Interact);//绑定交互按键事件
      
      		EnhancedInputComponent->BindAction(FlashAction, ETriggerEvent::Started, this, &ADungeon_Escape2Character::FlashInteract);//绑定手电筒按键事件
      
      		EnhancedInputComponent->BindAction(TipsAction, ETriggerEvent::Started, this, &ADungeon_Escape2Character::TipsInteract);//绑定提示文本按键事件
      
      		EnhancedInputComponent->BindAction(ExitAction, ETriggerEvent::Started, this, &ADungeon_Escape2Character::ExitInteract);//绑定退出按键事件
      	}
      	else
      	{
      		UE_LOG(LogDungeon_Escape2, Error, TEXT("'%s' Failed to find an Enhanced Input Component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
      	}
      }
      
      void ADungeon_Escape2Character::BeginPlay()//游戏开始时进行加载
      {
      	//这个必须要加,因为在这会跳过 ACharacter 和 AActor 在 BeginPlay 阶段的关键初始化,
      	// 其中就包括与 CharacterMovementComponent 和输入系统正常协作的部分,
      	// 从而导致移动指令无法被正确处理。
      	Super::BeginPlay();
      
      	////如果提示文本不为空
      //if (TipsWidght)
      //{
      //	UE_LOG(LogTemp, Error, TEXT("is Running!!!"));
      //	//添加到视口
      //	TipsWidght->AddToViewport();
      //	//隐藏提示文本
      //	TipsWidght->SetVisibility(ESlateVisibility::Visible);
      //	//更新文本显示状态
      //	IsShowTips = false;
      //}
      
      	//如果退出文本不为空
      	if (ExitWidght)
      	{
      		//添加到视口
      		ExitWidght->AddToViewport();
      		//隐藏退出按键
      		ExitWidght->SetVisibility(ESlateVisibility::Hidden);
      	}
      
      }
      void VecFunction(FVector MyVec)
      {
      	MyVec.X = 45.0f;
      	MyVec.Y = 66.0f;
      	MyVec.Z = 77.0f;
      }
      
      void VecRefFunction(FVector& MyVec)
      {
      	MyVec.X = 45.0f;
      	MyVec.Y = 66.0f;
      	MyVec.Z = 77.0f;
      }
      void ADungeon_Escape2Character::Interact()//交互逻辑
      {
      	//UE_LOG(LogTemp,Display,TEXT("Interact!!!"));
      	
      	//获取交互检测的初始位置
      	FVector Start = FirstPersonCameraComponent->GetComponentLocation();
      	//获取交互检测的目标位置
      	FVector End = Start + FirstPersonCameraComponent->GetForwardVector() * MaxInteractionDistance;
      
      	//DrawDebugLine:在游戏中打印出一条射线
      	//const UWorld* InWorld:当前世界上下文
      	//FVector const& LineStart:线条起点位置(世界坐标)
      	//FVector const& LineEnd:线条终点位置(世界坐标)
      	//FColor const& Color:线条颜色(FCoLor::Red等)
      	//bool bPersistentLines = false:是否持久显示(false = 仅一帧)
      	//float LifeTime = -1.f:显示持续时间(秒), -1表示仅当前帧
      	//uint8 DepthPriority = 0:深度优先级(0 = 场景默认)
      	//float Thickness = 0.f:线条粗细
      	DrawDebugLine(GetWorld(), Start, End,FColor::Blue,false,1.0f);
      
      	//交互形状大小的创建
      	FCollisionShape InteractionSphere=FCollisionShape::MakeSphere(InteractionSphereRadius);
      	//DrawDebugSphere:用于绘制一个调试球体
      	//Vector Center(中心):球心在三维空间中的位置
      	//Float(默认100) Radius(半径):球的大小
      	//Integer(默认12) Segments(分段):球体用多少段构成, 越大越圆
      	//FColor Line Color(线条颜色):颜色
      	//Float Duration(持续时间):画完多少秒后消失
      	//Float Thickness(厚度):线条的粗细
      	DrawDebugSphere(GetWorld(), Start, InteractionSphereRadius, 30, FColor::Black, false, 1.0f);
      	DrawDebugSphere(GetWorld(), End, InteractionSphereRadius, 30, FColor::Yellow, false,1.0f);
      
      	FHitResult HitResult;//存储碰撞检测结果
      	//存储是否击中
      	//SweepSingleByChannel:在世界中扫掠一个指定形状(如盒体、球体或胶囊体),
      	// 并使用特定的碰撞通道(Trace Channel)来检测,
      	// 返回第一个阻挡碰撞(Blocking Hit)的信息。
      	//struct FHitResult& OutHit:输出参数。如果发生碰撞,此结构体将包含第一个阻挡碰撞的详细信息(如碰撞点、法线、命中的Actor等)
      	//const FVector& Start:扫掠形状的起始位置(世界坐标)
      	//const FVector& End:扫掠形状的结束位置(世界坐标)
      	//const FQuat& Rot:扫掠形状的旋转(四元数)。如果形状不需要旋转,可传入 FQuat::Identity。
      	//ECollisionChannel TraceChannel:碰撞通道。用于决定本次查询会与哪些组件发生碰撞,例如 ECC_WorldStatic、ECC_Pawn 或自定义通道。(需先在引擎中的“项目设置\碰撞”中进行创建,随后在DefaultEngin配置文件中查找)
      	//const FCollisionShape& CollisionShape:要扫掠的几何形状。支持 Box、Sphere 和 Capsule。例如,可用 FCollisionShape::MakeBox(Extent) 创建一个盒体。
      	//const FCollisionQueryParams& Params:额外查询参数。可用于忽略特定Actor、指定是否返回复杂碰撞信息等。
      	//const FCollisionResponseParams& ResponseParam:响应容器。用于更精细地控制查询与不同碰撞通道的响应方式(阻挡、重叠或忽略)。
      	bool HasHit=GetWorld()->SweepSingleByChannel(HitResult,
      		Start, End,
      		FQuat::Identity,
      		ECC_GameTraceChannel2,
      		InteractionSphere
      	);
      
      	if (HasHit)//判断是否击中actor
      	{
      		//UE_LOG(LogTemp, Display, TEXT("HitSomething and Name is %s"),*HitResult.GetActor()->GetActorNameOrLabel());
      		
      		//保存击中的atcor相关信息
      		AActor* HitActor = HitResult.GetActor();
      		//判断击中actor的标签
      		if (HitActor->ActorHasTag("CollectableItem"))
      		{
      			//HitActor is a CollectableItem
      			//UE_LOG(LogTemp, Display, TEXT("Collectable Item!"));
      			
      			//类型转换,不然无法访问CollectableItem里面的相关属性
      			ACollectableItem* CollectableItem=Cast<ACollectableItem>(HitActor);
      			if (CollectableItem)//判断转换是否成功
      			{
      				//更新拿走状态
      				CollectableItem->IsTaken = true;
      
      				//把检测到的可收集物品名字存入背包列表
      				ItemList.Add(CollectableItem->ItemName);
      
      				//销毁检测到的CollectableItem可收集物品
      				CollectableItem->Destroy();
      
      				//UE_LOG(LogTemp, Display, TEXT("Collectable Item Name is %s!"), *CollectableItem->ItemName);
      			}
      		}
      		else if (HitActor->ActorHasTag("Lock"))
      		{
      			//HitActor is a Lock
      
      			//类型转换,不然无法访问ALcok里面的相关属性
      			ALock* LockActor = Cast<ALock>(HitActor);
      			if (LockActor)//判断是否转换成功
      			{
      				//判断触发器上是否已放置了钥匙
      				if (!LockActor->GetIsKeyPlaced())
      				{
      					//钥匙为空
      					int32 ItemRemoved=ItemList.RemoveSingle(LockActor->KeyItemName);//在背包列表中查找是否存在钥匙名称并移除
      					if (ItemRemoved)//如果背包里存在当前所需关键钥匙名
      					{
      						//设置关键钥匙已被触发,显示关键钥匙物品并设置相关触发器执行后续逻辑
      						LockActor->SetIsKeyPlaced(true);
      					}
      					else
      					{
      						UE_LOG(LogTemp, Display, TEXT("Key item is Not in inventory!!!"));
      					}
      				}
      				else
      				{
      					//钥匙不为空,则收回钥匙
      					ItemList.Add(LockActor->KeyItemName);//把物品名存入背包列表中
      					//关闭钥匙触发的相关功能
      					LockActor->SetIsKeyPlaced(false);
      				}
      				//UE_LOG(LogTemp, Display, TEXT("Lock Actor Name is %s!!!"),*LockActor->KeyItemName);
      			}
      		}
      	}
      	else
      	{
      		UE_LOG(LogTemp, Display, TEXT("Can't Hit Anything!"));
      	}
      #pragma region RefPractice
      	////引用初始必须赋值且不能为空指针
      	////引用一旦绑定即为终身,后续再绑定其他变量,只会把变量的值赋值给初始绑定的变量
      	//float MyFloat = 10.0f;
      	//float& FloatRef = MyFloat;
      	//UE_LOG(LogTemp, Display, TEXT("MyFloat is %f  FloatRef is %f"), MyFloat, FloatRef);//10.0f 10.0f
      
      	//FloatRef = 30.0f;
      	//UE_LOG(LogTemp, Display, TEXT("MyFloat is %f  FloatRef is %f"), MyFloat, FloatRef);//30.0f 30.0f
      
      	//MyFloat = 40.0f;
      	//UE_LOG(LogTemp, Display, TEXT("MyFloat is %f  FloatRef is %f"), MyFloat, FloatRef);//40.0f 40.0f
      
      	//float FloatRef2 = 88.8f;
      	//FloatRef = FloatRef2;
      	//UE_LOG(LogTemp, Display, TEXT("MyFloat is %f  FloatRef is %f"), MyFloat, FloatRef);//88.8f 88.8f
      
      	//FVector MyVector = { 1,2,3 };
      	//FVector& VectorRef = MyVector;
      	//UE_LOG(LogTemp, Display, TEXT("MyVector is %s  VectorRef is %s"), *MyVector.ToCompactString(), *VectorRef.ToCompactString());//1 2 3  1 2 3
      
      	//VectorRef.X = 13;
      	//VectorRef.Y = 14;
      	//VectorRef.Z = 520;
      	//UE_LOG(LogTemp, Display, TEXT("MyVector is %s  VectorRef is %s"), *MyVector.ToCompactString(), *VectorRef.ToCompactString());//13 14 520  13 14 520
      
      	////引用参数将改变传入变量本身
      	//FVector TestVec = { 1.0f,2.0f,3.0f };
      	//UE_LOG(LogTemp, Display, TEXT("NoRef Before MyVec is %s"), *TestVec.ToCompactString());
      	//VecFunction(TestVec);
      	//UE_LOG(LogTemp, Display, TEXT("NoRef After MyVec is %s"), *TestVec.ToCompactString());
      
      	//UE_LOG(LogTemp, Display, TEXT("Ref Before MyVec is %s"), *TestVec.ToCompactString());
      	//VecRefFunction(TestVec);
      	//UE_LOG(LogTemp, Display, TEXT("Ref After MyVec is %s"), *TestVec.ToCompactString());
      #pragma endregion
      
      }
      
      void ADungeon_Escape2Character::FlashInteract()//手电筒使用逻辑
      {
      	if (IsFlash)
      	{
      
      		//此时手电筒正在使用
      		FlashLight->SetVisibility(false);//更新手电筒状态
      	}
      	else
      	{
      		//此时手电筒未使用
      		FlashLight->SetVisibility(true);//更新手电筒状态
      	}
      	IsFlash = !IsFlash;//更新手电筒状态
      }
      
      void ADungeon_Escape2Character::TipsInteract()//提示文本交互函数
      {
      	if (TipsWidght)//如果游戏提示文本不为空
      	{
      		UE_LOG(LogTemp, Display, TEXT("判断前 IsShowTips = %s"), IsShowTips ? TEXT("true") : TEXT("false"));
      		if(IsShowTips)
      		{
      			UE_LOG(LogTemp, Display, TEXT("显示文本 IsShowTips = %s"),IsShowTips?TEXT("true"):TEXT("false"));
      			//处于显示状态
      			ShowTipsWidget();//游戏提示文本ui设置为可见
      		}
      		else
      		{
      			UE_LOG(LogTemp, Display, TEXT("关闭文本 IsShowTips = %s"), IsShowTips ? TEXT("true") : TEXT("false"));
      			//处于关闭状态
      			HiddenTipsWidget();//游戏提示文本ui设置为隐藏
      		}
      			IsShowTips = !IsShowTips;//更新游戏提示文本显示状态
      			UE_LOG(LogTemp, Display, TEXT("更新状态 IsShowTips = %s"), IsShowTips ? TEXT("true") : TEXT("false"));
      	}
      }
      
      void ADungeon_Escape2Character::ShowTipsWidget()//显示提示文本
      {
      	if (TipsWidght)
      	{
      		TipsWidght->SetVisibility(ESlateVisibility::Visible);//游戏提示文本ui设置为可见
      	}
      }
      
      void ADungeon_Escape2Character::HiddenTipsWidget()//隐藏提示文本
      {
      	if (TipsWidght)
      	{
      		TipsWidght->SetVisibility(ESlateVisibility::Hidden);//游戏提示文本ui设置为隐藏
      	}
      }
      
      void ADungeon_Escape2Character::ExitInteract()//游戏退出交互函数
      {
      	if (ExitWidght)//如果退出文本不为空
      	{	
      		if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
      		{
      			//成功获取玩家控制器
      			PC->bShowMouseCursor=true;//显示鼠标
      			FInputModeUIOnly Mode;//只读取UI输入
      			PC->SetInputMode(Mode);//设置游戏模式为只读取UI输入
      			ExitWidght->SetVisibility(ESlateVisibility::Visible);//退出ui设置为可见
      		}
      		
      	}
      }
      
      void ADungeon_Escape2Character::MoveInput(const FInputActionValue& Value)
      {
      	// get the Vector2D move axis
      	FVector2D MovementVector = Value.Get<FVector2D>();
      
      	// pass the axis values to the move input
      	DoMove(MovementVector.X, MovementVector.Y);
      
      }
      
      void ADungeon_Escape2Character::LookInput(const FInputActionValue& Value)
      {
      	// get the Vector2D look axis
      	FVector2D LookAxisVector = Value.Get<FVector2D>();
      
      	// pass the axis values to the aim input
      	DoAim(LookAxisVector.X, LookAxisVector.Y);
      
      }
      
      void ADungeon_Escape2Character::DoAim(float Yaw, float Pitch)
      {
      	if (GetController())
      	{
      		// pass the rotation inputs
      		AddControllerYawInput(Yaw);
      		AddControllerPitchInput(Pitch);
      	}
      }
      
      void ADungeon_Escape2Character::DoMove(float Right, float Forward)
      {
      	if (GetController())
      	{
      		// pass the move inputs
      		AddMovementInput(GetActorRightVector(), Right);
      		AddMovementInput(GetActorForwardVector(), Forward);
      	}
      }
      
      void ADungeon_Escape2Character::DoJumpStart()
      {
      	// pass Jump to the character
      	Jump();
      }
      
      void ADungeon_Escape2Character::DoJumpEnd()
      {
      	// pass StopJumping to the character
      	StopJumping();
      }
      
      
    • 效果图

      GIF 2026-10-27 13-38-23.gif

  • 功能3:存在一种可收集物品可供玩家拾取。

    • 在这个功能的实现上,主要是为了和功能2中拾取物品进行匹配,当玩家进行交互检测时如果该物体为可收集物品则执行拾取逻辑。这里为了方便我们直接创建一个Actor类的c++,并在构造函数中为其添加一个用于识别的标签(Tag),这样后续基于这个类创建的蓝图对象都将默认添加一个可收集物品的标签,十分方便。

    以下为详细完整代码:

    CollectableItem.h

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "CollectableItem.generated.h"
    
    UCLASS()
    class DUNGEON_ESCAPE2_API ACollectableItem : public AActor
    {
    	GENERATED_BODY()
    
    public:
    	// Sets default values for this actor's properties
    	ACollectableItem();
    
    protected:
    	// Called when the game starts or when spawned
    	virtual void BeginPlay() override;
    
    public:
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(EditAnywhere,BlueprintReadOnly,category="Value")
    	FString ItemName;//物品名称
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, category = "Value")
    	bool IsTaken = false;//判断是否以及被拿走
    };
    

    CollectableItem.cpp

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #include "CollectableItem.h"
    
    // Sets default values
    ACollectableItem::ACollectableItem()
    {
     	// 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;
    
    	Tags.Add("CollectableItem");//为每个基于这个c++类创建的actor都添加CollectableItem标签
    }
    
    // Called when the game starts or when spawned
    void ACollectableItem::BeginPlay()
    {
    	Super::BeginPlay();
    
    }
    
    // Called every frame
    void ACollectableItem::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    }
    
  • 功能4:存在一种类似机关的物品,当玩家使用形状射线检测时,如果拾取了特定物体,该机关将会被启动。

    • 这个功能也是匹配功能2中使用物品的场景,但在实现上却和功能3完全不一样。首先我先讲解一下具体实现逻辑:这个机关物品会挂载一个FString类型(字符串)的道具名,用于绑定该物体需要的道具名称,判断玩家是否已经拾取了该道具、一个标签用于射线检测判断是不是机关、一个触发器组件(也就是我们功能1的Trigger,用于执行相关触发逻辑)以及一个关键钥匙的静态网格体,这个钥匙的作用就是展示玩家使用或放置物品时的效果,但这个关键钥匙物品只会在玩家正确使用物品后,才会显示在场景中,并且这个关键钥匙物品是可以被重新收回的,当被收回时又会关闭显示。这些拼在一起就构成了这个功能4。
    • 所以我们需要创建一个新的Actor类c++,把这些绑定的组件全部在构造函数中直接为其添加上,后续创建不同的机关时就只需要改些参数即可。
    • 在这个c++类中我们还需要设置一个bool类型的变量对机关是否被触发进行判断,当被触发时,则调用Trigger组件让执行触发逻辑并让钥匙显示在场景中;反之,执行相反逻辑。

    以下为详细代码和效果图:

    • Lock.h

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "GameFramework/Actor.h"
      #include "Trigger.h" //触发器组件
      #include "Components/StaticMeshComponent.h" //静态网格体组件
      #include "Lock.generated.h"
      
      UCLASS()
      class DUNGEON_ESCAPE2_API ALock : public AActor
      {
      	GENERATED_BODY()
      
      public:
      	// Sets default values for this actor's properties
      	ALock();
      
      protected:
      	// Called when the game starts or when spawned
      	virtual void BeginPlay() override;
      
      public:
      	// Called every frame
      	virtual void Tick(float DeltaTime) override;
      
      	//获取私有变量IsKeyPlaced值
      	bool GetIsKeyPlaced();
      
      	//设置私有变量IsKeyPlaced值
      	void SetIsKeyPlaced(bool NewKeyPlaced);
      
      	UPROPERTY(VisibleAnywhere)
      	USceneComponent* RootComp;//场景根组件
      
      	UPROPERTY(VisibleAnywhere)
      	UTrigger* TriggerComponent;//触发器组件
      
      	UPROPERTY(VisibleAnywhere)
      	UStaticMeshComponent* KeyItemMesh;//静态网格体
      
      	UPROPERTY(EditAnywhere)
      	FString KeyItemName;//关键物品名
      
      private:
      	UPROPERTY(VisibleAnywhere)
      	bool IsKeyPlaced = false;//关键物品是否被放置
      };
      
    • Lock.cpp

      // Fill out your copyright notice in the Description page of Project Settings.
      
      #include "Lock.h"
      
      // Sets default values
      ALock::ALock()
      {
       	// 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;
      
      	Tags.Add("Lock");//添加标签
      
      	//创建默认子对象,场景组件命名为Root Comp
      	RootComp=CreateDefaultSubobject<USceneComponent>(TEXT("Root Comp"));
      	//设置为根组件
      	SetRootComponent(RootComp);
      
      	//创建默认子对象,Trigger组件命名为Trigger Comp
      	TriggerComponent = CreateDefaultSubobject<UTrigger>(TEXT("Trigger Comp"));
      	//把Trigger组件附加到场景组件RootComp上
      	TriggerComponent->SetupAttachment(RootComp);
      
      	//创建默认子对象,StaticMeshComponent组件命名为Key Item Mesh
      	KeyItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Key Item Mesh"));
      	//把静态网格体组件附加到场景组件RootComp上
      	KeyItemMesh->SetupAttachment(RootComp);
      
      }
      
      // Called when the game starts or when spawned
      void ALock::BeginPlay()
      {
      	Super::BeginPlay();
      	IsKeyPlaced = false;//更新钥匙是否放置
      	TriggerComponent->Set_Trigger(false);//更新触发器状态
      	KeyItemMesh->SetVisibility(false);//更新钥匙显示状态
      	//SetIsKeyPlaced(false);//初始设置钥匙没有被放置
      }
      
      // Called every frame
      void ALock::Tick(float DeltaTime)
      {
      	Super::Tick(DeltaTime);
      
      	//输出游戏已进行时间
      	//UE_LOG(LogTemp, Display, TEXT("Game is gone %f"), GetWorld()->TimeSeconds);
      }
      
      bool ALock::GetIsKeyPlaced()//获取IsKeyPlaced的值
      {
      	return IsKeyPlaced;
      }
      
      void ALock::SetIsKeyPlaced(bool NewKeyPlaced)//设置IsKeyPlaced的值
      {
      	if (NewKeyPlaced)
      	{
      		//触发器触发
      
      		//触发对象检测计数增加,防止多对象触发导致出错
      		//由于主角也会触发触发器,如果在已经放置钥匙的同时,角色再去触发
      		//那么将会出现触发器被关闭的情况,也就是原本已经被钥匙打开的门,在主角再次触发后,关闭了,但是钥匙依然还在上面
      		//原因:由于放置钥匙不会增加触发检测器计数,但主角触发会。当主角离开后触发检测器为0则自动触发关闭。
      		TriggerComponent->ActivatorCount++;
      	}
      	else
      	{
      		TriggerComponent->ActivatorCount--;//同理,钥匙被收回时,触发器检测器计数也不会减少
      	}
      	IsKeyPlaced = NewKeyPlaced;//更新钥匙是否放置
      	TriggerComponent->Set_Trigger(NewKeyPlaced);//更新触发器状态
      	KeyItemMesh->SetVisibility(NewKeyPlaced);//更新钥匙显示状态
      }
      
    • 效果图

      GIF_2026-10-28_17-20-40.gif

  • 个人额外功能5:触发器也可以选择进行移动。

    • 这个功能主要是有一些触发器有点类似于压力板,但物体或玩家站上去压力板却没有任何变化但机关却触发了,显得有些单调。我就给触发器添加了一个选项,用bool类型实现。如果勾选则当触发器被触发时,触发器也会进行移动,从而实现压力板的效果。当然移动逻辑和功能1是一致的。

    以下为相关代码和效果图:

    • 触发器移动核心逻辑

      Trigger.cpp
      
      if (IsSelfMove)//如果触发器自身要移动
      {
      	SelfStart = GetOwner()->GetActorLocation();//先获取自身初始位置
      	//UE_LOG(LogTemp, Warning, TEXT("OwnerName is %s"),*GetOwner()->GetActorNameOrLabel());
      	ChangeTriggerMove(false);//游戏刚开始先设置触发器目标位置
      	SelfMoveSpeed = TriggerMoveOffset.Length() / SelfMoveTime;//获取移动速度
      }
      
      void UTrigger::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction * ThisTickFunction)
      {
      	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
      	if (IsSelfMove)//如果触发器自身移动
      	{
      		FVector SelfCurrentLocation = GetOwner()->GetActorLocation();//实时获取触发器当前位置
      		SelfIsReached = SelfCurrentLocation.Equals(SelfTarget);//判断是否抵达目标位置
      		if (!SelfIsReached)
      		{
      			//未抵达目标位置,继续移动
      			FVector NewTriggerLocation = FMath::VInterpConstantTo(SelfCurrentLocation, SelfTarget, GetWorld()->GetDeltaSeconds(), SelfMoveSpeed);
      			GetOwner()->SetActorLocation(NewTriggerLocation);
      		}
      	}
      }
      
      void UTrigger::ChangeTriggerMove(bool NewTriggerMove)//触发器移动逻辑
      {
      	if (NewTriggerMove)
      	{
      		SelfTarget = SelfStart + TriggerMoveOffset;//更新目标位置
      	}
      	else
      	{
      		SelfTarget = SelfStart;//目标位置初始位置
      	}
      }
      
    • 效果图

      GIF_2026-10-27_18-08-23.gif

  • 个人额外功能6:手电筒、游戏提示文本、关卡提示文本以及退出文本的使用和显示。

    • 手电筒:按下T键开启、再次按下关闭。

      手电筒只需在Character.h中设置一个聚光源组件(USpotLightComponent)变量,随后在Character.cpp中构造函数中创建即可,之后只需在绑定函数中设置逻辑即可。

      以下为详细实现代码和效果图:

      • 手电筒核心交互逻辑

        void ADungeon_Escape2Character::FlashInteract()//手电筒使用逻辑
        {
        	if (IsFlash)
        	{
        
        		//此时手电筒正在使用
        		FlashLight->SetVisibility(false);//更新手电筒状态
        	}
        	else
        	{
        		//此时手电筒未使用
        		FlashLight->SetVisibility(true);//更新手电筒状态
        	}
        	IsFlash = !IsFlash;//更新手电筒状态
        }
        
      • 效果图

      GIF_2026-10-27_18-46-20.gif

    • 游戏提示文本:游戏开始时显示并按下TAB键显示、再次按下关闭。

      • 和手电筒相似也需要先创建一个用户控件组件(UUserWidget)变量,但不需要在构造函数中创建,这个需要在引擎中创建一个用户控件蓝图,随后直接在角色蓝图中赋值给Character中的用户控件变量即可。
      • 而让其在游戏开始时显示,由于该游戏有两个关卡,我只希望其在第二个关卡开始时显示。所以我们将其设置到对应关卡蓝图(LevelBlueprint)中进行实现即可,因为每个关卡都会有一个独立关卡蓝图,而其中执行的逻辑就是仅限于在当前关卡内,所以我觉得使用关卡蓝图来实现这个功能刚刚好。
      • 但这里的学习中只讨论C++,那么我们就需要创建一个LevelScriptActor类(它本质上是关卡蓝图在 C++ 层面的载体),所以我们只需要在这个c++类中为其添加Begin()函数并在其中调用Character.h中的游戏文本的变量为其设置相关逻辑即可。
      • 设置好相关逻辑后,最后进入到对应关卡的关卡蓝图中找到类设置,并在细节面板中把父类更改为当前c++类即可。

      以下为详细代码和效果图:

      • MainLeveScriptActor.h

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #pragma once
        
        #include "CoreMinimal.h"
        #include "Engine/LevelScriptActor.h"
        #include "MainLevelScriptActor.generated.h"
        
        /**
         *
         */
        UCLASS()
        class DUNGEON_ESCAPE2_API AMainLevelScriptActor : public ALevelScriptActor
        {
        	GENERATED_BODY()
        
        protected:
        	// Called when the game starts
        	virtual void BeginPlay() override;
        
        };
        
      • MainLeveScriptActor.cpp

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #include "MainLevelScriptActor.h"
        #include "Kismet/GameplayStatics.h"
        #include "Dungeon_Escape2Character.h"
        #include "Blueprint/UserWidget.h"//用户控件组件
        
        void AMainLevelScriptActor::BeginPlay()
        {
        	Super::BeginPlay();
        	if (GetWorld())  //获取当前世界
        	{
        		//UGameplayStatics::GetPlayerCharacter:用于快速获取指定玩家索引所控制的 ACharacter 对象。
        		// 它本质上是蓝图节点 “Get Player Character” 的 C++ 等价物。
        		//const UObject* WorldContextObject:世界上下文对象,用于确定函数在哪个世界(World)中执行。通常直接传入 this(在 Actor 或 Component 中)或 GetWorld()。
        		//int32 PlayerIndex:玩家索引,指定要获取哪位玩家的角色。0 通常代表第一个本地玩家。索引顺序为:先本地玩家,然后是远程玩家。
        		ACharacter* MyBase = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
        
        		if (ADungeon_Escape2Character* Player = Cast<ADungeon_Escape2Character>(MyBase))//转化为Dungeon_Escape2Character.h
        		{
        			//转化成功
        
        			//将其提示文本添加到视口
        			Player->TipsWidght->AddToViewport();
        			//更新提示文本显示状态
        			Player->IsShowTips = false;
        		}
        	}
        
        }
        
      • 提示文本交互核心逻辑

        //提示文本交互函数
        
        void ADungeon_Escape2Character::TipsInteract()//提示文本交互函数
        {
        	if (TipsWidght)//如果游戏提示文本不为空
        	{
        		if(IsShowTips)
        		{
        			//处于显示状态
        			ShowTipsWidget();//游戏提示文本ui设置为可见
        		}
        		else
        		{
        			//处于关闭状态
        			HiddenTipsWidget();//游戏提示文本ui设置为隐藏
        		}
        			IsShowTips = !IsShowTips;//更新游戏提示文本显示状态
        	}
        }
        
      • 效果图

      GIF_2026-10-27_18-50-35.gif

    • 退出文本:按下ESC键显示。

      • 这里按键的绑定就不再做过多赘述,参照功能2即可。
      • 具体逻辑也和以上两个类似,但这里面会多两个按绑定行退出游戏和关闭退出文本的显示的功能,这些逻辑主要在用户控件蓝图中进行,c++代码中不进行这些相关逻辑的实现。

      详细代码和效果图如下:

      • 退出文本交互核心逻辑

        void ADungeon_Escape2Character::ExitInteract()//游戏退出交互函数
        {
        	if (ExitWidght)//如果退出文本不为空
        	{
        		if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
        		{
        			//成功获取玩家控制器
        			PC->bShowMouseCursor=true;//显示鼠标
        			FInputModeUIOnly Mode;//只读取UI输入
        			PC->SetInputMode(Mode);//设置游戏模式为只读取UI输入
        			ExitWidght->SetVisibility(ESlateVisibility::Visible);//退出ui设置为可见
        		}
        
        	}
        }
        
      • 效果图

      GIF_2026-10-28_12-55-52.gif

    • 关卡提示文本:走到对应物品或机关处,显示相关文本提示并实时面向玩家,并在某些物品拿走或机关触发后不再显示文本(摧毁该Actor)。

      • 具体实现逻辑为:我们新创建一个Actor类型的C++,在其中创建一个文本渲染组件进行(UTextRenderComponent)用于对文本的展示,其次创建一个触发器组件(BoxComponent)用于对玩家进行检测,只有当玩家走进触发器范围才会显示,离开触发器就关闭显示。

      • 而实时面向玩家的逻辑则是与物体移动的逻辑相似,只是把位移改成了旋转量。首先需要实时获取文本当前位置以及目标位置(由于我们希望的是文本看向的是玩家头部也就是镜头,所以这里也就是摄像机位置),随后通过一个函数UkismetMathLibrary::FindLookAtRotation(),用于实时获取文本看向目标位置所需要的旋转量。

        UKismetMathLibrary::FindLookAtRotation():计算一个物体从 Start 位置看向 Target 位置时所需要的旋转量
        
        函数参数:
        const FVector& Start:观察者的世界位置(物体的当前位置)
        const FVector& Target:目标点的世界位置(物体想要看向的位置)
        
      • 随后实时获取文本当前旋转量,通过一个FMath::RInterpTo()函数丝滑转向目标,这个函数其实和我们用于物体移动的那个函数 FMath::VInterpConstantTo()相似,都是使用插值运算丝滑过渡到目标位置或方向,但前者是先快后慢(起步时很快,越接近目标速度越慢,最终平滑停止),而后者是匀速。

        Fmath::RInterpTo():用于实现旋转平滑插值,能让物体以“快进慢出”的丝滑效果,从当前旋转逐渐过渡到目标旋转,而不是瞬间跳转。
        
        函数参数:
        const FRotator& Current:当前旋转值
        const FRotator& Target:目标旋转值
        float DeltaTime:帧间时间
        float InterpSpeed:插值速度(值越大越快)
        
      • 而在让文本不再显示其实很简单,直接摧毁即可并且这样还减轻了游戏内存压力,但该如何让其触发摧毁才是重点要解决的问题。

      • 在我的游戏设计当中其实需要被销毁的文本其实分两类:一类是必定会销毁的也就是可收集物品类(CollectableItem),因为这类物品只能拾取且不能被放回,所以只要被拾取了就不会再被触发了,这时该物品的关卡提示文本也没有什么作用了,直接摧毁即可。第二类呢就是功能4中的类似机关的物品(Lock),这类就是玩家可触发使用物品的功能,但这类物品存在可以回收物品的功能,所以部分这类物品的关卡提示文本不能删除,但也有少部分这类物品的使用是一次性的所以需要摧毁。

      • 既然已明白整体分类,那么在实现上那就存在不同的方法,对于必定会被销毁的文本,我们直接在该c++的Begin()函数中直接获取该类对象(但在详细代码中我将其写到了Tick函数中,因为我当时觉得可能在游戏开始的时候还没有创建好,可能无法准确获取。但后来才发现其实没有什么影响,都是静态放置的物品,又不是动态生成的。建议把其写到Begin函数中,我太懒了就保持这样吧~),我们这里通过GetOverlappingActors()函数直接获取与我们自身重叠的actor,

        AActor::GetOverlappingActors:能立刻告诉你当前有哪些 Actor 或组件与你的触发区域(只要该 Actor 的任意组件与另一 Actor 的任意组件重叠,即视为重叠)发生了重叠。
        
        函数参数:
        TArray<AActor*>& OverlappingActors:一个 TArray<AActor*> 的引用,函数会把所有检测到的重叠 Actor 填充到这个数组中。
        TSubclassOf<AActor> ClassFilter = nullptr:一个类型过滤器,用于只返回特定类或其子类的 Actor。这是提升性能和代码简洁性的关键。
        

        而这里主要是与我们BoxComponent组件重叠的物体,因为这个组件在场景中是可以编辑大小的,我们只要把可收集物品放置在该BoxComponent的检测范围中即可。当然如果为了精确可以使用UPrimitiveComponent::GetOverlappingActors()函数,该函数作用为:将重叠查询精确到单个组件级别的核心函数。与 AActor 版本不同,它只关心当前这一个碰撞组件与哪些 Actor 发生了重叠。这里由于我们创建的是继承Actor类的c++,所以为了方便不多写一个头文件,直接就调用了GetOverlappingActors()这个函数,有兴趣可以去试试另一个更加精准的函数。

      • 在成功获取可收集物品类的物品信息后,我们需要实时获取其中的一个bool类型变量(IsTaken)信息即可,这个信息表示就是该物品是否以已经被拿走,如果拿走就执行文本摧毁逻辑。

      • 在摧毁逻辑这里我们采用了SetLifeSpan()函数来实现。

        AActor::SetLifeSpan():延时销毁自己。
        
        函数参数:
        float InLifespan:表示 Actor 从当前时刻起还能存活多久。一旦计时结束,Actor 就会自动销毁。当传入 0 时,会产生一个特殊行为:清除计时器,取消销毁计划,Actor 将不会因这个计时器而被销毁。
        
      • 而对于另一类存在部分需要摧毁关卡提示文本的类型,我们将其创建为一个可以在编辑器中任意编辑的AActor指针变量,如果某机关的文本需要被摧毁,则手动绑定该文本。并且在关卡文本c++类的Begin函数中判断是否获取成功,如果获取成功则证明该机关的文本需要在该机关被触发时被摧毁,具体的实现逻辑就和第一类的摧毁逻辑相似了。如果没有获取成功,则证明当前关卡不需要文本被摧毁。

      以下为详细代码和效果图:

      • TipsTextTrigger.h

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #pragma once
        
        #include "CoreMinimal.h"
        #include "GameFramework/Actor.h"
        #include "Components/TextRenderComponent.h"   // 文本渲染组件
        #include "Components/BoxComponent.h" //盒状组件
        #include "CollectableItem.h"//CollectableItem组件
        #include "Lock.h"//Lock组件
        #include "TipsTextTrigger.generated.h"
        
        UCLASS()
        class DUNGEON_ESCAPE2_API ATipsTextTrigger : public AActor
        {
        	GENERATED_BODY()
        
        public:
        	// Sets default values for this actor's properties
        	ATipsTextTrigger();
        
        protected:
        	// Called when the game starts or when spawned
        	virtual void BeginPlay() override;
        
        	//Actor 即将被销毁、关卡流送卸载、或者游戏结束时被调用。
        	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
        public:
        	// Called every frame
        	virtual void Tick(float DeltaTime) override;
        
        	UPROPERTY(EditAnywhere, Category = "TEXT_Trigger")
        	float RotationSpeed = 15.0f;//旋转速度
        
        	UPROPERTY(EditAnywhere, Category = "TEXT_Trigger")
        	float DestroyDelayTime = 2.0f;//摧毁时间
        
        	UPROPERTY(EditAnywhere, Category = "TEXT_Trigger")
        	ALock* LockItem;//锁组件
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	USceneComponent* RootComp;//根节点
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	UBoxComponent* TriggerBoxComp;//盒状碰撞体组件
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	UTextRenderComponent* TipText;//文本提示
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	bool TextIsShowed = false;//文本是否处于显示状态
        
        	UPROPERTY(VisibleAnywhere,Category="TEXT_Trigger")
        	bool DoOnce = false;//判断是否以及执行了一次
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	TArray<AActor*>OverLapActor;//获取重叠atcor
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	ACollectableItem* Collect_Data;//获取Collectable的相关信息
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	FTimerHandle DestroyTimerHandle;//一个用于物体摧毁的定时器
        
        	UPROPERTY(VisibleAnywhere, Category = "TEXT_Trigger")
        	bool IsDestorying = false;//判断是否正在被摧毁
        
        	//开始重叠的处理函数
        	UFUNCTION()
        	void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
        		UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
        		bool bFromSweep, const FHitResult& SweepResult);
        
        	// 结束重叠的处理函数
        	UFUNCTION()
        	void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
        		UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
        
        };
        
      • TipsTextTrigger.cpp

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #include "TipsTextTrigger.h"
        #include "Kismet/KismetMathLibrary.h"//UKismetMathLibrary函数访问路径
        #include "Math/UnrealMathUtility.h" //FMath函数访问路径
        #include "CollectableItem.h"//CollectableItem访问头文件
        #include "TimerManager.h"//延时函数需要头文件
        
        // Sets default values
        ATipsTextTrigger::ATipsTextTrigger()
        {
         	// 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;
        
            //创建根节点组件
            RootComp = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
        
            //更新根节点
            RootComponent = RootComp;
        
            //创建盒状组件
            TriggerBoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("TextTrigger"));
        
            //将盒状组件绑定到根节点下
            TriggerBoxComp->SetupAttachment(RootComp);
        
            //开启TriggerBox的重叠检测事件
            TriggerBoxComp->SetGenerateOverlapEvents(true);
        
            //创建一个文本渲染组件
            TipText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TipsText"));
        
            //附加到根组件
            TipText->SetupAttachment(RootComp);
        
            //给盒状触发器绑定重叠事件函数
            TriggerBoxComp->OnComponentBeginOverlap.AddDynamic(this, &ATipsTextTrigger::OnOverlapBegin);
            TriggerBoxComp->OnComponentEndOverlap.AddDynamic(this, &ATipsTextTrigger::OnOverlapEnd);
        
        }
        
        // Called when the game starts or when spawned
        void ATipsTextTrigger::BeginPlay()
        {
        	Super::BeginPlay();
        
            // 默认不显示文本
            TipText->SetVisibility(TextIsShowed);
        }
        
        // Called every frame
        void ATipsTextTrigger::Tick(float DeltaTime)
        {
        	Super::Tick(DeltaTime);
        
            //由于该判断语句中存在SetTimer语句,需要进行语句拦截,防止setTimer被一直调用而重置时间,导致无法执行后续语句逻辑
            if (!IsDestorying)
            {
                if (GetWorld()&& Collect_Data && Collect_Data->IsTaken)//如果可收集物品数据不为空,并且它它已经被拿走了
                {
                    IsDestorying = true;//更新摧毁状态
        
                    //SetLifeSpan()延时销毁自己。和SetTimer同样效果。
                    //优势:
                    //1.自动清理:Actor 被销毁时,LifeSpan 定时器会被引擎自动收回,绝不会引发内存问题。
                    //2.不需要 Handle:无需声明 FTimerHandle。
                    //3.无需 Lambda:不会再遇到捕获 this 导致野指针崩溃的问题。
                    this->SetLifeSpan(DestroyDelayTime);
        
        #pragma region SetTimer
                    ////UE_LOG(LogTemp, Display, TEXT("Destroying!!!"));
                    ////如果成功获取CollectableItem组件actor,并且该actor已经被拾取且未处于正在销毁状态,那就延时摧毁文本提示actor
                    ////SetTimer():实现延时或重复执行逻辑的核心函数
                    ////FTimerHandle& InOutHandle:它是你之后对定时器进行暂停(PauseTimer)、恢复(UnPauseTimer)、清除(ClearTimer)或查询状态(IsTimerActive)的唯一凭证。
                    ////TFunction<void(void)>&& Callback:指定当定时器到期时要执行的具体内容
                    ////float InRate:时间间隔
                    ////bool InbLoop:是否循环
                    ////float InFirstDelay:首次延迟
                    //GetWorld()->GetTimerManager().SetTimer(
                    //    DestroyTimerHandle,//定时器
                    //    [this]() {this->Destroy(); },//延时执行逻辑
                    //    DestroyDelayTime,//摧毁延时时间
                    //    false//不循环
                    //);
        #pragma endregion
        
                }
        
                else if (LockItem && LockItem->GetIsKeyPlaced())//如果LockItem不为空,并且已经被放置了,那么执行摧毁
                {
                    IsDestorying = true;//更新摧毁状态
                    //SetLifeSpan()延时销毁自己。和SetTimer同样效果。
                    //优势:
                    //1.自动清理:Actor 被销毁时,LifeSpan 定时器会被引擎自动收回,绝不会引发内存问题。
                    //2.不需要 Handle:无需声明 FTimerHandle。
                    //3.无需 Lambda:不会再遇到捕获 this 导致野指针崩溃的问题。
                    this->SetLifeSpan(DestroyDelayTime);
                }
        
            }
            if (!DoOnce)//只执行一次
            {
                DoOnce = true;
                //AActor::GetOverlappingActors():能立刻告诉你当前有哪些 Actor 或组件与你的触发区域发生了重叠。
                //TArray<AActor*>& OverlappingActors,:一个 TArray<AActor*> 的引用,函数会把所有检测到的重叠 Actor 填充到这个数组中。
                //TSubclassOf<AActor> ClassFilter = nullptr:一个类型过滤器,用于只返回特定类或其子类的 Actor。这是提升性能和代码简洁性的关键。
                GetOverlappingActors(OverLapActor,ACollectableItem::StaticClass());
        
                for (auto Actor:OverLapActor)//遍历重叠的actor
                {
                    ACollectableItem* Collect = Cast<ACollectableItem>(Actor);//转换为CollectableItem
                    if (Collect)//如果成功转换
                    {
                        Collect_Data = Collect;
                        //UE_LOG(LogTemp, Display, TEXT("Successfully Find CollectableItem ,Name is %s"), *Collect->GetActorNameOrLabel());
                    }
                }
            }
            if (TextIsShowed)//如果文本当前处于显示状态
            {
                //文本正在显示
        
                //获取玩家控制器
                APlayerController* Player = GetWorld()->GetFirstPlayerController();
        
                //判断角色控制器是否成功获取并且玩家是否有效
                //因为PlayerController存在并不代表它当前一定拥有一个Pawn。GetPawn()完全可能返回nullptr,如果直接解引用就会导致空指针崩溃。
                //存在的可能性:
                //1.玩家死亡后:Pawn 被销毁,但 PlayerController 仍然存在,等待重生。
                //2.游戏刚开始时PlayerController已创建,但Pawn尚未生成或尚未 Possess。
                //3. 观战模式:PlayerController 没有 Possess 任何 Pawn,只是观察者。
                //4.切换 Pawn 的过程中:旧 Pawn 已销毁,新 Pawn 还未 Possess。
                //5.某些自定义游戏模式:玩家可能以“无实体”形式存在,只控制 UI 或相机。
                if (Player && Player->GetPawn())
                {
                    //获取相机位置
                    FVector CameraLocation = Player->PlayerCameraManager->GetCameraLocation();
        
                    //获取文本位置
                    FVector TextLocation = TipText->GetComponentLocation();
        
                    //使用UKismetMathLibrary::FindLookAtRotation函数得到文本需要看向的目标方向
                    //UKismetMathLibrary::FindLookAtRotation:计算一个物体从 Start 位置看向 Target 位置时所需要的旋转量
                    //const FVector& Start:观察者的世界位置(物体的当前位置)
                    //const FVector& Target:目标点的世界位置(物体想要看向的位置)
                    FRotator TargetRotation = UKismetMathLibrary::FindLookAtRotation(TextLocation, CameraLocation);
        
                    //防止文本翻转(也就是绕X轴旋转),只保留Yaw(绕Z轴旋转)和Pitch(绕Y轴旋转)
                    //TargetRotation.Roll = 0.0f;
                    //TargetRotation.Pitch = 0.0f;
        
                    //获取当前文本旋转量
                    FRotator CurrentRotation = TipText->GetComponentRotation();
        
                    //使用Fmath::RInterpTo()函数,获取当前旋转到达目标旋转的插值量
                    //Fmath::RInterpTo():用于实现旋转平滑插值,能让物体以“快进慢出”的丝滑效果,从当前旋转逐渐过渡到目标旋转,而不是瞬间跳转。
                    //const FRotator& Current:当前旋转值
                    //const FRotator& Target:目标旋转值
                    //float DeltaTime:帧间时间
                    //float InterpSpeed:插值速度(值越大越快)
                    FRotator NewRotation = FMath::RInterpTo(CurrentRotation, TargetRotation, DeltaTime, RotationSpeed);
        
                    //更新文本旋转量
                    TipText->SetWorldRotation(NewRotation);
        
                }
            }
        }
        
        void ATipsTextTrigger::EndPlay(const EEndPlayReason::Type EndPlayReason)
        {
            //必须调用 Super,否则父类逻辑(如组件清理、网络清理)不会执行
            Super::EndPlay(EndPlayReason);
        
            if (GetWorld())
            {
                //清除定时器
                GetWorld()->GetTimerManager().ClearTimer(DestroyTimerHandle);
            }
        }
        
        void ATipsTextTrigger::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
        {
            if (OtherActor && OtherActor->ActorHasTag("Player"))
            {
                // 显示文本
                TextIsShowed = true;
                TipText->SetVisibility(true);
            }
        }
        
        void ATipsTextTrigger::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
        {
            if (OtherActor && OtherActor->ActorHasTag("Player"))
            {
                // 隐藏文本
                TextIsShowed = false;
                TipText->SetVisibility(false);
            }
        }
        
      • 效果图

      GIF_2026-10-28_16-54-18.gif

      GIF_2026-10-28_16-57-24.gif

  • 个人额外功能7:胜利文本。

    • 胜利文本:玩家通关后展示的文本,并展示其通关时间。

      • 胜利文本的展示处于游戏通关时才会触发,而触发该逻辑则是当玩家抵达某一位置是就会触发,所以这里将使用触发器进行实现,我们创建一个TriggerBox类的c++,这个TriggerBox之前在功能1中讲到过,在其身上挂载场景物体时,在场景中将不会显示,这里就刚好合适。但又有一个问题就是,这里不是要显示胜利文本吗?那这不是就没法显示吗?这里我们不会在其身上挂载任何东西,那个胜利文本我们是设置为EditAnywhere的用户控件变量在编辑器中自行添加,所以并不会受影响。在这个c++类中我们会在begin函数中将这个胜利文本添加到视口并隐藏,然后在触发逻辑中开启显示即可。

      • 但怎么才能让其只触发一次呢?这个很简单只需要关闭触发器检测的GenerateOverlapEvents(生成重叠事件),所有的碰撞检测都是基于这个产生的,只需关闭即可不会再触发。

      • 所以整体逻辑就已经很明了了,在这个c++类中先为其添加碰撞检测事件,只需要一个开始检测函数即可OnOverlapBegin(),当被触发后,显示胜利文本,并关闭生成重叠事件。

      • 显示通关时间则需要一个double类型变量(StartTime)在begin函数中使用GetTimeSeconds()函数记录游戏开始时间,然后在触发器被触发时使用用当前时间减去StartTime得到游戏通关用时。

        GetTimeSeconds()【UWorld 类的成员函数】 :虚幻引擎中用于获取游戏世界已运行时间的核心函数。它返回的是“游戏内时间”,而非现实世界的物理时钟时间,因此它的推进节奏会受到游戏暂停和“时间膨胀(Time Dilation)”效果的影响。
        
      • 在记录具体时间后,我们需要将其转换为具体的时分秒,这里将用到一个结构体FTimeSpan,其是 Unreal Engine(UE)中用于表示时间间隔(一段时间的长度)的核心结构体。我们创建一个具体时间为我们刚记录好的FTimeSpan的结构体,随后通过其内函数获取所有我们需要的值。最后使用FString::Printf()函数把其转换为我们想要的FString类型格式。

        FTimespan:是虚幻引擎中用于表示时间间隔或时长的核心结构体。它与表示“时间点”的 FDateTime 是互补关系,一个回答“多久”,一个回答“何时”。
        
        部分函数
        GetDays():完整的整天数
        GetHours():0 ~ 23 的小时数(天数的余数)
        GetMinutes():0 ~ 59 的分钟数
        GetSeconds():0 ~ 59 的秒数
        GetFractionMilli():毫秒部分。
        
        FString::Printf():是虚幻引擎中用于构建格式化字符串的核心函数,其行为等同于经典的 C++ sprintf,但返回一个安全的 FString 对象。它是 C++ 中进行调试输出、日志记录和运行时字符串拼接时最常用的工具之一。
        
      • 最后虽然我们得到我们想要格式的通关时间的字符串,但在胜利文本上怎么设置呢?我们肯定需要在胜利文本控件中找到对应的通关时间文本控件,随后把这个我们得到的通关时间字符串给设置上去。这里我将设置一个由蓝图去实现的函数,回到头文件中创建一个Void类型的函数(ShowPassTime),并给其添加一个FString类型的参数,这个参数就是用来传递通关时间的,随后把这个函数用UFUNCTION(BlueprintImplementableEvent)宏设置一下相关属性,这个的意思就是c++无需实现该函数的具体逻辑,由蓝图去进行实现。我们在触发胜利文本逻辑中调用这个函数并把胜利时间传递进去即可,随后在蓝图中实现具体的设置以及显示逻辑。当然这里也可以在c++中进行实现,代码中也展示了如何在c++中进行文本的设置(在注释中),但由于后续还有许多UI的设置在c++中不方便,我就直接全在蓝图中实现了,该兴趣可以自己在c++中自行设计,这里就不多赘述了。

      以下为详细代码和效果图:

      • TextTrigger.h

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #pragma once
        
        #include "CoreMinimal.h"
        #include "Engine/TriggerBox.h"
        #include "Components/TextRenderComponent.h"   // 文本渲染组件
        #include "TextTrigger.generated.h"
        
        /**
         *
         */
        UCLASS()
        class DUNGEON_ESCAPE2_API ATextTrigger : public ATriggerBox
        {
        	GENERATED_BODY()
        
        public:
        	// Sets default values for this component's properties
        	ATextTrigger();//构造函数
        
        protected:
        	// Called when the game starts
        	virtual void BeginPlay() override;
        
        public:
        	virtual void Tick(float DeltaTime) override;
        
        	UPROPERTY(EditAnywhere,BlueprintReadWrite)
        	UUserWidget* VectoryWidget;//胜利ui
        
        	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
        	double StartTime;//开始时间
        
        	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
        	FString  VectoryTime = {"00:00:00:00"};//通关用时
        
        	//开始重叠的处理函数
        	UFUNCTION()
        	void OnOverlapBegin(AActor * OverlappedActor, AActor * OtherActor);
        
        	//BlueprintImplementableEvent:表示c++无需实现该函数逻辑,由蓝图去实现
        	UFUNCTION(BlueprintImplementableEvent, Category = "Time")
        	void ShowPassTime(const FString& FinalPassTime);//传递最终通关时间,并执行后续逻辑
        };
        
      • TextTrigger.cpp

        // Fill out your copyright notice in the Description page of Project Settings.
        
        #include "TextTrigger.h"
        #include "Kismet/KismetMathLibrary.h"//UKismetMathLibrary函数访问路径
        #include "Math/UnrealMathUtility.h" //FMath函数访问路径
        #include "Components/BoxComponent.h"//BoxComponent组件
        #include "Blueprint/UserWidget.h"//用户控件组件
        #include "Components/EditableTextBox.h"//可编辑文本框组件
        #include "Blueprint/WidgetTree.h"
        
        ATextTrigger::ATextTrigger()
        {
        
            //绑定OnActorBeginOverlap事件
            OnActorBeginOverlap.AddDynamic(this, &ATextTrigger::OnOverlapBegin);
        
        }
        
        void ATextTrigger::BeginPlay()
        {
            Super::BeginPlay();
            if (GetWorld())
            {
                //获取开始时间
                StartTime = GetWorld()->GetTimeSeconds();
            }
            //游戏开始先把胜利ui添加到视口,并且折叠起来
            VectoryWidget->AddToViewport();
            //ESlateVisibility::Collapsed:隐藏且不占空间,布局会像它从未存在过一样重新排布
            //ESlateVisibility::Visible:默认状态,正常显示且可交互
            //ESlateVisibility::HitTestInvisible:自己可见但自身不响应点击,子控件仍可响应
            //ESlateVisibility::SelfHitTestInvisible:自己可见但自身和所有子控件都不响应点击
            //ESlateVisibility::Hidden:隐藏但保留布局占位,其他控件位置不因此变动
            VectoryWidget->SetVisibility(ESlateVisibility::Collapsed);
        
        }
        
        void ATextTrigger::Tick(float DeltaTime)
        {
        
        }
        void ATextTrigger::OnOverlapBegin(AActor* OverlappedActor, AActor* OtherActor)
        {
            if (GetWorld()&&OtherActor->ActorHasTag("Player"))
            {
                //获取通关用时
                double VecTime = GetWorld()->GetTimeSeconds() - StartTime;
                //UE_LOG(LogTemp, Error, TEXT("VecTime is %f"), VecTime);
        
                //FTimespan 是 Unreal Engine(UE)中用于表示时间间隔(一段时间的长度)的核心结构体。
                FTimespan VectoryTimeSpan=FTimespan::FromSeconds(VecTime);//创建一个时间间隔为VecTime的FTimespan
                int32 H = VectoryTimeSpan.GetHours();//获取小时数
                int32 M = VectoryTimeSpan.GetMinutes();//获取分钟数
                int32 S = VectoryTimeSpan.GetSeconds();//获取秒数
                int32 Ms = VectoryTimeSpan.GetFractionMilli();//获取毫秒数
                //VectoryTime = VectoryTimeSpan.ToString(TEXT("%h:%m:%s:%f"));//时间格式的转换
                VectoryTime = FString::Printf(TEXT("%02d:%02d:%02d:%02d"), H, M, S,Ms);//时间格式的转换
               // UE_LOG(LogTemp, Error, TEXT("vectoryTime is %s"),*VectoryTime);
                ShowPassTime(VectoryTime);//传递最终通关时间给蓝图,并在蓝图中实现后续逻辑
        
        #pragma region SetText
        
                ////在c++中设置胜利文本
                ////先通过控件名字找到对应控件
                //if (UWidget* FindVectotText = VectoryWidget->GetWidgetFromName(TEXT("Time")))
                //{
                //    //UE_LOG(LogTemp, Display, TEXT("Find Time"));
                //    //UE_LOG(LogTemp, Warning, TEXT("找到了 Time 控件!它的真实类名是: %s"), *FindVectotText->GetClass()->GetName());
                //    //if (VectoryWidget && VectoryWidget->WidgetTree)
                //    //{
                //    //    // 打印出这个UI里所有控件的名字,看看"Time"到底在不在
                //    //    VectoryWidget->WidgetTree->ForEachWidget([](UWidget* Widget) {
                //    //        if (Widget)
                //    //        {
                //    //            UE_LOG(LogTemp, Log, TEXT("Widget Name: %s"), *Widget->GetName());
                //    //        }
                //    //        });
                //    //}
                //
                //    //将其转换为 UEditableTextBox (文本框)类型
                //    if (UEditableTextBox* PassTime = Cast<UEditableTextBox>(FindVectotText))
                //    {
                //        //随后更改其文本
                //        PassTime->SetText(FText::FromString(VectoryTime));
                //        //显示胜利文本
                //        VectoryWidget->SetVisibility(ESlateVisibility::Visible);
        
                //    }
                //}
        
        #pragma endregion
        
                //关闭重叠事件
                if (GetCollisionComponent())
                {
                    //(只关闭重叠事件的广播(不执行 BeginOverlap 等逻辑)。)
                    GetCollisionComponent()->SetGenerateOverlapEvents(false);
                }
        
                ////也可以实现关闭重叠事件(彻底关闭该 Actor 及其所有子组件的碰撞。)
                //this->SetActorEnableCollision(false);
            }
        
        }
        
      • 效果图

      GIF_2026-10-29_13-10-29.gif

注:个人还添加了背景音乐、主菜单、场景切换等功能,这些主要使用蓝图实现就不过多赘述,该学习只对UEC++进行讨论。