愛をもってゲームをつくるゴリラのブログ

UE5勉強中のゴリラがいろいろ頑張ります。

SetFocus 〜あなただけ見つめてる〜

この記事は Unreal Engine (UE)のカレンダー | Advent Calendar 2023 - Qiita
シリーズ3の13日目の投稿記事です。

検証時のバージョン: UE5.2.0


はじめに

今まで敵キャラクターがプレイヤーに体を向けて周囲をぐるぐる移動する処理を作る際、
向きを合わせるという処理を、ビヘイビアツリーのサービスを作成し、
角度計算と向きの設定を行う処理を記述していました。
UE5のゲームサンプルであるLyraStarterGameを見ていると、 サービス内にそのような処理を記述せずとも、AIControllerのSetFocusという処理を用いることで
同様のことができることがわかりました。
今回はSetFocusの概要と使い方についてまとめてみました。

SetFocusとは


SetFocusはAIControllerが特定のアクターに注意を向けるための処理です。
使い方は簡単で、引数のNewFocusに注意を向けたいアクターを指定するだけです。

処理の流れ

設定したアクターの情報は、AAIController::UpdateControlRotation内で利用されます。
この処理では、AIControllerが操作しているポーンから対象への角度計算が行われ、
操作しているポーンのAPawn::FaceRotationに渡すことで、向きを設定します。
※ポーンのUseControllerRotationYawに
 チェックを入れていないと、向きの設定は反映されません。

プレイヤーを対象とした例

以下の動画は、SetFocusを使用してプレイヤーの周囲をぐるぐる移動させる様子です。
自分で実装した部分は、プレイヤー周辺を移動する処理のみとなります。

アクター以外を注目したい場合


AAIController::SetFocalPoint の処理があり、特定アクターではなく、
特定座標へ注目させることもできます。

まとめ

SetFocusはAIControllerが特定のアクターや座標に注意を向けるための便利な処理です。
簡単な使い方と流れをまとめました。
不明な点や間違ってる点などありましたら、連絡をいただければ幸いです。
明日は mokoさんの UE5 エミッシブの値を露出に関わらず一定に保つ です。 お楽しみに!

3次元の経路探索 - データ作成編

この記事は qiita.comUnreal Engine 4(UE4) #1 Advent Calendar 2020
の23日目の投稿記事です。

ごりです。久々の投稿になります。
3次元の経路探索を何回かに分けて書かせていただきます。
今回はデータの作成編です。

はじめに

この記事は、下記を参考に作成したものになります。
誤っている点などございましたら、連絡いただけると助かります。

http://www.gameaipro.com/GameAIPro3/GameAIPro3_Chapter21_3D_Flight_Navigation_Using_Sparse_Voxel_Octrees.pdf

サンプルプロジェクト

下記で配布しています。
drive.google.com

バージョン

ソースコードも含めていますので、ビルド環境が必要となります。
以下の環境で作成しました。
UE4 Ver.4.25.4
Microsoft Visual Studio Community 2019 Version 16.8.3

使い方

プロジェクトを開くとSVOBoundsVolumeが置かれたレベルが開きます。
詳細タブのカテゴリ【SVO】で階層数を指定し、Generateボタンを押すと、SVOが構築されます。
f:id:m-goolee-y:20201221230436p:plain

カテゴリ【デバッグ】ではそれぞれチェックを入れることで可視化することができます。
f:id:m-goolee-y:20201221231025p:plain

全てにチェックをいれると、このようになります。
f:id:m-goolee-y:20201222210928p:plain

SVO( Sparse Voxel Octrees )

SVOはライティングやレイトレーシングで使用される一般的なデータ構造となっています。
基本的な作りは八分木で、各ツリーのボリュームを8つに階層的に分割することで、
高速な位置検索を可能としています。
SVOはリーフノード、ノード、リンクの3つの要素で構築できます。

リーフノード

SVOでのリーフノードは通常の八分木とは扱いが異なり、
最下層(レイヤー0)のノードのことを指します。
リーフノードは衝突or空き領域だけを考慮しているので、
必要なデータは状態を表す1ビットのみとなります。
リーフノードは通常のノードとは異なり、
64個の場所を表すノードで、サブノードと呼ばれます。
上層のノードはこのリーフノードを元に構築されます。

