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を使用してプレイヤーの周囲をぐるぐる移動させる様子です。
自分で実装した部分は、プレイヤー周辺を移動する処理のみとなります。
ブログ用 pic.twitter.com/Wf7cIxceld
— goolee (@goolee07) 2023年5月31日
アクター以外を注目したい場合
AAIController::SetFocalPoint の処理があり、特定アクターではなく、
特定座標へ注目させることもできます。
まとめ
SetFocusはAIControllerが特定のアクターや座標に注意を向けるための便利な処理です。
簡単な使い方と流れをまとめました。
不明な点や間違ってる点などありましたら、連絡をいただければ幸いです。
明日は mokoさんの UE5 エミッシブの値を露出に関わらず一定に保つ です。 お楽しみに!
3次元の経路探索 - データ作成編
この記事は qiita.comUnreal Engine 4(UE4) #1 Advent Calendar 2020
の23日目の投稿記事です。
ごりです。久々の投稿になります。
3次元の経路探索を何回かに分けて書かせていただきます。
今回はデータの作成編です。
はじめに
この記事は、下記を参考に作成したものになります。
誤っている点などございましたら、連絡いただけると助かります。
サンプルプロジェクト
下記で配布しています。
drive.google.com
バージョン
ソースコードも含めていますので、ビルド環境が必要となります。
以下の環境で作成しました。
UE4 Ver.4.25.4
Microsoft Visual Studio Community 2019 Version 16.8.3
使い方
プロジェクトを開くとSVOBoundsVolumeが置かれたレベルが開きます。
詳細タブのカテゴリ【SVO】で階層数を指定し、Generateボタンを押すと、SVOが構築されます。
カテゴリ【デバッグ】ではそれぞれチェックを入れることで可視化することができます。
全てにチェックをいれると、このようになります。
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が構築されます。
リーフノードの生成
リーフノードは最下層から更に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にてサンプルプロジェクトを配布しています。
作りに不明な点ありましたらご連絡ください。
バージョン
ソースコードも含めていますので、ビルド環境が必要となります。
以下の環境で作成しました。
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
- アルゴリズムの流れ
- CityData.h/cpp
- まとめ
はじめに
A*アルゴリズムとは、探索アルゴリズムの一種です。
スタートノード(開始地点)からゴールノード(目標地点)までの経路を計算し、
この経路が最短であることを保証するアルゴリズムとなります。
今回はレベルに配置するウェイポイントをノードとして
A*アルゴリズムの経路探索を行っていきます。
GameplayAbility - GameplayAbility と コンポーネント の準備編
こんばんは、ごりです。
今日は、こちらの記事の続きからです。
goolee.hatenablog.com
今回は、実際に使う GameplayAbility と Component のクラスを作っていきます。
コンテンツブラウザの 新規C++クラスをクリック。
全てのクラスを表示にチェックし、 GameplayAbility と打ちます。
GameplayAbility を選択し、次へ。
ファイル名は 「GameplayAbilityBase」 とします。
続いて、新規C++クラスから 「AbilitySystemComponent」と打ちます。
ファイル名は 「AbilitySystemComponentBase 」とします。
作成が終わったら、
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が追加され、
デフォルトの詳細に、Abilitiesが増えているのがわかりますね。
これで GameplayAbilityを使う準備は整いました。
次回から使い方を書いていきたいと思います。
その都度、機能拡張をしていきます。
ソースコードのコメントに、説明書きましたが、
不明な点があれば、ご連絡ください。
ではでは。
GameplayAbility - AttributeSet の準備編
こんばんは、ごりです。
今日はこちらの記事の続きからです。
goolee.hatenablog.com
今回は GameplayAbility を使う上で必要となってくるパラメータをまとめたクラスの準備をしていきます。
UE4サンプルの ActionRPG では キャラクターのステータスとして使っています。
前回作成した GPAStudy プロジェクトを起動して、 新規C++クラスを追加します。
右上の 全てのクラスを表示にチェックをして AttributeSet と検索します。
「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」とします。
フォルダは わかりやすくコンテンツ直下にします。
作成されると、BP_CharacterBaseが開かれると思います。
イベントグラフで 右クリックして GetHealth と打ってみましょう。
無事にでてきましたね。
BeginPlay から printString で 値の確認をしてみましょう。
起動時の ThirdPersonExampleMap に追加して、実行してみます。
上の写真のように表示されればOKです。
これでAttributeSetの準備が完了しました。
今回はここまでとします。
次回からは GameplayAbility を使うためのコンポーネントを準備していきます。
今日の内容でわからないことがあればご連絡ください。
ではでは。
GameplayAbility のための準備はじめました。
こんばんは、ごりです。
これからは、GameplayAbilityの使い方を色々な機能を作って
説明していきたいと思います。
今回は GameplayAbility を使うためのプロジェクトの準備をしていきます。
まず作成から。
C++の Thirdpersonテンプレートプロジェクトを作成してください。
プロジェクト名はなんでもいいですが今回は「 GPAStudy 」 とします。
プロジェクトを作成したら、GameplayAbilitySystemプラグインを有効にします。
以前はエンジンの機能に含まれていたみたいですが、
現在は Pluginとして機能が用意されています。
有効にしてプロジェクトを再起動します。
再起動後は,プロジェクトの Source/GPAStudy にある、
「GPAStudy.Build.cs」を編集します。
これはプロジェクトビルド時にビルドするモジュールを設定するためのファイルです。
GamplayAbilities のモジュールがビルドされるように
PublicDependencyModuleNames.AddRange の下に
以下のソースコードを追加してください。
PrivateDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks"});
モジュールの設定が終わったら、最後にGameplayAbility を使うキャラクターの基底クラスを作成します。
コンテンツブラウザの新規追加から、新規C++クラスをクリックし、
親クラスに Character を選択したら 次へ をクリックします。
ファイル名は 「CharacterBase」とします。
クラスを作成をクリックするとコンパイルが始まります。
コンパイルが完了すると コンテンツブラウザの C++ クラスフォルダに CharacterBaseが追加されていますね。
今回はここまでにします。
何か質問ありましたらご連絡ください。
次回、CharacterBase に AttributeSet を追加していきます。
その時に、AttributeSet についても説明いたします。
ではでは。