UE4 UMGのコンストラクタ/デストラクタが呼ばれるタイミング

やあ

UMG(WidgetBlueprint)のコンストラクタが呼ばれるタイミングを調べてみたよ。
間違ってたら教えてください。

Constructの呼ばれるタイミング

ウィジェットViewportに追加されたときに呼ばれます。
こんな感じのWBPがある。
f:id:pto8913:20210215001533p:plain

呼ばれる例
AddToViewportしたとき。
f:id:pto8913:20210215001530p:plain

PanelWidget(BorderやVerticalBoxなど)にウィジェットAddChildしたとき。
f:id:pto8913:20210215001536p:plain

呼ばれない例
VisibilityRenderOpacityなどを切り替えるだけでは呼ばれません。
f:id:pto8913:20210215001540p:plain

CreateWidgetでも呼ばれません。
f:id:pto8913:20210215001527p:plain

Destructの呼ばれるタイミング

PanelWidgetからRemoveChildしたり、RemoveFromViewportしたりしたとき。
f:id:pto8913:20210215001536p:plain

注意

親UMGがAddToViewportされたりAddChildされないと、子UMGのConstructは呼ばれないので注意。

UE4 UMG WBPをWBPに直接配置するとオブジェクトの数が増える話

やあ

調べ方が悪いんだろうけど、調べてもなにも情報が出てこなかったのでメモしておく。

使ったプロジェクト
github.com

問題

例えば、UserWidgetから継承したクラス、WBP_CustomButtonWBP_ChoiceUIがあったとする。
このWBP_ChoiceUIWBP_CustomButtonをそのまま配置する。
f:id:pto8913:20210209153251p:plain
f:id:pto8913:20210209153456p:plain

これをこんな風に出したり消したりする。
f:id:pto8913:20210209163321p:plain

スタンドアローンで実行してMemreport -full
f:id:pto8913:20210209160259p:plain WBP_ChoiceUIを表示中のレポート
f:id:pto8913:20210209154858p:plain

WBP_CustomButtonを3つしか配置してないのに、表示中のレポートではWBP_CustomButtonのオブジェクトの数が9つになっているのがわかる。

解決策

これを回避するには、すべてのWBP_CustomButtonCreateWidgetすればいい。
f:id:pto8913:20210209155141p:plain
f:id:pto8913:20210209155749p:plain

f:id:pto8913:20210209160433p:plain WBP_ChoiceUIを表示中のレポート
f:id:pto8913:20210209155917p:plain
直接配置していたのをCreateWidgetで表示するようにしただけでオブジェクトの数が3つになった。

おわりに

割と重大な情報っぽいのに調べても何も出てこない(多分調べ方が悪い)ので、みんなそんなに気にしてないのかなぁと思いました。
それともCreateWidget以外に何か回避策があるのかな?

知ってる人は教えてください。

C++ 関数の配列

やあ

関数の配列ってどうやるんだろーって調べながら書いてみた。
間違ってるところとか、もっといい書き方あれば教えてください。

メンバ関数の配列

FuncArray.h

#pragma once

#include <vector>

class FuncArray
{
public:
    FuncArray();

    using PtoIntNoParamFunc = int (FuncArray::*) () const;
    // typedef int (FuncArray::*PtoIntNoParamFunc) ();

    using PtoVoidOneIntParamFunc = void (FuncArray::*) (const int&);
    // typedef void (FuncArray::*PtoVoidOneIntParamFunc) (const int&);

    void ShowSpecs() const;

    std::vector<PtoVoidOneIntParamFunc> GetAddFuncs() {
        return AddFuncs;
    }

protected:
    int A;
    int GetA() const;
    void AddA(const int& In);

    int B;
    int GetB() const;
    void AddB(const int& In);

    int C;
    int GetC() const;
    void AddC(const int& In);

    std::vector<PtoIntNoParamFunc> GetFuncs = {
        &FuncArray::GetA, &FuncArray::GetB, &FuncArray::GetC
    };

    std::vector<PtoVoidOneIntParamFunc> AddFuncs = {
        &FuncArray::AddA, &FuncArray::AddB, &FuncArray::AddC
    };
};

FuncArray.cpp

#include "FuncArray.h"
#include <iostream>

FuncArray::FuncArray()
{
    A = 10;
    B = 4;
    C = 111;
}

int FuncArray::GetA() const
{
    return A;
}

int FuncArray::GetB() const
{
    return B;
}

int FuncArray::GetC() const
{
    return C;
}

void FuncArray::AddA(const int& In)
{
    A += In;
}