USTRUCT(BlueprintType)
struct FSVOLeafNode {

	//! サブノードインデックスリスト
	int64 mSubNodeIndices;
};

ノード

ツリーのノードには下記が含まれます。
・空間上の位置を知るための位置情報
・親ノードへのリンク
・最初の子ノードへのリンク
・ノード間を移動できるように隣接ノードへのリンク

子ノードへのリンクが最初の子だけで済むのは、
ノードがモートンコード順に格納されているため、
0~7をオフセットして子ノードへ移動することができます。
子ノードへのリンクが無効の場合、このノードにはボクセルが含まれていないと判断できます。

USTRUCT(BlueprintType)
struct FSVONode {

	//! 位置情報
	FVector mLocation;

	//! 親ノードへのリンク
	FSVOLink mParent;

	//! 最初の子へのリンク
	FSVOLink mFirstChild;

	//! 隣接リンク
	TArray< FSVOLink > mNeighbours;
};

隣接リンク

隣接ノードへのリンクはポインタを使用すると、32ビットと64ビットOSで
データサイズが大きく変化してしまうので、メモリ使用量制御のため、
ポインタの代わりに配列へのオフセットが使用されます。
リンクは経路探索でも使用されるので、同じ階層のノードだけでなく、
上下の層を行き来できるように階層番号とノード番号が必要になります。
また、リーフノードは64個の場所を表すノード(サブノードと呼ばれています)で、
サブノード番号もリンクに必要になります。
この3つの情報は32ビットの整数に変換して格納されます。

USTRUCT(BlueprintType)
struct FSVOLink {

	//! レイヤーインデックス( 0 ~ 15 )
	int32 mLayerIndex : 4;

	//! ノードインデックス( 0 ~ 4194303 )
	int32 mNodeIndex : 22;

	//! サブノートインデックス( 0 ~ 63 )
	//! リーフノード内のボクセルインデックスにのみ使用
	int32 mSubNodeIndex : 6;
};

構築手順

1層分のSVOの構築手順は下図のようになっており、
リーフノードを生成し、それをもとに上層のノードを生成、
最後に均一なノードを削除してSVOが構築されます。
f:id:m-goolee-y:20201222212109p:plain

リーフノードの生成

リーフノードは最下層から更に2階層細かくボリュームを分割し、ボリューム内に
コリジョンボクセルが含まれているかどうかを64ビットの値に格納していきます。
ここで、使用しているUSVOSystemLibrary::BoxOverlapActors は
UKismetSystemLibrary::BoxOverlapActorsを、
回転値をオーバーラップ判定に用いれるように変更しています。

void ASVOBoundsVolume::GenerateLeafNodes(){

	/** リーフノード数を計算 **/
	int32 NodeNum = GetNodeNum(SVO::LAYER_LEAF);

	/** リーフノードのボクセルサイズを取得 **/
	FVector VoxelSize = GetVoxelSizeInLayer(SVO::LAYER_LEAF);

	/** 障害物判定 **/
	TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes = {
		EObjectTypeQuery::ObjectTypeQuery1,	//! WorldStatic
	};
	TArray<AActor*> IgnoreActors = { this };
	TArray<AActor*> OverlapActors;
	for (int32 i = 0; i < NodeNum; i++) {

		int32 LeafNodeIndex = i >> SVO::BIT_LEAFLAYER;
		if (!mLeafNodes.Contains(LeafNodeIndex)) {
			mLeafNodes.Add(LeafNodeIndex, FSVOLeafNode());
		}

		/** 指定階層のノード座標を取得 **/
		FVector Location = GetNodeLocationInLayer(SVO::LAYER_LEAF, i);
		/** コリジョンがノード内に存在するか判定 **/
		if (USVOSystemLibrary::BoxOverlapActors(
			this,
			Location,
			VoxelSize * 0.5f,
			GetActorRotation(),
			ObjectTypes,
			nullptr,
			IgnoreActors,
			OverlapActors
		)) {
			mLeafNodes[LeafNodeIndex].SetBlock(i & SVO::BIT_LEAFNODEINDEX);
		}
	}
}
各層のノードの生成

