ゲーム開発
Unity
UnrealEngine
C++
Blender
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20240519_02_ue5_cpp_run_game

      【UE5】第二回 ミニゲーム制作で学ぶUnrealC++ 〜キャラクター・ゲーム実装 編〜

      UnrealEngineC++制作日記ミニゲームUE5
      2024-05-18

      マイケル
      マイケル
      みなさんこんにちは! マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      「ミニゲームの制作過程を通してUnrealC++で開発する際の方法をざっくり知ろう」という趣旨で、全三回に分けてUnrealEngineでのミニゲーム制作について紹介しています。 前回はUnrealC++の概要について紹介したので、今回からは実際のゲーム制作内容について踏み込んでいきます!
      【UE5】第一回 ミニゲーム制作で学ぶUnrealC++ 〜UnrealC++の概要 編〜
      2024-05-18

      ▲前回の記事


      ▲制作したゲーム
      エレキベア
      エレキベア
      猫のランゲームについて制作したのだったクマね
      マイケル
      マイケル
      今回はメインとなる ・ゲーム概要と設計 ・ゲーム素材の準備 ・キャラクター制御の実装 ・ゲーム内容の実装 について紹介していきます。 コードは下記リポジトリにあげていますので、こちらもよければ合わせてご参照ください!

      ▼GitHubリポジトリ
      GitHub - plasmo310 / ue5-cat-runner

      ▼UnrealEngineバージョン
      5.3.2

      エレキベア
      エレキベア
      やったるクマ〜〜〜

      参考書籍

      マイケル
      マイケル
      今回実装・情報整理するにあたり、下記書籍を参考にさせていただきました!

      C++でつくるUnreal Engineアプリ開発 for Windows & ...

      Unreal Engine 5で極めるゲーム開発:サンプルデータと動画で学ぶゲー...

      マイケル
      マイケル
      C++でつくるUnreal Engineアプリ開発は、UE4の情報にはなりますが数少ないC++について触れられた書籍です。 Unreal Engine 5で極めるゲーム開発は通称極め本と呼ばれていて、業界内では定番書籍として扱われています。 量はありますが、一度見ておいて損はないと思います!
      エレキベア
      エレキベア
      極め本はUE4版もあったクマが、UE5になってまたボリュームが大きくなったクマね

      ゲーム概要と設計

      マイケル
      マイケル
      制作の話に入る前に、まずはゲームの概要と設計について紹介します。

      ゲーム概要

      マイケル
      マイケル
      今回制作したゲームはシンプルな3Dエンドレスランゲームで、下記のようなルールになっています。
      1. 自動で前進し、ジャンプと左右移動でネコを操作してちゅ〜るを集める
      20240519_01_ue5_cpp_run_game_01
      1. 道の端まで到着すると、曲がるとともにスピードや難易度が上がる
      20240519_01_ue5_cpp_run_game_02
      1. トゲのブロックに当たるとゲーム終了
      20240519_01_ue5_cpp_run_game_03
      操作方法
      操作
      キー
      ジャンプ
      Space
      左移動
      A
      右移動
      D
      しゃがむ
      S
      エレキベア
      エレキベア
      トゲをよけながらちゅ〜るを集めるということクマね

      設計

      マイケル
      マイケル
      プロジェクトは基本的にBlueprintは使用せず、C++メインで制作しています。 コード部分のフォルダ構成は下記になっています。

      GitHub - plasmo310 / ue5-cat-runner (Source)

      フォルダ
      概要
      Audio
      オーディオ関連
      Cat
      ねこ関連
      Data
      ステージデータ関連
      GameInstance
      GameInstance関連
      GameMode
      GameMode関連 ※デフォルト
      Gimmick
      ギミック関連
      Hit
      衝突処理関連
      Level
      レベル関連
      Pickup
      取得アイテム関連
      Stage
      ステージ関連
      StateMachine
      ステートマシン
      UMG
      UI関連
      マイケル
      マイケル
      依存関係はざっくりと下記のようになっています。 Levelスクリプトがメインとなってゲームルールを制御しています。
      20240519_01_ue5_cpp_run_game_04
      ▲ざっくりとした設計

      エレキベア
      エレキベア
      この規模ならこれくらいがちょうどいいクマね UI側からは見るだけで済むのは綺麗クマ

      ゲーム素材の準備

      マイケル
      マイケル
      ゲームを制作にするにあたって必要な素材については大きく ・キャラ、アイテムの3Dモデル ・ステージ、エフェクトのテクスチャ を用意しました。

      ねこ、ちゅーるのモデル

      マイケル
      マイケル
      3Dモデルとしては、 ・ねこ ・ちゅ〜る(取得アイテム) の2種類をBlenderにて作成しました。
      20240519_01_ue5_cpp_run_game_05
      ▲3Dモデル(ねこ)

      20240519_01_ue5_cpp_run_game_06
      ▲3Dモデル(ちゅ〜る)

      エレキベア
      エレキベア
      これくらいのモデルならそこまで時間をかけずに作れそうクマね
      マイケル
      マイケル
      ねこのモデルに関しては操作アクションがあるため、ボーンを設定して数種類のアニメーションも用意しています。
      20240519_01_ue5_cpp_run_game_25
      ▲ボーン設定やアニメーション制作も行なった

      マイケル
      マイケル
      そしてこのねこのモデルとなったのは我が家の猫です・・・。 もはやこの子を3Dモデルにしたかっただけ説もあります・・・。
      20240519_01_ue5_cpp_run_game_24
      ▲モデルとなった家の猫

      エレキベア
      エレキベア
      なんて愛らしいんだクマ・・・

      ステージのテクスチャ

      マイケル
      マイケル
      がっつり作ったのは3Dモデルくらいで、あとは手間をかけずにさくっと作りました。 ステージ用のテクスチャはStableDiffusionで制作しています。
      20240519_01_ue5_cpp_run_game_07
      20240519_01_ue5_cpp_run_game_08
      エレキベア
      エレキベア
      背景とか雰囲気を出したい場合にすごく便利クマね
      マイケル
      マイケル
      今回はこれらのテクスチャを使用して、 下記のような床を繋げることでステージを生成しました。
      20240519_01_ue5_cpp_run_game_26
      ▲一つの床・壁として使用する

      エフェクトのテクスチャ

      マイケル
      マイケル
      エフェクトに関しても、下記のように簡易的なもので実装しています。 こちらはペイント、Gimpで作りました。
      20240519_01_ue5_cpp_run_game_09
      20240519_01_ue5_cpp_run_game_10
      エレキベア
      エレキベア
      これをパーティクルとしてエフェクトを作成するクマね
      20240519_01_ue5_cpp_run_game_11
      ▲Niagaraを使用してエフェクトを作成する

      マイケル
      マイケル
      今回のゲーム制作に必要な素材は以上になります! あとはがんがん実装していきしょう!

      キャラクター制御の実装

      マイケル
      マイケル
      まずはキャラクターの実装から始めます。 今回はThirdPersonテンプレートからC++プロジェクトを作成し、下記のようにキャラクターとカメラが配置された状態から始めました。
      20240519_01_ue5_cpp_run_game_27
      ▲キャラクター、カメラが設定された状態

      マイケル
      マイケル
      ThirdPersonテンプレートでのプロジェクト作成については前回の記事でも少し触れていますので、あまり触ったことがない方はこちらもご参照ください。
      【UE5】第一回 ミニゲーム制作で学ぶUnrealC++ 〜UnrealC++の概要 編〜
      2024-05-18
      エレキベア
      エレキベア
      ここからねこの動きを作っていくクマね

      入力処理周り

      マイケル
      マイケル
      まずは入力処理について実装します。 今回のゲームはシンプルで、下記入力に対してそれぞれのアクションを行います。
      ねこ入力アクション定義
      入力キー
      アクション
      スペース
      ジャンプ
      A
      左回転
      D
      右回転
      S
      しゃがむ
      エレキベア
      エレキベア
      デフォルトはTPSのような操作になっているクマから、それを今回のゲーム形式に変えるクマね
      マイケル
      マイケル
      UE5ではEnhancedInputというシステムを使用して入力制御を行います。 これはUnityでいうInputSystem的なもので、アクションに対して操作を割り当てることで制御するものです。

      UnrealEngineドキュメント - EnhancedInput

      マイケル
      マイケル
      イメージとしては下記のように、アクションの数だけInputActionを作成し、InputMappingContextで入力操作と紐づけることで定義します。 設定方法の詳細については公式ドキュメントを参照しましょう!
      20240519_01_ue5_cpp_run_game_12
      ▲InputActionの作成

      20240519_01_ue5_cpp_run_game_28
      ▲ジャンプアクションの例

      20240519_01_ue5_cpp_run_game_13
      ▲InputMappingContextの作成

      20240519_01_ue5_cpp_run_game_29
      ▲アクションに対して入力操作を割り当てる

      エレキベア
      エレキベア
      これで入力操作に対して各アクションが割り当てられたクマね
      マイケル
      マイケル
      あとはCharacterクラス内でこれらをUPROPERTY設定できるようにして、アクション処理を定義した関数と紐づければ完了です!
      private:
      
      ・・・
      
      	/** MappingContext */
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
      	TObjectPtr<UInputMappingContext> DefaultMappingContext;
      
      	/** Jump Input Action */
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
      	TObjectPtr<UInputAction> JumpAction;
      
      	/** Rotate L Input Action */
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
      	TObjectPtr<UInputAction> RotateLAction;
      	
      	/** Rotate R Input Action */
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
      	TObjectPtr<UInputAction> RotateRAction;
      
      	/** Squat Input Action */
      	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
      	TObjectPtr<UInputAction> SquatAction;
      
      ・・・
      
      void ACatCharacter::BeginPlay()
      {
      	Super::BeginPlay();
      
      	// Add Input Mapping Context
      	if (const APlayerController* PlayerController = Cast<APlayerController>(Controller))
      	{
      		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
      		{
      			Subsystem->AddMappingContext(DefaultMappingContext, 0);
      		}
      	}
      
      ・・・略・・・
      
      }
      
      void ACatCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
      {
      	UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
      	if (EnhancedInputComponent == nullptr)
      	{
      		UE_LOG(LogTemplateCharacter, 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));
      		return;
      	}
      
      	// Jumping
      	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACatCharacter::InputJump);
      	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
      
      	// Rotate
      	EnhancedInputComponent->BindAction(RotateLAction, ETriggerEvent::Started, this, &ACatCharacter::InputRotateL);
      	EnhancedInputComponent->BindAction(RotateRAction, ETriggerEvent::Started, this, &ACatCharacter::InputRotateR);
      
      	// Squat
      	EnhancedInputComponent->BindAction(SquatAction, ETriggerEvent::Started, this, &ACatCharacter::InputSquat);
      	EnhancedInputComponent->BindAction(SquatAction, ETriggerEvent::Completed, this, &ACatCharacter::InputSquatCanceled);
      }
      
      ・・・
      
      // === ジャンプの例
      
      void ACatCharacter::InputJump()
      {
      	// Runステート時のみが対象
      	if (GetCurrentState() != ECatState::Run)
      	{
      		return;
      	}
      
      	// ステート変更
      	ChangeState(ECatState::Jump);
      	Super::Jump();
      }
      
      20240519_01_ue5_cpp_run_game_30
      ▲Blueprint内で入力アクションを設定する

      エレキベア
      エレキベア
      これでC++側で入力処理を制御できるようになったクマね

      ステート処理周り

      マイケル
      マイケル
      次にステート処理についてですが、今回は下記のようなステートを定義しました。
      ねこステート定義
      状態
      ステート名
      Wait
      待機
      Run
      走る
      Jump
      ジャンプ
      Rotate
      左右回転
      Squat
      しゃがむ
      TurnCorner
      角を曲がる
      Dead
      死亡
      エレキベア
      エレキベア
      入力アクション+αで定義したクマね
      マイケル
      マイケル
      ステートの制御は今回は独自のステートマシンクラスを作成して使用しています。 ステートベースのシンプルな制御クラスです。

      plasmo310 / ue5-cat-runner - StateMachine.h

      plasmo310 / ue5-cat-runner - StateBase.h

      エレキベア
      エレキベア
      Unity開発で使っていたのを移植したクマね
      マイケル
      マイケル
      使い方は下記のようにStateBaseクラスを継承した各ステートクラスを作成し、ステートマシンに設定することで使用します。
      /**
       * ねこステート定義
       */
      UENUM(BlueprintType)
      enum class ECatState : uint8
      {
      	None,
      	Wait,       // 待機
      	Run,        // 走る
      	Jump,       // ジャンプ
      	RotateL,    // 左回転
      	RotateR,    // 右回転
      	Squat,      // しゃがむ
      	TurnCorner, // 角を曲がる
      	Dead,       // 死亡
      };
      
      
      ▲ステートのenum定義
      private:
      	/** ステートマシン */
      	TUniquePtr<ZStateMachine<ACatCharacter>> StateMachine;
      
      	/** 現在のステート変更処理 */
      	void ChangeState(ECatState State) const;
      
      ・・・略・・・
      
      	/**
      	 * State: 待機
      	 */
      	class StateWait : public ZStateBase<ACatCharacter>
      	{
      	public:
      		virtual void OnBegin(ACatCharacter* StateOwner) override;
      		virtual void OnTick(ACatCharacter* StateOwner, float DeltaTime) override;
      		virtual void OnEnd(ACatCharacter* StateOwner) override;
      	};
      
      
      ▲StateBaseを継承したクラスを定義(Waitステートの例)
      void ACatCharacter::StateWait::OnBegin(ACatCharacter* StateOwner)
      {
      	FStateBase::OnBegin(StateOwner);
      }
      
      void ACatCharacter::StateWait::OnTick(ACatCharacter* StateOwner, float DeltaTime)
      {
      	FStateBase::OnTick(StateOwner, DeltaTime);
      
      	// 移動準備ができたか?
      	if (StateOwner->bIsReadyMove)
      	{
      		StateOwner->ChangeState(ECatState::Run);
      	}
      }
      
      void ACatCharacter::StateWait::OnEnd(ACatCharacter* StateOwner)
      {
      	FStateBase::OnEnd(StateOwner);
      }
      
      
      ▲ステートに対する実装(Waitステートの例)
      void ACatCharacter::BeginPlay()
      {
      
      ・・・略・・・
      
      	// ステート初期化
      	StateMachine = MakeUnique<FStateMachine<ACatCharacter>>(this);
      	StateMachine->Add<StateWait>(static_cast<int>(ECatState::Wait));
      	StateMachine->Add<StateRun>(static_cast<int>(ECatState::Run));
      	StateMachine->Add<StateJump>(static_cast<int>(ECatState::Jump));
      	StateMachine->Add<StateRotate>(static_cast<int>(ECatState::RotateL));
      	StateMachine->Add<StateRotate>(static_cast<int>(ECatState::RotateR));
      	StateMachine->Add<StateSquat>(static_cast<int>(ECatState::Squat));
      	StateMachine->Add<StateTurnCorner>(static_cast<int>(ECatState::TurnCorner));
      	StateMachine->Add<StateDead>(static_cast<int>(ECatState::Dead));
      	StateMachine->OnBegin(static_cast<int>(ECatState::Wait));
      }
      
      void ACatCharacter::Tick(float DeltaTime)
      {
      
      ・・・略・・・
      
      	// ステート更新
      	StateMachine->OnTick(DeltaTime);
      }
      
      ECatState ACatCharacter::GetCurrentState() const
      {
      	if (StateMachine == nullptr)
      	{
      		return ECatState::None;
      	}
      	return static_cast<ECatState>(StateMachine->GetCurrentStateId());
      }
      
      void ACatCharacter::ChangeState(ECatState State) const
      {
      	StateMachine->ChangeState(static_cast<int>(State));
      }
      
      ▲ステートマシンへの設定と操作
      エレキベア
      エレキベア
      これで各ステートの制御を分割して記述しやすくなったクマね
      マイケル
      マイケル
      もし独自クラスを作成したくない場合は、下記記事で紹介した Sample01: enumを使用した実装 のように、Tick関数内でswitch文で切り替える方式を使用するといいと思います。
      【Unity】カニの動きで学ぶ有限ステートマシン(FSM)【ゲームAI】
      2021-12-30

      トランスフォーム制御周り

      マイケル
      マイケル
      次にトランスフォーム制御についてですが、こちらは 位置:Location(FVector) 回転:Rotation(FRotator) を使用して制御します。 GetActorXXX、SetActorXXXといった関数が用意されているので、そちらを使用して制御を行いましょう。
      		// 曲がるポイントに到着したかのチェック
      		constexpr float AllowDistance = 50.0f;
      		const auto CurrentLocation = StateOwner->GetActorLocation();
      		const auto DiffForwardLocation = (StateOwner->StartTurnCornerPoint - CurrentLocation).GetAbs() * StateOwner->GetActorForwardVector();
      		if (DiffForwardLocation.Length() <= AllowDistance)
      		{
      			bIsArriveTurnCorner = true;
      
      			// プレイヤーを回転させる
      			const FVector NewActorLocation = CurrentLocation + DiffForwardLocation;
      			StateOwner->SetActorLocation(NewActorLocation);
      			const FRotator NewActorRotator = StateOwner->GetActorRotation() + StateOwner->ChangeTurnCornerRotator;
      			StateOwner->SetActorRotation(NewActorRotator);
      		}
      
      エレキベア
      エレキベア
      名称が違うだけでUnityと操作感はあまり変わらないクマね

      アニメーション制御

      マイケル
      マイケル
      最後にアニメーション制御についてですが、UE5ではAnimationBlueprint (以降ABP)というものを作成して設定します。

      参考:
      UnrealEngineドキュメント - アニメーションブループリント

      20240519_01_ue5_cpp_run_game_31
      ▲AnimationBlueprintの作成

      20240519_01_ue5_cpp_run_game_33
      ▲キャラクターBP内でABPを設定して紐づける

      マイケル
      マイケル
      ABP内ではステートマシンを作成することができ、特定条件でアニメーションを遷移させるといった設定をすることができます。
      20240519_01_ue5_cpp_run_game_32
      ▲ステートマシンを作成して定義できる

      20240519_01_ue5_cpp_run_game_15
      ▲作成したステートマシンの実装

      エレキベア
      エレキベア
      UnityでいうAnimationControllerクマね
      マイケル
      マイケル
      この遷移条件も通常はABP内でノードを組み立てる必要があるのですが、UAnimInstanceクラスを継承したクラスを親クラスにすることで、ロジックをある程度C++側に寄せることができます。 下記は各ステートに遷移するフラグをBlueprint側に公開する例です。
      // Copyright (c) 2024, CatRunner All Rights Reserved.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "Animation/AnimInstance.h"
      #include "CatAnimInstance.generated.h"
      
      /**
       * ねこ: AnimInstance
       */
      UCLASS()
      class CATRUNNER_API UCatAnimInstance : public UAnimInstance
      {
      	GENERATED_BODY()
      
      public:
      	/** update event */
      	virtual void NativeUpdateAnimation(float DeltaSeconds) override;
      
      protected:
      	UPROPERTY(BlueprintReadOnly, Category = "CatRunner: Cat")
      	bool bIsRunState;
      
      	UPROPERTY(BlueprintReadOnly, Category = "CatRunner: Cat")
      	bool bIsJumpState;
      
      	UPROPERTY(BlueprintReadOnly, Category = "CatRunner: Cat")
      	bool bIsRotateLState;
      
      	UPROPERTY(BlueprintReadOnly, Category = "CatRunner: Cat")
      	bool bIsRotateRState;
      
      	UPROPERTY(BlueprintReadOnly, Category = "CatRunner: Cat")
      	bool bIsSquatState;
      
      };
      
      
      ▲アニメーションステート遷移のフラグを公開する
      // Copyright (c) 2024, CatRunner All Rights Reserved.
      
      
      #include "Cat/CatAnimInstance.h"
      #include "Cat/CatCharacter.h"
      
      void UCatAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
      {
      	Super::NativeUpdateAnimation(DeltaSeconds);
      
      	// プロパティ初期化
      	bIsRunState = false;
      	bIsJumpState = false;
      	bIsRotateLState = false;
      	bIsRotateRState = false;
      	bIsSquatState = false;
      
      	// プレイヤーのステートをチェック
      	const auto player = Cast<ACatCharacter>(GetOwningActor());
      	if (player == nullptr)
      	{
      		return;
      	}
      	const ECatState CurrentState = player->GetCurrentState();
      	switch (CurrentState)
      	{
      	case ECatState::Run:
      		bIsRunState = true;
      		break;
      	case ECatState::Jump:
      		bIsJumpState = true;
      		break;
      	case ECatState::RotateL:
      		bIsRotateLState = true;
      		break;
      	case ECatState::RotateR:
      		bIsRotateRState = true;
      		break;
      	case ECatState::Squat:
      		bIsSquatState = true;
      		break;
      	default:
      		break;
      	}
      }
      
      
      ▲フラグの設定(※今回はねこのステートを見るだけのシンプルな条件)
      20240519_01_ue5_cpp_run_game_14
      ▲作成したクラスをABPの親クラスに設定

      マイケル
      マイケル
      これでABP内の遷移条件内でフラグを参照できるようになります!
      20240519_01_ue5_cpp_run_game_16
      エレキベア
      エレキベア
      今回はシンプルな条件クマが、複雑な条件になるとC++に寄せた方がいろいろと恩恵がありそうクマね

      キャラクター実装完了

      マイケル
      マイケル
      ここまででキャラクターが入力操作に応じて操作できるようになりました!
      20240519_01_ue5_cpp_run_game_34
      20240519_01_ue5_cpp_run_game_35
      エレキベア
      エレキベア
      あとはゲームルールを実装すれば遊べそうクマね

      ゲーム内容の実装

      マイケル
      マイケル
      キャラクターの実装が完了したところで、ゲーム内容の実装に入ります。

      ゲームシーケンス制御

      マイケル
      マイケル
      まずゲームのシーケンス制御についてですが、こちらはキャラクターと同様に独自のステートマシンクラスを使用してステート定義することで制御しています。
      ゲームステート定義
      状態
      ステート名
      Ready
      準備
      Play
      プレイ中
      End
      終了
      Result
      リザルト
      20240519_01_ue5_cpp_run_game_01
      ▲ゲームの流れのステートになる

      マイケル
      マイケル
      問題はこれをどのクラスに定義するかですが、UnrealEngineではレベルBPというレベルを制御するBlueprintが用意されており、こちらに定義することにしました。 ALevelScriptActorクラスを継承したクラスを作成することで、親クラスを差し替えることができます。

      参考:
      UnrealEngineドキュメント - Levelブループリント

      /**
       * RunGameレベルクラス
       */
      UCLASS()
      class ARunGameLevelScript : public ALevelScriptActor
      {
      	GENERATED_BODY()
      
      public:
      	ARunGameLevelScript();
      
      protected:
      	virtual void BeginPlay() override;
      	virtual void Tick(float DeltaSeconds) override;
      	
      ・・・
      
      
      ▲レベルクラスの作成
      /**
       * RunGameレベルステート定義
       */
      UENUM(BlueprintType)
      enum class ERunGameState : uint8
      {
      	None,
      	Ready,  // 準備中
      	Play,   // プレイ中
      	End,    // 終了
      	Result, // リザルト
      };
      
      
      ▲キャラクターと同様、ステートを定義して実装する
      エレキベア
      エレキベア
      あとは各ステートに対して処理を実装すれば制御できそうクマね

      ステージレベル情報の管理

      マイケル
      マイケル
      今回のゲームでは、一定距離進むごとにステージの難易度が上がる設計にしています。 このようなステージレベル情報の管理方法として、DataTableを使用することにしました。 これはCSVデータと連携して管理することができるので、表形式でデータ管理したい場合に有用です。

      UnrealEngineドキュメント - データ ドリブン ゲームプレイ要素

      20240519_01_ue5_cpp_run_game_36
      ▲DataTableを使用して情報を管理することができる

      エレキベア
      エレキベア
      Unityだと有料アセットでしか見たことないクマから、デフォルトで用意されているのはありがたいクマね
      マイケル
      マイケル
      作成方法も簡単で、FTableRowBase構造体を継承した構造体クラスを作成し、 定義に合わせたCSVファイルを用意してD&Dするだけで作成できます。
      // Copyright (c) 2024, CatRunner All Rights Reserved.
      
      #pragma once
      
      #include "CoreMinimal.h"
      #include "StageLevelTableRow.generated.h"
      
      /**
       * ステージレベル情報
       */
      USTRUCT(BlueprintType)
      struct FStageLevelTableRow : public FTableRowBase
      {
      	GENERATED_BODY()
      
      	/** プレイヤーに加算する前進速度 */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	float AddPlayerForwardSpeed;
      
      	/** 1つの道に含まれる床の数 */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	uint8 RoadFloorCount;
      
      	/** 取得アイテム生成確率(%) */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	uint8 GeneratePickupPercent;
      
      	/** 取得アイテム最大生成数 */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	uint8 MaxGeneratePickupCount;
      
      	/** ギミック最小生成数 */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	uint8 MinGenerateGimmickCount;
      
      	/** ギミック最大生成数 */
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	uint8 MaxGenerateGimmickCount;
      };
      
      
      ▲構造体データの定義
      20240519_01_ue5_cpp_run_game_37
      ▲データに合わせたCSVを定義する

      エレキベア
      エレキベア
      なんて簡単なんだクマ・・・
      マイケル
      マイケル
      C++側で利用する方法も簡単で、下記のようにUDataTableクラスをUPROPERTYで設定できるようにしてFindRow関数を呼び出すことで対象行のデータが取得できます。
      	/** ステージレベルテーブル */
      	UPROPERTY(EditAnywhere)
      	TObjectPtr<UDataTable> StageLevelTable;
      
      FStageLevelTableRow* ARunGameLevelScript::GetStageLevelInfo(int StageLevel) const
      {
      	const FString Context;
      	return StageLevelTable->FindRow<FStageLevelTableRow>(FName(FString::FromInt(StageLevel)), Context);
      }
      
      エレキベア
      エレキベア
      これでデータ周りの管理は問題なさそうクマね

      ステージの生成処理

      マイケル
      マイケル
      次にステージ生成処理についてですが、こちらは冒頭でも紹介した床・壁のモデルを繋げることで生成しています。 道の端には左右へ曲がる角の床も用意しておいて、進行に合わせて道単位で生成・破棄を行っています。
      20240519_01_ue5_cpp_run_game_38
      ▲ステージを生成してギミック・アイテムを配置する

      20240519_01_ue5_cpp_run_game_26
      ▲配置する単位の床・壁モデル

      20240519_01_ue5_cpp_run_game_39
      ▲曲がり角用のモデルも用意しておく

      エレキベア
      エレキベア
      簡単なロジックで生成できそうクマね
      マイケル
      マイケル
      詳細な生成処理については省略しますが、ステージ生成用のStageGeneratorというアクタを用意して必要な情報を設定することで処理を実装しました。
      ・・・
      
      private:
      	/** ステージ床アクタ */
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<AStageFloor> StageFloor;
      
      	/** ステージ床アクタ(左L字) */
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<AStageFloor> StageFloorL;
      
      	/** ステージ床アクタ(右L字) */
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<AStageFloor> StageFloorR;
      
      	/** 曲がるときに出すボーナスピックアップ */
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<APickup> TurnCornerBonusPickup;
      
      	/** 取得アイテム生成情報テーブル */
      	UPROPERTY(EditAnywhere)
      	TArray<FPickupGenerateInfo> PickupGenerateTable;
      
      	/** ギミック(現状1種類) */
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<AGimmick> Gimmick;
      	
      ・・・
      
      ▲ステージ生成情報の定義
      TObjectPtr<AStageFloor> AStageGenerator::GenerateFloor(const FVector& SpawnLocation,  const FRotator& SpawnRotator, const EStageFloorType StageFloorType)
      {
      	// 床の種類からアクタを取得
      	TSubclassOf<AStageFloor> StageFloorActor = nullptr;
      	switch (StageFloorType)
      	{
      	case EStageFloorType::Normal:
      		StageFloorActor = StageFloor;
      		break;
      	case EStageFloorType::Left:
      		StageFloorActor = StageFloorL;
      		break;
      	case EStageFloorType::Right:
      		StageFloorActor = StageFloorR;
      		break;
      	default:
      		break;
      	}
      	if (StageFloorActor == nullptr)
      	{
      		return nullptr;
      	}
      
      	// アクタを生成
      	FActorSpawnParameters SpawnParams;
      	SpawnParams.Owner = this;
      	SpawnParams.Instigator = GetInstigator();
      	UWorld* const World = GetWorld();
      	return World->SpawnActor<AStageFloor>(StageFloorActor, SpawnLocation, SpawnRotator, SpawnParams);
      }
      
      ▲床のスポーン処理
      エレキベア
      エレキベア
      これでステージ生成も制御できるようになったクマ〜〜

      衝突判定

      マイケル
      マイケル
      最後に、プレイヤーとギミック・アイテムの衝突処理についてです。 こちらは衝突した対象によってスコアの加算・死亡といった処理を行います。
      20240519_01_ue5_cpp_run_game_40
      ▲ギミックやアイテムにはコライダを設定している

      エレキベア
      エレキベア
      衝突処理のイベントをどう受け取るかクマね
      マイケル
      マイケル
      これには各コライダのコンポーネントに用意されたイベントに対して処理を紐づけることで検知できます。 今回はキャラクターのCapsuleComponent内、OnComponentBeginOverlapイベントに対して処理を設定しました。
      ACatCharacter::ACatCharacter()
      	:CurrentMoveForwardSpeed(0.0f)
      	,StartTurnCornerPoint(FVector::ZeroVector)
      	,ChangeTurnCornerRotator(FRotator::ZeroRotator)
      	,bIsReadyMove(false)
      	,bIsPushSquat(false)
      {
      	// Set size for collision capsule
      	GetCapsuleComponent()->InitCapsuleSize(50.f, 50.0f);
      	GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &ACatCharacter::OnOverlapBegin); // setup overlap event.
      		
      ・・・
      
      }
      
      ・・・
      
      void ACatCharacter::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
      	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
      {
      	// イベント発行
      	if (OnHitActorDelegate.IsBound())
      	{
      		OnHitActorDelegate.Execute(this, OtherActor);
      	}
      }
      
      ▲衝突イベントの設定
      20240519_01_ue5_cpp_run_game_42
      ▲BP上で操作できるイベントは大体C++側からも設定できる

      マイケル
      マイケル
      衝突した際のアクションについては、別途クラスを作成してそちらで管理するようにしています。 この辺りは好みにもよるので、アクタに直接書くなど好きに実装しましょう。
      FHitInfo ABlockGimmick::OnHit(AActor* HitActor)
      {
      	// エフェクト、サウンド再生
      	const FVector PlayerLocation = HitActor->GetActorLocation();
      	if (PlayerDestroyEffect != nullptr)
      	{
      		UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), PlayerDestroyEffect, PlayerLocation);
      	}
      	if (PlayerDestroySound != nullptr)
      	{
      		UCatRunnerGameInstance::GetAudioService()->PlaySe3d(PlayerDestroySound, PlayerLocation);
      	}
      
      	// ヒット対象を破棄する
      	FHitInfo HitInfo;
      	HitInfo.bIsTargetDestroy = true;
      	return HitInfo;
      }
      
      
      ▲衝突対象からはヒットした情報のみ返す
      void UHitDetectManager::Initialize(ACatCharacter* Player) const
      {
      	// プレイヤーに監視イベントを設定
      	Player->OnHitActorDelegate.BindLambda([this](ACatCharacter* Player, AActor* HitActor)
      	{
      		OnDetectedHitPlayer(Player, HitActor);
      	});
      }
      
      void UHitDetectManager::OnDetectedHitPlayer(ACatCharacter* Player, AActor* HitActor)
      {
      	IHitObjectInterface* HitObject = Cast<IHitObjectInterface>(HitActor);
      	if (HitObject == nullptr)
      	{
      		return;
      	}
      
      	// ヒット情報によって処理を行う
      	const FHitInfo HitInfo = HitObject->OnHit(Player);
      
      	// スコアの加算
      	if (HitInfo.AddScore != 0)
      	{
      		const auto LevelScriptActor = UGameplayStatics::GetActorOfClass(Player->GetWorld(), ARunGameLevelScript::StaticClass());
      		const auto RunGameLevelScript = Cast<ARunGameLevelScript>(LevelScriptActor);
      		if (RunGameLevelScript != nullptr)
      		{
      			RunGameLevelScript->AddPickupScore(HitInfo.AddScore);
      		}
      	}
      
      	// 死亡させるか?
      	if (HitInfo.bIsTargetDestroy)
      	{
      		Player->OnDie();
      	}
      
      	// 回転させるか?
      	if (HitInfo.ChangeTurnCornerRotator != FRotator::ZeroRotator)
      	{
      		Player->OnTurnCorner(HitInfo.ChangeTurnCornerRotator, HitActor->GetActorLocation());
      	}
      }
      
      
      ▲受け取ったヒット情報から処理を行う
      エレキベア
      エレキベア
      これで衝突処理もクリアクマ〜〜〜

      ゲーム実装完了

      マイケル
      マイケル
      以上でゲーム内容の実装も完了です! ここまでで一応ゲームとして遊べるようになりました。
      20240519_01_ue5_cpp_run_game_41
      ▲ステージが生成され、アイテム等も取得できる

      エレキベア
      エレキベア
      やったクマ〜〜〜

      おわりに

      マイケル
      マイケル
      というわけで今回はUnrealC++を使用したキャラクター制御とゲーム内容の実装についてでした! どうだったかな??
      エレキベア
      エレキベア
      いろいろ覚えないといけないことはあるクマが、慣れればUnityと同じような感覚で実装できたクマね
      マイケル
      マイケル
      Unityの機能と紐づけて考えると理解しやすかったね。 それとUnrealEngineの情報はブループリントによる実装が多いから、それとC++実装を関連づける知識は必要そうだと感じました。
      マイケル
      マイケル
      次回はこのゲームにUI・エフェクト・サウンドなど味付けして仕上げていきます! お楽しみに!!
      エレキベア
      エレキベア
      クマ〜〜〜〜〜〜

      【UE5】第二回 ミニゲーム制作で学ぶUnrealC++ 〜キャラクター・ゲーム実装 編〜 〜完〜

      【UE5】第一回 ミニゲーム制作で学ぶUnrealC++ 〜UnrealC++の概要 編〜
      2024-05-18
      【UE5】第二回 ミニゲーム制作で学ぶUnrealC++ 〜キャラクター・ゲーム実装 編〜
      2024-05-18
      【UE5】第三回 ミニゲーム制作で学ぶUnrealC++ 〜UI・仕上げ実装 編〜
      2024-05-18

      UnrealEngineC++制作日記ミニゲームUE5
      2024-05-18

      関連記事
      【UE5】Niagara SimulationStageによるシミュレーション環境構築
      2024-05-30
      【UE5】第三回 ミニゲーム制作で学ぶUnrealC++ 〜UI・仕上げ実装 編〜
      2024-05-18
      【UE5】第一回 ミニゲーム制作で学ぶUnrealC++ 〜UnrealC++の概要 編〜
      2024-05-18
      【JUCE】DTMプラグインを作ってみる 〜ディストーション編〜【VST/AU】
      2024-03-22
      【Unity】「怪盗チョコレート」をリリース!工夫点や反省点をざっと振り返る【バレンタイン】
      2023-02-12
      【DirextX12】第四回 DirextX12を使ったゲーム開発 〜FBX SDKを使用した3Dモデル描画〜
      2022-12-07
      【DirextX12】第三回 DirextX12を使ったゲーム開発 〜座標変換と3Dオブジェクト表示〜
      2022-11-27
      【DirextX12】第二回 DirextX12を使ったゲーム開発 〜DirectXTexを用いたテクスチャ描画〜
      2022-11-25