UE4オンラインラーニング
ゲームプレイのコンポーネントを分解する
を受講した際のおぼえ書きです。
https://learn.unrealengine.com/course/3762527
ActorComponentの作り方
・コンテンツブラウザで
C++フォルダで右クリック>新規C++クラスを選択>ActorComponentを選択
することで作成することができます。

・クラス名を入力して、クラスを作成

・エディタから、C++のヘッダとソース作成に成功しました

・ソースを記述し、ビルド実行
ヘッダーファイル
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "FloatingComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class COMPONENTSOFGAMEPLAY_API UFloatingComponent : public UActorComponent
{
GENERATED_BODY()
public:
UFloatingComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Floating")
FVector FloatRange = FVector(0,0,0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Floating")
float SpeedMultiplier = 1.0f;
private :
UPROPERTY()
AActor* Parent = nullptr;
FVector StartLocation;
};
ソースファイル
#include "FloatingComponent.h"
UFloatingComponent::UFloatingComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UFloatingComponent::BeginPlay()
{
Super::BeginPlay();
Parent = GetOwner();
if (Parent)
{
StartLocation = Parent->GetActorLocation();
Parent->GetRootComponent()->SetMobility(EComponentMobility::Movable);
}
}
void UFloatingComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
const float Time = GetWorld()->GetRealTimeSeconds();
const float Sine = FMath::Sin(Time * SpeedMultiplier);
if (Parent)
{
Parent->SetActorLocation(StartLocation + (FloatRange * Sine));
}
}
・制作したFloatingComponentをアクタに追加し、
プロパティを入力します。

・無事、上下運動をする
FloatingComponentを持つアクタを配置することに成功しました。
youtu.be
SceneComponentの作り方
SceneComponentは、ActorComponentと違いTransformを含みます。
位置情報を含みます。
・コンテンツブラウザで
C++フォルダで右クリック>新規C++クラスを選択>SceneComponentを選択
することで作成することができます。

・クラス名を入力して、クラスを作成

・エディタから、C++のヘッダとソース作成に成功しました

・ソースを記述し、ビルド実行
ヘッダーファイル
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "HoverComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class COMPONENTSOFGAMEPLAY_API UHoverComponent : public USceneComponent
{
GENERATED_BODY()
public:
UHoverComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
UPROPERTY()
UPrimitiveComponent* ParentRoot;
UPROPERTY(EditAnywhere, Category="Hover Variables")
float MaxHoverForce = 100.0f;
UPROPERTY(EditAnywhere, Category="Hover Variables")
float HoverForceDistance = 100.0f;
UPROPERTY(EditAnywhere, Category="Debug")
bool bDrawDebug = false;
};
ソースファイル
#include "HoverComponent.h"
#include "DrawDebugHelpers.h"
UHoverComponent::UHoverComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UHoverComponent::BeginPlay()
{
Super::BeginPlay();
ParentRoot = GetOwner()->FindComponentByClass<UPrimitiveComponent>();
if(ParentRoot)
{
ParentRoot->SetMobility(EComponentMobility::Movable);
ParentRoot->SetSimulatePhysics(true);
ParentRoot->SetLinearDamping(2.0f);
ParentRoot->SetAngularDamping(2.0f);
MaxHoverForce = MaxHoverForce * ParentRoot->GetMass();
}
}
void UHoverComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
FHitResult Hit;
GetWorld()->LineTraceSingleByChannel(Hit, GetComponentLocation(), GetComponentLocation() - FVector(0,0,HoverForceDistance), ECollisionChannel::ECC_Visibility);
if(Hit.bBlockingHit)
{
const FVector StartLocation = GetComponentLocation();
const float Distance = (StartLocation - Hit.Location).Size();
const float Ratio = FMath::Clamp(Distance / HoverForceDistance, 0.0f, 1.0f);
const FVector Force = (1.0f - Ratio) * MaxHoverForce * Hit.ImpactNormal;
ParentRoot->AddForce(Force, NAME_None, false);
if(bDrawDebug)
{
DrawDebugLine(GetWorld(), StartLocation, Hit.Location, FColor::White, false, 0.0f);
DrawDebugPoint(GetWorld(), Hit.Location, 10.0f, FColor::Black, false, 0.0f);
}
}
}
・制作したHoverComponentをアクタに追加し、
プロパティを入力します。

・無事、上下運動をする
HoverComponentを持つアクタを配置することに成功しました。
youtu.be
C++でのコンポーネントの追加方法
・C++でのコンポーネントを追加する
ヘッダーファイルでは、以下のように記述しプロパティを作成
UPROPERTY(EditDefaultsOnly, Category="Components")
UStaticMeshComponent* Mesh = nullptr;
ソースファイルでは、以下のように記述しコンポーネントを作成
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
さらに、作成したコンポーネントでルートコンポーネントを置き換えることが可能です。
RootComponent = Mesh;
・ビルド実行
無事ADemoActorを継承した、BP_DemoActorは
最初からコンポーネントが追加された状態になることが確認できます。
※ルートコンポーネントがMeshになっているのも注目点です

