とあるゲームプログラマの挑戦と敗北の歴史

UE4とプログラミングとmayaとpythonとhtmlとマラソンを中心に情報を発信する元「技術ブログを目指すブログ」から再始動した毎日の日々を発信するブログです。

UE4:オンラインラーニング(ゲームプレイのコンポーネントを分解する)

UE4オンラインラーニング
ゲームプレイのコンポーネントを分解する
を受講した際のおぼえ書きです。
https://learn.unrealengine.com/course/3762527



ActorComponentの作り方

・コンテンツブラウザで
 C++フォルダで右クリック>新規C++クラスを選択>ActorComponentを選択
 することで作成することができます。
f:id:toncrimentan_w:20210924010336p:plain

・クラス名を入力して、クラスを作成
f:id:toncrimentan_w:20210924010459p:plain

・エディタから、C++のヘッダとソース作成に成功しました
f:id:toncrimentan_w:20210924010528p:plain

・ソースを記述し、ビルド実行
ヘッダーファイル

#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をアクタに追加し、
 プロパティを入力します。
f:id:toncrimentan_w:20210924010719p:plain

・無事、上下運動をする
 FloatingComponentを持つアクタを配置することに成功しました。
youtu.be



SceneComponentの作り方
SceneComponentは、ActorComponentと違いTransformを含みます。
位置情報を含みます。

・コンテンツブラウザで
 C++フォルダで右クリック>新規C++クラスを選択>SceneComponentを選択
 することで作成することができます。
f:id:toncrimentan_w:20210924011026p:plain

・クラス名を入力して、クラスを作成
f:id:toncrimentan_w:20210924015949p:plain

・エディタから、C++のヘッダとソース作成に成功しました
f:id:toncrimentan_w:20210924015607p:plain

・ソースを記述し、ビルド実行
ヘッダーファイル

#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をアクタに追加し、
 プロパティを入力します。
f:id:toncrimentan_w:20210924011208p:plain

・無事、上下運動をする
 HoverComponentを持つアクタを配置することに成功しました。
youtu.be



C++でのコンポーネントの追加方法

C++でのコンポーネントを追加する
ヘッダーファイルでは、以下のように記述しプロパティを作成

UPROPERTY(EditDefaultsOnly, Category="Components")
UStaticMeshComponent* Mesh = nullptr;

ソースファイルでは、以下のように記述しコンポーネントを作成

Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));

さらに、作成したコンポーネントでルートコンポーネントを置き換えることが可能です。

RootComponent = Mesh;

・ビルド実行
無事ADemoActorを継承した、BP_DemoActorは
最初からコンポーネントが追加された状態になることが確認できます。
※ルートコンポーネントがMeshになっているのも注目点です
f:id:toncrimentan_w:20210924011839p:plain

・メッシュのアセットの指定
BP_DemoActorのMeshコンポーネントのStaticMeshをエディタで指定します。

・無事、コンポーネントC++で定義し、
 スタティックメッシュはBPで指定したアクタの作成に成功します。
f:id:toncrimentan_w:20210924012217p:plain



スタティックメッシュの作成方法

・アクタを配置パネルから、キューブをレベルに配置
 右クリック>スタティックメッシュへ変換をクリック
f:id:toncrimentan_w:20210924012704p:plain

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

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



オーバーラップイベント関数の登録方法
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());
}

そして、エディタで爆破の有無、アセットの指定などを個別に行うようにしてあります
f:id:toncrimentan_w:20210924080347p:plain

実際にできたものはこちら
youtu.be



その他のテクニック
コンポーネントのアクタへのアタッチ方法はこちら

MagnetSphere->SetupAttachment(this);

プロパティの変更によって呼び出される関数はこちら

void UHomingComponent::PostEditChangeProperty(
	FPropertyChangedEvent& PropertyChangedEvent)



※注意点
 UE4.26で検証しているのですが、コース通りに作成してもうまく動かず、
 以下の2点を修正することでビルドが通るようになりました。

DevelopmentEditor Win64 でビルド
f:id:toncrimentan_w:20210924084753p:plain

includeパスが通っていない場合がある
f:id:toncrimentan_w:20210924084807p:plain



エンジニアであれば、必ず受講しておく必要があるすばらしいコースです。
コンポーネントを利用した、再利用可能なクラス設計ができるようになります。
クラス継承による拡張ではなく、
コンポーネントによる拡張ができるようになります。

コールバック関数のC++での登録方法なども理解できます。