各層のノードは先程生成したリーフノードをもとに
下層から上層に向けて、生成していきます。
リーフノードの値が0以外の場合、リーフノードには
コリジョンが含まれているということがわかります。
このときに、上層のノードへリンクづけを行います。

void ASVOBoundsVolume::GenerateLayerNodes() {
	/** 指定層分の領域確保 **/
	mNodeList.SetNum(mNumOfLayers+1);
	for (auto LeafNode : mLeafNodes) {
		int32 CurrentLayer = 0;
		while (CurrentLayer <= mNumOfLayers) {

			/** 子の階層とインデックスを計算 **/
			int32 ChildLayer = FMath::Max(0, CurrentLayer - 1);
			int32 ChildIndex = LeafNode.Key >> (SVO::BIT_LAYER * ChildLayer);

			if (CurrentLayer != 0) {
				/** 最下層でない場合は自身へのリンクを子ノードに設定する **/
				int32 ParentIndex = LeafNode.Key >> (SVO::BIT_LAYER * CurrentLayer);
				mNodeList[CurrentLayer - 1][ChildIndex].SetParent(FSVOLink(CurrentLayer, ParentIndex));
			}

			int32 NodeIndex = LeafNode.Key >> (SVO::BIT_LAYER * CurrentLayer);
			if (!mNodeList[CurrentLayer].Contains(NodeIndex)) {
				FSVONode Node;
				Node.SetLocation(GetNodeLocationInLayer(CurrentLayer, NodeIndex));
				mNodeList[CurrentLayer].Add(NodeIndex, Node);
			}

			if (!mNodeList[CurrentLayer][NodeIndex].HasAnyChildren()) {
				if (!LeafNode.Value.IsOpen()) {
					FSVOLink Child(ChildLayer, ChildIndex);
					if (CurrentLayer == 0) {
						/** 最下層のノードが均一でない場合はリーフノードの最初のサブノートインデックスを最初の子に設定 **/
						int32 SubNodeIndex = LeafNode.Key << SVO::BIT_LEAFNODEINDEX;
						Child.SetSubNodeIndex(SubNodeIndex);
					}
					mNodeList[CurrentLayer][NodeIndex].SetFirstChild(Child);
				}
			}
			CurrentLayer++;
		}
	}
}
隣接ノードのリンクづけ

上層までノードが生成されたので、次は上層から下層の順に
隣接ノードのリンクづけをしていきます。
この時点では均一なノードは削除していないので、ノードリストに含まれていない
モートンコードはボリューム外として、処理をスキップします。
均一なノード(子を持たないノード)だった場合は上層も均一かどうか判定し、
均一だった場合は上層のノードを隣接ノードとしてリンクを生成していきます。

void ASVOBoundsVolume::GenerateNeighbourLink() {

	int32 CurrentLayer = mNumOfLayers;
	while ( CurrentLayer >= 0 ) {
		for (auto& Node : mNodeList[CurrentLayer]) {
			/** モートンコードを1度座標に戻す **/
			FVector Location = Morton::Decode( Node.Key );
			for (int32 i = 0; i < SVO::Directions.Num(); i++) {
				/** 上下左右前後の6方向の座標でモートンコードに変換 **/
				int32 NeighborIndex = Morton::Code( Location + SVO::Directions[i] );
				FSVOLink Neighbour;
				if (FindNeighbour(CurrentLayer, NeighborIndex, Neighbour )) {
					Node.Value.AddNeighbour( Neighbour );
				}
			}
		}
		CurrentLayer--;
	}
}

bool ASVOBoundsVolume::FindNeighbour(int32 CurrentLayer, int32 NeighbourIndex, FSVOLink& Neighbour) {

	/** 隣接ノード情報をクリア **/
	Neighbour.Clear();

	if (!mNodeList[CurrentLayer].Contains(NeighbourIndex)) {
		/** 領域外の場合スキップ **/
		return false;
	}

	Neighbour.SetLayerIndex(CurrentLayer);
	Neighbour.SetNodeIndex(NeighbourIndex);
	if (mNodeList[CurrentLayer][NeighbourIndex].HasAnyChildren()) {
		if (CurrentLayer == 0 && mLeafNodes[NeighbourIndex].IsClosed()) {
			return false;
		}
		/** ノードが均一でない場合は、ここでストップ **/
		return true;
	}
	
	/** 均一な場合、均一な上層ノードを探索 **/
	int32 Layer = CurrentLayer+1;
	int32 Index = NeighbourIndex >> SVO::BIT_LAYER;
	while ( Layer <= mNumOfLayers ) {
		if (!mNodeList[CurrentLayer][Index].HasAnyChildren()) {
			Neighbour.SetLayerIndex(Layer);
			Neighbour.SetNodeIndex(Index);
		}
		Layer++;
		Index = Index >> SVO::BIT_LAYER;
	}
	return true;
}
均一ノードの削除