void FuncArray::AddB(const int& In)
{
    B += In;
}

void FuncArray::AddC(const int& In)
{
    C += In;
}

void FuncArray::ShowSpecs() const
{
    for (auto&& Func : GetFuncs)
    {
        std::cout << (this->*Func)() << std::endl;
    }
}

Main.cpp

#include "FuncArray.h"

int main()
{
    FuncArray* FuncArrayTest = new FuncArray();
    int Idx = 0;
    for (auto&& func : FuncArrayTest->GetAddFuncs())
    {
        (FuncArrayTest->*func)(Idx);
        ++Idx;
    }
    FuncArrayTest->ShowSpecs();
}

グローバル関数の配列

Main.cpp

using PtoFuncArrayTest = void(*) ();

void FuncA()
{
    std::cout << "FuncA()" << std::endl;
}
void FuncB()
{
    std::cout << "FuncB()" << std::endl;
}
void FuncC()
{
    std::cout << "FuncC()" << std::endl;
}

int main()
{
    PtoFuncArrayTest funcs[] = {
        FuncA, FuncB, FuncC
    };
    for (auto&& func : funcs)
    {
        func();
    }
    return 0;
}

UE4 C++ GameplayAbilitySystemを勉強していくPart.3-4 GameplayEffectExectuionCalculation

やあ

GameplayEffectExectuionCalculationについてのメモ
以下GEECと略します。
間違ってるところなどあれば教えてください。

f:id:pto8913:20210126112658p:plain

目次

キャプチャするAttribute

キャプチャするAttributeの宣言

今回はほぼすべてのゲームにある、最大HPをキャプチャしてみる。
まず、キャプチャしたいすべてのAttributeを宣言するための構造体の作成。
GEEC_Test.h

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "GEEC_Test.generated.h"

UCLASS()
class TESTPROJECT_API UGEEC_Test : public UGameplayEffectExecutionCalculation
{
    GENERATED_BODY()
};

GEEC_Test.cpp

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "Abilities/GEEC_Test.h"

struct TestStatics {
    /* キャプチャしたいAttributeの宣言 */
    DECLARE_ATTRIBUTE_CAPTUREDEF(MaxHealth);

    /* コンストラクタ */
    TestStatics()
    {
        /* 
       @ param1 : UTestAttributeSet : 自分で作ったAttributeSetクラスを指定 
       @ param2 : MaxHealth : UTestAttributeSet内にあるキャプチャしたいAttributeの名前。上で宣言したものと同じ名前
       @ param3 : Source : キャプチャする対象。あとで説明します。
       @ param4 : true : スナップショットするかどうか。あとで説明します
       */
        DEFINE_ATTRIBUTE_CAPTUREDEF(UTestAttributeSet, MaxHealth, Source, true);
    }
};

static const TestStatics& Test()
{
    static TestStatics test;
    return test;
}

キャプチャするAttributeをGEECの内部で処理できるようにする(超大事)

GEECのコンストラクタで、
RelevantAttributesToCaptureにキャプチャするAttributeを追加することで、内部から値を処理できるようになる。

InvalidScopedModifierAttributesに追加すると、値のキャプチャはできるけど、エディタから処理もできないし、値も処理できないようになる。
と思ってたのですがどうもちがうっぽい

これを忘れて、値が変わらないよーって泣くことにならないように覚えておく。

GEEC_Test.hに追加

/* コンストラクタ */
GEEC_Test();

GEEC_Test.cppに追加

UGEEC_Test::UGEEC_Test()
{
    /* 宣言したAttributeをエディタに公開する画像.2参照 */
    RelevantAttributesToCapture.Add(Test().MaxHealthDef);
    /* 宣言だけしてエディタに公開しない */
    // InvalidScopedModifierAttributes.Add(Test().MaxHealthDef);
}

画像.2 f:id:pto8913:20210126114548j:plain

RelevantAttributesToCaptureに追加することで画像.2の赤枠内に追加される。

目次に戻る

Attributeをキャプチャする対象

TestStaticsのコンストラクタのparam3部分

/* コンストラクタ */
TestStatics()
{
    /* 
   @ param1 : UTestAttributeSet : 自分で作ったAttributeSetクラスを指定 
   @ param2 : MaxHealth : UTestAttributeSet内にあるキャプチャしたいAttributeの名前。上で宣言したものと同じ名前
   @ param3 : Source : キャプチャする対象。
   @ param4 : true : スナップショットするかどうか。あとで説明します
   */
    DEFINE_ATTRIBUTE_CAPTUREDEF(UTestAttributeSet, MaxHealth, Source, true);
}

