章节介绍:
我们需要用一个组件控制场景中物体的移动;
步骤1: 创建组件类
创建角色组件类, 继承自:Actor Component类, 命名为Mover

步骤2: 使用组件类
使用组件有3种主要方法:
1. 直接选中场景中的物体后, 就可以点击Add搜索添加Mover组件


2. 在蓝图中也可添加:
C++ "看不见" 它, 如果你想在 C++ 里调用这个组件的功能,你不能直接调用。

3.在其他的C++类代码中调用, 在蓝图中只需挂载就行
cpp
#include "Mover.h"
class AXXX : public AActor
{
UPROPERTY(EditAnywhere)
UMover* MoverComp;
};
我是不是还需要再定义一个TSubclassOf<UMover> MoverClass来存放类型, 不然无法构造组件?
你不需要!
因为在 C++ 的构造函数中创建组件时,我们通常是在**编译时(Compile Time)**就已经确定了要用哪个 C++ 类, 这个东西是需要写死的
请看这行代码的奥妙:
cpp// 尖括号 <UMover> 就是在告诉编译器类型 MoverComp = CreateDefaultSubobject<UMover>(TEXT("Mover"));
<UMover> (模板参数):这直接告诉编译器:"我要造一个 UMover 类型的对象"。编译器看到这一行,就会去分配 UMover 类所需的内存大小
不需要变量存类型:因为类型已经"写死"在尖括号里了
什么时候才需要"定义一个变量来存放类型"?
只有当你希望**"让策划在编辑器里选择要用哪个子类"**的时候,才需要定义类型变量(TSubclassOf)。
这种情况通常发生在:
生成 Actor (SpawnActor):比如发射子弹。子弹有普通的、火焰的、冰冻的,你不知道要生成哪种,所以留一个 TSubclassOf<ABullet> 让策划选。
创建 UI (CreateWidget):比如显示血条。C++ 不知道你的 UI 蓝图长什么样,所以留一个变量让策划选。
对比一下你就明白了
场景 A:创建组件(你的情况)
特点:组件是身体的一部分,通常比较固定。
做法:直接写死 C++ 类型。
cpp// .h UPROPERTY(VisibleAnywhere) UMover* MoverComp; // 只要存实例指针 // .cpp MoverComp = CreateDefaultSubobject<UMover>(TEXT("Mover")); // 类型写在尖括号里场景 B:动态生成物品(比如生成子弹)
特点:不知道具体要生成哪种蓝图子类。
做法:定义一个类型变量让蓝图选。
cpp// .h UPROPERTY(EditAnywhere) TSubclassOf<AActor> BulletClass; // 1. 这是一个变量,用来存"类型" // .cpp // 2. 使用这个变量里存的类型来生成 GetWorld()->SpawnActor<AActor>(BulletClass, ...);
步骤3: 写组件C++逻辑代码
cpp
//Mover.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Mover.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DUNGEONESCAPE_API UMover : public UActorComponent
{
GENERATED_BODY()
public:
UMover();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
// 移动偏移量:控制Actor移动的方向和距离(如X=1000表示沿X轴移动1000单位)
UPROPERTY(EditAnywhere)
FVector MoveOffset;
// 移动总时长:Actor从起点到终点的耗时(单位:秒)
// 作用:保证移动速度均匀,不管偏移量多大,都在设定时间内完成
UPROPERTY(EditAnywhere)
float MoveTime = 4.0f;
// 移动开关:控制Actor是否执行移动(true=移动,false=返回初始位置)
// - EditAnywhere:编辑器可编辑,默认值false(开局不移动)
UPROPERTY(EditAnywhere)
bool ShouldMove = false;
// 到达目标标记:显示Actor是否已移动到目标位置(仅可读,不可编辑)
// - VisibleAnywhere:在编辑器细节面板中可见(能看状态,但不能改)
UPROPERTY(VisibleAnywhere)
bool ReachedTarget = false;
// 初始位置:Actor的起始位置(BeginPlay时记录,无需暴露给编辑器)
FVector TargetLocation;
FVector StartLocation;
};
cpp
//Mover.cpp
#include "Math/UnrealMathUtility.h"
#include "Mover.h"
UMover::UMover()
{
PrimaryComponentTick.bCanEverTick = true;
}
// 组件开始播放函数:关卡启动时执行一次,初始化核心变量
void UMover::BeginPlay()
{
Super::BeginPlay();
// 记录Actor的初始位置(组件所属Actor的世界坐标)
// GetOwner():获取当前组件附着的Actor(比如门、平台等需要移动的物体)
StartLocation = GetOwner()->GetActorLocation();
// 强制初始化移动开关为关闭状态(确保Actor开局不移动)
ShouldMove = false;
// 初始化目标位置为初始位置(Actor开局停在初始点)
TargetLocation = StartLocation;
}
// 组件Tick函数:每帧执行,核心逻辑是根据ShouldMove状态更新Actor位置
// - DeltaTime:帧间隔时间(秒),用于帧率补偿(保证不同帧率下移动速度一致)
// - TickType:Tick类型(引擎内部使用,新手无需关注)
// - ThisTickFunction:Tick函数的内部参数(新手无需关注)
void UMover::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// 第一步:根据ShouldMove状态确定目标位置
if (ShouldMove == true)
{
// 开启移动:目标位置 = 初始位置 + 偏移量(MoveOffset是预设的移动距离/方向)
TargetLocation = StartLocation + MoveOffset;
}
else if (ShouldMove == false)
{
// 关闭移动:目标位置 = 初始位置(回到起点)
TargetLocation = StartLocation;
}
// 第二步:获取Actor当前的世界位置
FVector CurrentLocation = GetOwner()->GetActorLocation();
// 第三步:判断Actor是否已到达目标位置(Equals默认误差2cm,避免浮点精度问题)
// ReachedTarget=true → 已到目标,停止移动;false → 未到,继续插值移动
ReachedTarget = CurrentLocation.Equals(TargetLocation);
// 第四步:未到达目标位置时,执行平滑插值移动
if (ReachedTarget == false)
{
// 计算移动速度:总偏移长度 / 移动总时间(保证移动耗时固定,不受帧率影响)
// MoveOffset.Length():获取移动偏移的总距离(比如X轴移1000,长度就是1000)
// MoveTime:预设的移动总时长(比如2秒,Actor会在2秒内从起点到终点)
float Speed = MoveOffset.Length() / MoveTime;
// 匀速插值计算新位置(VInterpConstantTo:固定速度插值,移动更平滑)
// 参数:当前位置、目标位置、帧间隔时间、移动速度
FVector NewLocation = FMath::VInterpConstantTo(CurrentLocation,
TargetLocation, DeltaTime, Speed);
// 更新Actor的世界位置(核心操作:让Actor移动)
GetOwner()->SetActorLocation(NewLocation);
}
}