隣接ノードへのリンクを生成したら、最後に下層から上層に向けて
均一なノードの子ノードを削除していきます。

void ASVOBoundsVolume::Rasterize() {
	int32 CurrentLayer = 1;
	while ( CurrentLayer <= mNumOfLayers) {
		for ( auto Node : mNodeList[CurrentLayer] ) {
			if ( Node.Value.HasAnyChildren()) {
				continue;
			}
			/** 均一なノードの場合、子ノードは不要なので削除 **/
			for (int32 i = 0; i < SVO::NUM_CHILDREN; i++) {
				int32 RemoveNodeIndex = Node.Key << SVO::BIT_LAYER | i;
				mNodeList[CurrentLayer-1].Remove(RemoveNodeIndex);
				if (CurrentLayer-1 == 0) {
					/** 最下層の場合はリーフノードも削除 **/
					mLeafNodes.Remove(RemoveNodeIndex);
				}
			}
		}
		CurrentLayer++;
	}
}

まとめ

ここまで見ていただきありがとうございました。
昔に作ったプロジェクトを引っ張り出してきて確認してみたら、
リーフノードの部分完全に抜けており、
「あれ?これ全然違うな。」となり、大慌てで作り直してました。

続編については年内に投稿予定です。

明日は @nokonoko_08 さんの記事になります。
果たして上司に脅されずに済んだのでしょうか。

ゲーム内の1日のサイクルをつくる

  • はじめに
  • サンプルプロジェクト
    • バージョン
  • 実装解説
    • DayCycleGameStateBase.h
    • DayCycleGameStateBase.cpp
    • W_DayCycle
    • BP_DayCycleSky
  • まとめ

はじめに

シミュレーションゲーム等で
必要となってくる、ゲーム内のサイクルの構築について説明いたします。

サンプルプロジェクト

GooglDriveにてサンプルプロジェクトを配布しています。
作りに不明な点ありましたらご連絡ください。

drive.google.com

バージョン

ソースコードも含めていますので、ビルド環境が必要となります。
以下の環境で作成しました。
UE4 Ver.4.24.0
Microsoft VisualStudio Community 2017 - Ver.15.9.5

続きを読む

ウェイポイントの経路探索をA*アルゴリズムでやってみました。

この記事はUnreal Engine 4(UE4) #2 Advent Calendar 2019の15日目の投稿記事です。 qiita.com

ごりです。 久々の投稿で、初のアドカレ参加となります。
わかりにくいところ等ありましたら
連絡をいただけると助かります。

  • はじめに
  • サンプルプロジェクト作成しました
    • バージョン
    • 動かしてみる
  • 実装解説
    • CityData.h/cpp
      • ノードの定義
      • ノードの管理
    • WayPoint.h/cpp
    • PathFindComponent.h/cpp
  • まとめ

はじめに

A*アルゴリズムとは、探索アルゴリズムの一種です。
スタートノード(開始地点)からゴールノード(目標地点)までの経路を計算し、
この経路が最短であることを保証するアルゴリズムとなります。
今回はレベルに配置するウェイポイントをノードとして
A*アルゴリズムの経路探索を行っていきます。

続きを読む

GameplayAbility - GameplayAbility と コンポーネント の準備編

こんばんは、ごりです。
今日は、こちらの記事の続きからです。
goolee.hatenablog.com

今回は、実際に使う GameplayAbility と Component のクラスを作っていきます。

f:id:m-goolee-y:20181129224335p:plain

コンテンツブラウザの 新規C++クラスをクリック。
全てのクラスを表示にチェックし、 GameplayAbility と打ちます。