このparam3でキャプチャする対象を選択する。
マクロで隠されているけど、実際はEGameplayEffectAttributeCaptureSource::Sourceという列挙体で、
EGameplayEffectAttributeCaptureSource::Source
EGameplayEffectAttributeCaptureSource::Target
の二つがある。

EGameplayEffectAttributeCaptureSource::Source

これは、このGEECを持つGameplayEffectの実行者です。
例1.
プレイヤーが攻撃して、敵にダメージを与えるGameplayEffectの実行者はプレイヤーになる。
画像.3参照

例2.
ゲーム開始時に、キャラクターのステータスを決定するGameplayEffectの実行者はそのキャラクター自身になる。

EGameplayEffectAttributeCaptureSource::Target

これは、このGEECを持つGameplayEffectを適用するターゲットのことです。
プレイヤーが攻撃して、敵にダメージを与えるGameplayEffectを適用されるのは、敵なので、ターゲットは敵になる。
画像.3参照

例2.
ゲーム開始時に、キャラクターのステータスを決定するGameplayEffectのターゲットはそのキャラクター自身になる。

画像.3 f:id:pto8913:20210126120545p:plain

目次に戻る

キャプチャするAttributeをスナップショットするかどうか

正直スナップショットについては認識があってるかわからないです。間違ってたらごめんなさい

キャプチャするAttribute
TestStaticsのコンストラクタのparam4部分

/* コンストラクタ */
TestStatics()
{
    /* 
   @ param1 : UTestAttributeSet : 自分で作ったAttributeSetクラスを指定 
   @ param2 : MaxHealth : UTestAttributeSet内にあるキャプチャしたいAttributeの名前。上で宣言したものと同じ名前
   @ param3 : Source : キャプチャする対象。
   @ param4 : true : スナップショットするかどうか。
   */
    DEFINE_ATTRIBUTE_CAPTUREDEF(UTestAttributeSet, MaxHealth, Source, true);
}

このparam4でスナップショットするかどうかを決める。

このGEECを持つGameplayEffectのスペックGameplayEffectSpecを作成した瞬間に、キャプチャしたAttributeを保持するかどうか。


キャラクターが火の玉の魔法を、敵に向けて発射した場合。

理由 :
火の玉を発射した時に魔法攻撃力が5だったが、火の玉が敵に当たった時には、キャラクターの魔法攻撃力が10になっていた場合に、敵が受けるダメージは5であるはずが、10になるのを防ぐためです。

スナップショットをすると、キャラクターが魔法を使ったときに、魔法攻撃力が5なら、敵に魔法が当たる前に魔法攻撃力が変化しても、敵に魔法が当たった時には、魔法攻撃力が5のまま計算されます。
f:id:pto8913:20210126123458p:plain

目次に戻る

実装の話

キャプチャしたAttributeの取得

GEEC_Test.hに追加

virtual void Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput
) const override;

GEEC_Test.cppに追加

void UGEEC_EnemyStatus::Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput
) const
{
    const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = SourceTags;
    EvaluationParameters.TargetTags = TargetTags;

    float BaseValue_MaxHealth = 0.f;
    /* ModifierMagnitudeを処理する前の値 */
    ExecutionParams.AttemptCalculateCapturedAttributeBaseValue(TargetSt().MaxHealthDef, BaseValue_MaxHealth);

    float BonusMaxHealth = 0.f;
    /* キャプチャしたAttriuteのModifierMagnitudeの取得 画像.6参照 */
    ExecutionParams.AttemptCalculateCapturedAttributeBonusMagnitude(Test().MaxHealthDef, EvaluationParameters, BonusMaxHealth);

    float MaxHealth = 0.f;
    /* キャプチャしたAttributeの取得 画像.5参照 */
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(Test().MaxHealthDef, EvaluationParameters, MaxHealth);
}

画像.5 f:id:pto8913:20210126112850p:plain
画像.6 f:id:pto8913:20210126131250p:plain

AttemptCalculateCapturedAttributeBaseValue

float BaseValue_MaxHealth = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeBaseValue(TargetSt().MaxHealthDef, BaseValue_MaxHealth);

キャプチャしたAttributeの元の値。


キャラクターのMaxHealth : 100に20追加するGameplayEffectの場合、元の値は100になる

AttemptCalculateCapturedAttributeBonusMagnitude

float BonusMaxHealth = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeBonusMagnitude(Test().MaxHealthDef, EvaluationParameters, BonusMaxHealth);