・メッシュのアセットの指定
BP_DemoActorのMeshコンポーネントのStaticMeshをエディタで指定します。
・無事、コンポーネントはC++で定義し、
スタティックメッシュはBPで指定したアクタの作成に成功します。

スタティックメッシュの作成方法
・アクタを配置パネルから、キューブをレベルに配置
右クリック>スタティックメッシュへ変換をクリック

・スタティックメッシュ名を入力することで、
アセットを作成することができます。

・コリジョンの指定
作成したスタティックメッシュを開き
メニューバーから
コリジョン>ボックス単純化コリジョンを追加
を選択することでコリジョンを追加することを忘れずに

オーバーラップイベント関数の登録方法
HomingComponent.hを確認すれば理解できます。
・C++でのコールバック関数を登録する
ヘッダーファイルでは、以下のように自作の関数を記述
UFUNCTION(Category = "Homing")
void OnOverlapEnter(class UPrimitiveComponent* ThisComp,
class AActor* OtherActor, class UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION(Category= "Homing")
void OnEndOverlap(class UPrimitiveComponent* ThisComp,
class AActor* OtherActor, class UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
ソースファイルでは、以下のように記述しコールバック関数を登録
MagnetSphere->OnComponentBeginOverlap.AddDynamic(this, &UHomingComponent::OnOverlapEnter);
MagnetSphere->OnComponentEndOverlap.AddDynamic(this, &UHomingComponent::OnEndOverlap);
ダメージ時イベント関数の登録方法
・C++でのコールバック関数を登録する
ヘッダーファイルでは、以下のように自作の関数を記述
UFUNCTION()
void TakeDamage(AActor* DamagedActor, float Damage,
const class UDamageType* DamageType, class AController* InstigatedBy,
AActor* DamageCauser);
ソースファイルでは、以下のように記述しコールバック関数を登録
Owner->OnTakeAnyDamage.AddDynamic(this, &UHealthComponent::TakeDamage);
・インターフェイスの呼び出し
TakeDamageでは、アクターのインターフェイスを呼び出すようにしています。
インターフェイスを実装していれば、その処理を行い
インターフェイスを実装していなければ、スルーされるテクニックです。
IHealthInterface* Interface = Cast<IHealthInterface>(Owner);
if(Interface)
{
Interface->Execute_HealthDepleted(Owner);
}
アクタ破棄時イベント関数の登録方法
ヘッダーファイルでは、以下のように自作の関数を記述
UFUNCTION()
void Destruct(AActor* DestroyedActor);
ソースファイルでは、以下のように記述しコールバック関数を登録
Owner->OnDestroyed.AddDynamic(this, &UDestructionComponent::Destruct);
破壊時のコールバック関数では、
アセットが指定されていた時のみ、爆破エフェクトを再生するようにしています。
if(ParticleSystem)
{
UNiagaraComponent* Effect =
UNiagaraFunctionLibrary::SpawnSystemAtLocation(
GetWorld(), ParticleSystem, Owner->GetActorLocation());
}
爆破の指定がある場合のみ
周辺のアクタに対してダメージをあたえるようにしてあります
const bool Hit = GetWorld()->SweepMultiByChannel(
HitResults, Start, End, FQuat::Identity, ECC_Camera, ColShape);
for(FHitResult const HitResult : HitResults)
{
FPointDamageEvent PointDamage;
HitResult.GetActor()->TakeDamage(
100.0f, PointDamage,
GetWorld()->GetFirstPlayerController(), GetOwner());
}
そして、エディタで爆破の有無、アセットの指定などを個別に行うようにしてあります

実際にできたものはこちら
youtu.be
その他のテクニック
コンポーネントのアクタへのアタッチ方法はこちら
MagnetSphere->SetupAttachment(this);
プロパティの変更によって呼び出される関数はこちら
void UHomingComponent::PostEditChangeProperty(
FPropertyChangedEvent& PropertyChangedEvent)
※注意点
UE4.26で検証しているのですが、コース通りに作成してもうまく動かず、
以下の2点を修正することでビルドが通るようになりました。
DevelopmentEditor Win64 でビルド

includeパスが通っていない場合がある

エンジニアであれば、必ず受講しておく必要があるすばらしいコースです。
コンポーネントを利用した、再利用可能なクラス設計ができるようになります。
クラス継承による拡張ではなく、
コンポーネントによる拡張ができるようになります。
コールバック関数のC++での登録方法なども理解できます。