f:id:m-goolee-y:20181201182725p:plain

GameplayAbility を選択し、次へ。
ファイル名は 「GameplayAbilityBase」 とします。

続いて、新規C++クラスから 「AbilitySystemComponent」と打ちます。
ファイル名は 「AbilitySystemComponentBase 」とします。

f:id:m-goolee-y:20181201183413p:plain

作成が終わったら、
Component に処理を追加していきます。
今回もソースコードを記述します。

まず AbilitySystemComponentBase.h

/**
*@file	 AbilitySystemComponentBase.h
*@brief  GameplayAbility を用いるためのComponent
*@author goolee
*@date	 2018/12/01
*/
#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "GameplayTagContainer.h"
#include "AbilitySystemComponentBase.generated.h"

class UGameplayAbilityBase;

/**
 * GameplayAbility を扱う Component 
 * ゲーム中に指定したデータを扱うために、AbilitySystemComponent を拡張
 */
UCLASS()
class GPASTUDY_API UAbilitySystemComponentBase : public UAbilitySystemComponent
{
	GENERATED_BODY()
	
public:

	/**
	 *@fn
	 *コンストラクタ
	 */
	UAbilitySystemComponentBase();

	/**
         *@fn
         *現在実行中の Ability の中から指定した GameplayTag と一致するものすべて取得
         *@param  (GameplayTagContainer) 指定するGameplayTagの集まり
         *@return (ActiveAbilities) 指定したタグと一致したAbility
         */
	void GetActiveAbilitiesWithTags( const FGameplayTagContainer& GameplayTagContainer, TArray<UGameplayAbilityBase*>& ActiveAbilities );
	
	
};

つづいて AbilitySystemComponent.cpp

#include "AbilitySystemComponentBase.h"
#include "GameplayAbilityBase.h"

UAbilitySystemComponentBase::UAbilitySystemComponentBase() {}

void UAbilitySystemComponentBase::GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<UGameplayAbilityBase*>& ActiveAbilities) {

	TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
	GetActivatableGameplayAbilitySpecsByAllMatchingTags( GameplayTagContainer, AbilitiesToActivate, false );

	// Iterate the list of all ability specs
	for (FGameplayAbilitySpec* Spec : AbilitiesToActivate)
	{
		// Iterate all instances on this ability spec
		TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();

		for (UGameplayAbility* ActiveAbility : AbilityInstances)
		{
			ActiveAbilities.Add(Cast<UGameplayAbilityBase>(ActiveAbility));
		}
	}
}

GameplayAbilityBase には今回は処理を追加しません。

ではこのComponentとAbility をキャラクターに持たせて、
GameplayAbilityを使えるようにしていきます。

CharacterBase.h と CharacterBase.cpp です。

/**
*@file	 CharacterBase.h
*@brief  GameplayAbility を用いる キャラクターの基底クラス
*@author goolee
*@date	 2018/12/01
*/
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AttributeSetBase.h"
#include "AbilitySystemInterface.h"
#include "AbilitySystemComponentBase.h"
#include "CharacterBase.generated.h"

UCLASS()
class GPASTUDY_API ACharacterBase : public ACharacter, public IAbilitySystemInterface
{
	GENERATED_BODY()

public:
	
	/**
	 * @fn
	 * コンストラクタ
	 */
	ACharacterBase();

  /**
	 *@fn
	 *コントローラ所有された際の処理
	 *@param このキャラクターを所有するコントローラ
	 *
	 *前回、この処理書いてなかったです、すみません。
	 */
	virtual void PossessedBy(AController* NewController) override;

protected:

	/**
	 * GameplayAbilityを扱うコンポーネント
	 */
	UPROPERTY()
	UAbilitySystemComponentBase* AbilitySystem;

	/**
	 * キャラクターのステータス 
	 * ブループリントから呼び出しはできないが、
	 * ガベージコレクションに追加するため UPROPERTY() を記述
	 */
	UPROPERTY()
	UAttributeSetBase* AttributeSet;

	/**
	 *キャラクター生成時から実行可能なAbility
	 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Abilities")
	TArray<TSubclassOf<UGameplayAbilityBase>> Abilities;

	/**
	 * @fn
	 * ゲーム開始時や、生成時に呼ばれる処理
	 */
	virtual void BeginPlay() override;