キャプチャしたAttributeに追加されるMagnitudeModifierの値。


キャラクターのMaxHealth : 100に20追加するGameplayEffectの場合、値は20になる

AttemptCalculateCapturedAttributeMagnitude

float MaxHealth = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(Test().MaxHealthDef, EvaluationParameters, MaxHealth);

キャプチャしたAttributeの元の値とModifierMagnitudeModifierOpで処理した結果の値。


キャラクターのMaxHealth : 100に20追加するGameplayEffectの場合、値は120になる

目次に戻る

キャプチャしたAttributeに処理結果を適用する

GEEC_Test.cppに追加

/*
@param1 : Test().MaxHealthProperty : 処理結果を適用したいAttribute
@param2 : EGameplayModOp::Additive : 処理結果の適用方法
@param3 : MaxHealth : 処理結果の値
*/
OutExecutionOutput.AddOutputModifier(
    FGameplayModifierEvaluatedData(
        Test().MaxHealthProperty,
        EGameplayModOp::Additive,
        MaxHealth
    )
);

適用する対象などはTestStaticsで宣言したものになるので、一番慎重に決めなければいけないのはTestStatics部分です。

目次に戻る

UE4 C++ TSoftClassPtr(TObjectPtr)のメモ

やあ

メモなので期待しないでください。
一部、感想や想像が含まれます

FStreamableManagerの宣言場所

・SingletonやGameInstanceなど、値を永続的に保持する場所で宣言する。
レベル遷移中のアセットの読み込みとかそのキャンセルをするときに、そういった場所にあったほうがバグらなさそう。

Singleton
historia.co.jp

GameInstance
meganeo.blog.shinobi.jp

GameInstanceで宣言してみる

HogeGameInstance.h

#include "Engine/StreamableManager.h"

UCLASS()
class HOGEPROJECT_API UHogeGameInstance : public UGameInstance
{
    GENERATED_BODY()
public:
    static UHogeGameInstance* GetGameInstance();
    
    static FStreamableManager StreamableManager;
    static FStreamableManager& GetStreamableManger();
};

HogeGameInstance.cpp

#include "Engine.h"

FStreamableManager UHogeGameInstance::StreamableManager;

UHogeGameInstance UHogeGameInstance::GetGameInstance()
{
    UHogeGameInstance* instance = nullptr;
    if (GEngine != nullptr)
    {
        FWorldContext* context = GEngine->GetWorldContextFromGameViewport(GEngine->GameViewport);
        instance = Cast<UHogeGameInstance>(context->OwningGameInstance);
    }

    return instance;
}

FStreamableManager& UHogeGameInstance::GetStreamableManger()
{
    return StreamableManager;
}

これでGameInstanceからStreamableManagerを受け取って読み込みの処理をする。
AssetManagerを使ってるならそこで宣言したほうがまとまってていい気がする。
というか普通、ソフト参照にしたいUIのクラスとかもAssetManagerに突っ込むんですかね?

読み込み

TSoftClassPtrTSoftObjectPtrも同じ方法で読み込みができる。

同期読み込み(Synchronous)

hoge.h

class UUserWidget;

UCLASS()
class HOGEPROJECT_API AHoge : public ACharacter
{
    GENERATED_BODY();
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
        TSoftClassPtr<UUserWidget> InventoryUIClass;
    UPROPERTY(BlueprintReadOnly)
        UUserWidget* InvnetoryUI;
    UFUNCTION(BlueprintCallable)
        void OpenInventoryUI();
};
void AHoge::OpenInventoryUI()
{
    UClass* _InventoryUIClass = InventoryUIClass.LoadSynchronous();
    InventoryUI = CreateWidget<UUserWidget>(GetWorld(), _InventoryUIClass);
    InventoryUI->AddToViewport();
}

もちろん同期読み込みのため、InventoryUIClassの読み込みが終わるまで処理が止まります。
なので、たくさんのソフト参照や、サイズの大きいものを同期読み込みしようとすると、
プレイヤーは、このゲーム重いわーとか思ったり思わなかったり。
それを防ぐための非同期読み込み。

非同期読み込み(Asynchronous)

hoge.h

class UUserWidget;

UCLASS()
class HOGEPROJECT_API AHoge : public ACharacter
{
    GENERATED_BODY();
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
        TSoftClassPtr<UUserWidget> InventoryUIClass;
    UPROPERTY(BlueprintReadOnly)
        UUserWidget* InvnetoryUI;
    UFUNCTION(BlueprintCallable)
        void BeginLoadInventoryUI();
    UFUNCTION(BlueprintCallable)
        void FinishedLoadInventoryUI();
    TSharedPtr<FStreamableHandle> InventoryUIHandle;
};
#include "HogeGameInstance.h"