	/**
	 * @fn
	 * キャラクターの体力を取得
	 * @return キャラクターの体力
	 */
	UFUNCTION(BlueprintCallable)
	virtual float GetHealth() const;

public:	

	/**
	 * @fn
	 * 毎フレーム呼ばれる処理
	 */
	virtual void Tick(float DeltaTime) override;

	/**
	 *@fn
	 *AbilitySystemComponentを取得
	 *@return AbilitySystemComponent
	 */
	UAbilitySystemComponent* GetAbilitySystemComponent() const override;

	/**
	 *@fn
	 *指定したタグと一致するAbilityを実行
	 *@param  (AbilityTags)				実行するAilityがもつGameplayTag
	 *@param  (bAllowRemoteActivation)	true...ローカル/サーバーで実行 false...ローカルでのみ実行
	 *@return true...実行に成功 false...失敗
	 */
	UFUNCTION(BlueprintCallable, Category = "Abilities")
	bool ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation = true);

	/**
	*@fn
	*キャラクターが現在実行中の Ability の中から指定した GameplayTag と一致するものすべて取得
	*@param  (AbilityTags)     指定するGameplayTagの集まり
	*@return (ActiveAbilities) 指定したタグと一致したAbility
	*/
	UFUNCTION(BlueprintCallable, Category = "Abilities")
	void GetActivateAbilitiesWithTags( FGameplayTagContainer AbilityTags, TArray<UGameplayAbilityBase*>& ActiveAbilities);

};
#include "CharacterBase.h"
#include "GameplayAbilityBase.h"


ACharacterBase::ACharacterBase(){
    //trueにすると毎フレームTick関数を呼び出す設定
	PrimaryActorTick.bCanEverTick = true;
	
	//AbilitySystemConponentの生成
	AbilitySystem = CreateDefaultSubobject<UAbilitySystemComponentBase>(TEXT("AbilitySystem"));
	//マルチプレイヤー用の設定
	AbilitySystem->SetIsReplicated(true);

	//AttributeSet の生成
	AttributeSet = CreateDefaultSubobject<UAttributeSetBase>(TEXT("AttributeSet"));
}

void ACharacterBase::PossessedBy(AController* NewController) {
	Super::PossessedBy( NewController );

	if (AbilitySystem) {
		//AbilitySytemを持つ Actor情報と このAbilitySystemで動く Actor情報の初期化 
		AbilitySystem->InitAbilityActorInfo(this, this);
	}
}

void ACharacterBase::BeginPlay() {

	Super::BeginPlay();
	
	/**
	 * AbilitySytemに使えるAbilityを登録
	 * これをしないとTagでAbilityを指定しても実行されない。
	 */
	if (AbilitySystem) {
		for (auto Ability : Abilities) {
			AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability, 1, INDEX_NONE, this));
		}
	}

}

void ACharacterBase::Tick(float DeltaTime){

	Super::Tick(DeltaTime);

}

float ACharacterBase::GetHealth() const {
	return AttributeSet->GetHealth();
}

UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent() const {
	return AbilitySystem;
}

bool ACharacterBase::ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation) {

	if (AbilitySystem) {
		return AbilitySystem->TryActivateAbilitiesByTag( AbilityTags, bAllowRemoteActivation );
	}
	return false;

}

void ACharacterBase::GetActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UGameplayAbilityBase*>& ActiveAbilities) {
	
	if (AbilitySystem) {
		AbilitySystem->GetActiveAbilitiesWithTags( AbilityTags, ActiveAbilities );
	}

}

ここまで出来たら、ビルドして BP_CharacterBase を見てみましょう。
コンポーネントに AbilitySystemが追加され、
f:id:m-goolee-y:20181201195815p:plain

デフォルトの詳細に、Abilitiesが増えているのがわかりますね。
f:id:m-goolee-y:20181201200113p:plain

これで GameplayAbilityを使う準備は整いました。

次回から使い方を書いていきたいと思います。
その都度、機能拡張をしていきます。

ソースコードのコメントに、説明書きましたが、
不明な点があれば、ご連絡ください。

ではでは。




GameplayAbility - AttributeSet の準備編

こんばんは、ごりです。

今日はこちらの記事の続きからです。
goolee.hatenablog.com


今回は GameplayAbility を使う上で必要となってくるパラメータをまとめたクラスの準備をしていきます。
UE4サンプルの ActionRPG では キャラクターのステータスとして使っています。

前回作成した GPAStudy プロジェクトを起動して、 新規C++クラスを追加します。
f:id:m-goolee-y:20181129224335p:plain

右上の 全てのクラスを表示にチェックをして AttributeSet と検索します。

f:id:m-goolee-y:20181201000909p:plain

「AttributeSet」を選択して次へをクリック。

ファイル名は 「AttributeSetBase」 とします。
今回はソースコードを記述していきます。
説明はコメントで省略いたします。
まず AttributeSetBase.h です。

/**
*@file	 AttributeSetBase.h
*@brief  GameplayAbility で用いるパラメータの集合
*@author goolee
*@date	 2018/12/01
*/
#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "AttributeSetBase.generated.h"

/** 
 *  @def
 *  AttributeSet.h で定義されている
 *  Attribute への Setter, Getter を定義するためのマクロ
 */
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

/**
 * @class
 * GameplayAbilityで用いるキャラクターのパラメータをまとめたクラス
 */
UCLASS()
class GPASTUDY_API UAttributeSetBase : public UAttributeSet
{
	GENERATED_BODY()
	
public:

	/**
	 * @fn
	 * コンストラクタ
	 */
	UAttributeSetBase();

	/**
	 * Blueprintから読み取り可能な キャラクターの体力を表す変数
	 * マクロで Setter,Getterを定義しているので、 SetHealth(), GetHealth()が呼び出し可能
	 */
	UPROPERTY(BlueprintReadOnly, Category="Health", ReplicatedUsing = OnRep_Health)
	FGameplayAttributeData Health;
	ATTRIBUTE_ACCESSORS( UAttributeSetBase, Health )

	/**
	 * @fn
	 * レプリケートされるAttributeSetの変数を取得する
	 * @brief  この関数の中で DOREPLIFTIME マクロを用いてレプリケートされた変数として追加する
	 * @return (OutLifeTimeProps) 
	 */
	virtual void GetLifetimeReplicatedProps( TArray<FLifetimeProperty>& OutLifetimeProps ) const override;

protected:

	/**
	 * @fn
	 * Healthの値が変更された際に呼び出される処理
	 */
	UFUNCTION()
	virtual void OnRep_Health();
};

つづいて、 AttributSet.cpp です。

#include "AttributeSetBase.h"
#include "Net/UnrealNetwork.h"

UAttributeSetBase::UAttributeSetBase() : Health(10.0f){
}

void UAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME( UAttributeSetBase, Health );
}

void UAttributeSetBase::OnRep_Health() {
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, Health);
}

GetLifetimeReplicatedProps() と OnRepHealth() は マルチプレイヤーゲームを
作る際に必要となる処理です。
ブログを読んでいる方でマルチプレイヤーゲームを作りたい方は
こちらを記述してください。
そうでない方は、この二つの関数を記述していなくても大丈夫です。
間違ってたら連絡ください...

ではこのAttributeSetをキャラクターに持たせていきます。
前回作成した CharacterBase クラスに書いていきます。
まず CharacterBase.h です。

/**
*@file	 CharacterBase.h
*@brief  GameplayAbility を用いる キャラクターの基底クラス
*@author goolee
*@date	 2018/12/01
*/
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AttributeSetBase.h"
#include "CharacterBase.generated.h"

UCLASS()
class GPASTUDY_API ACharacterBase : public ACharacter
{
	GENERATED_BODY()

public:
	
	/**
	 * @fn
	 * コンストラクタ
	 */
	ACharacterBase();

protected:

	/**
	 * キャラクターのステータス 
	 * ブループリントから呼び出しはできないが、
	 * ガベージコレクションに追加するため UPROPERTY() を記述
	 */
	UPROPERTY()
	UAttributeSetBase* AttributeSet;

	/**
	 * @fn
	 * ゲーム開始時や、生成時に呼ばれる処理
	 */
	virtual void BeginPlay() override;