void AHoge::BeginLoadInventoryUI()
{
    FStreamableManager& StreamableManager = UHogeGameInstance::GetStreamableManager();
    const FSoftObjectPath ObjectPath = InventoryUIClass.ToSoftObjectPath();
    InventoryUIHandle = StreamableManager.RequestAsyncLoad(
        ObjectPath,
        FStreamableDelegate::CreateUObject(
            this, &AHoge::FinishedLoadInventoryUI
        );
    );
}

void AHoge::BeginLoadInventoryUI()
{
    if (InventoryUIHandle.IsValid())
    {
        UObject* LoadedObj = InventoryUIHandle.Get()->GetLoadedAsset();
        if (LoadedObj)
        {
            UClass* _InventoryUIClass = Cast<UClass>(LoadedObj);
            if (_InventoryUIClass)
            {
                InventoryUI = CreateWidget<UUserWidget>(GetWorld(), _InventoryUIClass);
                InventoryUI->AddToViewport();                
            }
        }
    }
}

もちろんデリゲートを使わずに一つの関数のなかでもできます。

デリゲートを使わない例

FStreamableManager& StreamableManager = UHogeGameInstance::GetStreamableManager();
const FSoftObjectPath ObjectPath = InventoryUIClass.ToSoftObjectPath();
InventoryUIHandle = StreamableManager.RequestAsyncLoad(
    ObjectPath,
    FStreamableDelegate::CreateUObject(
        this, &AHoge::FinishedLoadInventoryUI
    );
);
if (InventoryUIHandle.IsValid())
{
    UObject* LoadedObj = InventoryUIHandle.Get()->GetLoadedAsset();
    if (LoadedObj)
    {
        UClass* _InventoryUIClass = Cast<UClass>(LoadedObj);
        if (_InventoryUIClass)
        {
            InventoryUI = CreateWidget<UUserWidget>(GetWorld(), _InventoryUIClass);
            InventoryUI->AddToViewport();                
        }
    }
}

なお、ソフト参照のサイズが大きいと読み込みが間に合わずにLoadedObjnullptrになるぞ!

非同期読み込みのタイミングよくわからない。。。誰か教えてください。

UE4 GameplayEffectを適用するだけのGameplayAbilityを実行してクラッシュするだけのおばかな話

エラーの内容

Assertion failed: Spec.Ability [File:D:/Build/++UE4/Sync/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp] [Line: 250]

めちゃくちゃどうでもいいことをだらだらと書いてるので原因まで飛んでください。

原因に飛ぶ

今日も今日とてゲーム制作を進めていると、とあるGameplayAbilityを実行するとクラッシュすることに気付いた。
そのGameplayAbilityは所有者にGameplayEffectを適用するだけの単純なもの。
エラーの内容は、
Assertion failed: Spec.Ability [File:D:/Build/++UE4/Sync/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp] [Line: 250]

エラーの場所を見てみると、UAbilitySystemComponent::GiveAbilityだった。

GiveAbilityはキャラクターがコントローラーを所有したときに行っているので、GameplayAbilityを実行したときに、ここでエラーが出るのはおかしい。。。
冷静に考えれば、どこかのGrantedAbilityにnullptrを設定している可能性が高いことがわかるのだが、
僕は
「んー?呼び出し方が悪いのかなー?」→違う
「あれー。じゃあ呼び出すタイミングが悪いんだ!」→違う
「えー。じゃあ呼び出し方が(」→違う
「だったら、GameplayAbility(アセット)が!」→違う
を永遠にループして繰り返した。

その結果、まじで一ミリも原因が思いつかなかった僕はAnswerHubに投稿した。
「クラッシュシマース(英語)」

ふぅ、これで誰かしらが案を出してくれるだろう。
ちょっと休憩する前にもう一回、GameplayAbility(アセット)を見てみるかー。
んーなにも問題ないよなぁ。。。
念のためGameplayEffect(アセット)も確認っと。
マウスホイールスイ―ッ。
あれ?GrantedAbilitiesに何か設定してある。

原因

GameplayEffectアセットの一番下にある、GrantedAbilitiesにnullptrを設定していたため起こりました。
f:id:pto8913:20210108212430p:plain

f:id:pto8913:20210108213909p:plain
この間僅か3分っ!

みんなもnullptrに気を付けよう!