	/**
	 * @fn
	 * キャラクターの体力を取得
	 *@return キャラクターの体力
	 */
  UFUNCTION(BlueprintCallable)
	virtual float GetHealth() const;

public:	

	/**
	 * @fn
	 * 毎フレーム呼ばれる処理
	 */
	virtual void Tick(float DeltaTime) override;

};

キャラクターに AttributeSetBase 型の変数と 
AttributeSet内の体力を取得する関数を宣言しました。

つづいて CharacterBase.cpp です。

#include "CharacterBase.h"

ACharacterBase::ACharacterBase()
{
    //trueにすると毎フレームTick関数を呼び出す設定
	PrimaryActorTick.bCanEverTick = true;

	//AttributeSet の生成
	AttributeSet = CreateDefaultSubobject<UAttributeSetBase>(TEXT("AttributeSet"));
}

void ACharacterBase::BeginPlay()
{
	Super::BeginPlay();
	
}

void ACharacterBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

float ACharacterBase::GetHealth() const {
	return AttributeSet->GetHealth();
}

今回ソースコードに追加するのはここまでです。
ここまで書いたらビルドしてプロジェクトに戻ってください。

コンテンツブラウザの C++クラスフォルダ内の CharacterBase を右クリックして
「CharacterBaseに基づくブループリントクラスを作成します」をクリック。
名前は 「BP_CharacterBase」とします。
フォルダは わかりやすくコンテンツ直下にします。

f:id:m-goolee-y:20181201021037p:plain

作成されると、BP_CharacterBaseが開かれると思います。
イベントグラフで 右クリックして GetHealth と打ってみましょう。

f:id:m-goolee-y:20181201022000p:plain

無事にでてきましたね。

BeginPlay から printString で 値の確認をしてみましょう。

f:id:m-goolee-y:20181201022119p:plain

起動時の ThirdPersonExampleMap に追加して、実行してみます。

f:id:m-goolee-y:20181201022354p:plain

上の写真のように表示されればOKです。


これでAttributeSetの準備が完了しました。
今回はここまでとします。

次回からは GameplayAbility を使うためのコンポーネントを準備していきます。

今日の内容でわからないことがあればご連絡ください。

ではでは。

GameplayAbility のための準備はじめました。

こんばんは、ごりです。

これからは、GameplayAbilityの使い方を色々な機能を作って
説明していきたいと思います。

今回は GameplayAbility を使うためのプロジェクトの準備をしていきます。

まず作成から。

f:id:m-goolee-y:20181129221106p:plain

C++の Thirdpersonテンプレートプロジェクトを作成してください。

プロジェクト名はなんでもいいですが今回は「 GPAStudy 」 とします。

f:id:m-goolee-y:20181129222123p:plain
プロジェクトを作成したら、GameplayAbilitySystemプラグインを有効にします。


以前はエンジンの機能に含まれていたみたいですが、

現在は Pluginとして機能が用意されています。

有効にしてプロジェクトを再起動します。

 
再起動後は,プロジェクトの Source/GPAStudy にある、 

「GPAStudy.Build.cs」を編集します。

f:id:m-goolee-y:20181129222455p:plain

これはプロジェクトビルド時にビルドするモジュールを設定するためのファイルです。

GamplayAbilities のモジュールがビルドされるように

PublicDependencyModuleNames.AddRange の下に 
 以下のソースコードを追加してください。

PrivateDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks"}); 

モジュールの設定が終わったら、最後にGameplayAbility を使うキャラクターの基底クラスを作成します。
コンテンツブラウザの新規追加から、新規C++クラスをクリックし、
f:id:m-goolee-y:20181129224335p:plain

親クラスに Character を選択したら 次へ をクリックします。
f:id:m-goolee-y:20181129224523p:plain

ファイル名は 「CharacterBase」とします。
クラスを作成をクリックするとコンパイルが始まります。

コンパイルが完了すると コンテンツブラウザの C++ クラスフォルダに CharacterBaseが追加されていますね。
f:id:m-goolee-y:20181129225133p:plain

今回はここまでにします。
何か質問ありましたらご連絡ください。

次回、CharacterBase に AttributeSet を追加していきます。
その時に、AttributeSet についても説明いたします。

ではでは。