keywords: UE4, Gameplay Development Notes

slogan_image

Switch Level

论坛上有不少资料说切换场景时使用GEngine->Browse():

if (FWorldContext* Context = GEngine->GetWorldContextFromWorld(GetWorld()))
{
    FString Error;
    FURL URL(TEXT("/Game/Demo/scecn_test/scecn_test02"));
    EBrowseReturnVal::Type RS = GEngine->Browse(*Context, URL, Error);
    if (EBrowseReturnVal::Type::Failure == RS)
    {
        WSI->ChangePlayerState(EPlayerState::EP_LoginFailed);
    }
}

GEngine->Browse这种方式第一次打开场景时正常,如果第二次切换场景(比如从别的场景再切回原场景),则会导致程序崩溃。
建议用:UGameplayStatics::OpenLevel(GetWorld(), TEXT("/Game/Map/TestMap"));

How to get UWorld
if(UWorld* World = GEngine->GetCurrentPlayWorld(nullptr))
{
    ULevel* Level = World->GetLevel(0);
}
Check if it’s server or client without WorldContext

Quoted from CoreMisc.h

bool IsRunningDedicatedServer();

bool IsRunningGame();

bool IsRunningClientOnly();
Subsystem (Unreal Stylized Singleton)

How to create customized Subsystem?
header

UCLASS()
class MYGAME_API UMySubsystem : public UWorldSubsystem
{
    GENERATED_BODY()

public:

    UMySubsystem();

    static UMySubsystem* Get(const UObject* WorldContext)
    {
        UMySubsystem* Ret = nullptr;
        if (WorldContext)
        {
            Ret = WorldContext->GetWorld()->GetSubsystem<UMySubsystem>();
        }
        return Ret;
    }

    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;
}

cpp

#include "MySubsystem.h"

UMySubsystem::UMySubsystem()
    : UWorldSubsystem()
{
}

void UMySubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);

}

void UMySubsystem::Deinitialize()
{
    Super::Deinitialize();

}

How to disable initialization of GameInstanceSubsystem on dedicated server?
Quoted from:
Engine\Plugins\Experimental\CommonUI\Source\CommonUI\Private\CommonUISubsystemBase.cpp

bool UCommonUISubsystemBase::ShouldCreateSubsystem(UObject* Outer) const
{
    return !CastChecked<UGameInstance>(Outer)->IsDedicatedServerInstance();
}
Save Game / Load Game
void MyGameInstance::LoadSaveGame()
{
    UMySaveGame* SaveGame = Cast<UMySaveGame>(
            UGameplayStatics::LoadGameFromSlot(SaveSlotName, UserIndex));
    if (!SaveGame)
    {
        if (UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(Instance()->SaveGameClass));)
        {
            UGameplayStatics::SaveGameToSlot(SaveGame, SaveGame->SaveSlotName, SaveGame->UserIndex);
            MySaveGame = SaveGame;
        }
    }
    else
    {
        MySaveGame = SaveGame;
    }
}

Easy Multi Save is an all-in-one save and load solution that will literally save you a lot of time.
https://www.fab.com/listings/49f745a1-cbdd-4b18-8278-22ae1075d91d

How to get PlayerStart Point in Level
AActor* AGameModeBase::ChoosePlayerStart(AController* Player);
    
AActor* AGameModeBase::FindPlayerStart(AController* Player, const FString& IncomingName = TEXT(""));

Event

Level Event

Level changed (switch level) event:

//只有Standalone和Client中触发,Server不触发。
FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UMySubsystem::OnPreLoadMap);
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMySubsystem::OnPostLoadMap);

Level destroyed (e.g. exit game) event:

GetWorld()->GetLevel(0)->OnCleanupLevel.AddUObject(this, &UMySubsystem::OnCleanupLevel);

Actor spawned event (spawned at runtime, not the actor placed in level):

FOnActorSpawned::FDelegate Delegate;
Delegate.BindUObject(this, &AMyActor::OnActorAdded);
GetWorld()->AddOnActorSpawnedHandler(Delegate);

or

GEngine->OnLevelActorAdded().AddUObject(this, &AMyActor::OnActorSpawned);
GEngine->OnLevelActorDeleted().AddUObject(this, &AMyActor::OnActorDeleted);
GEngine->OnLevelActorListChanged().AddUObject(this, &AMyActor::OnLevelActorListChanged);
Engine Event
FGameDelegates::Get().GetExitCommandDelegate().AddUObject(this, &UMyGameInstance::OnEnginePreExit);
FGameDelegates::Get().GetExitCommandDelegate().AddUObject(this, &UMyGameInstance::OnEnginePreExit);

APIs

Interface Example

Define: PyWrapperBase.h

#pragma once
 
#include "ReactToTriggerInterface.generated.h"
 
/*
Empty class for reflection system visibility.
Uses the UINTERFACE macro.
Inherits from UInterface.
*/
UINTERFACE(MinimalAPI, Blueprintable)
class UReactToTriggerInterface : public UInterface
{
    GENERATED_BODY()
};
 
/* Actual Interface declaration. */
class IReactToTriggerInterface
{
    GENERATED_BODY()
 
public:
    /* A version of the React To Trigger function that can be implemented in C++ or Blueprint. */
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category=Trigger Reaction)
    bool ReactToTrigger();

    /* A version of the React To Trigger function that can only be implemented in C++. */
    //virtual bool ReactToTrigger();

Usage: check if an actor implements an interface.
Header

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ReactToTriggerInterface.h"
#include "Trap.generated.h"
 
UCLASS(Blueprintable, Category="MyGame")
class ATrap : public AActor, public IReactToTriggerInterface
{
    GENERATED_BODY()
 
public:
    virtual bool ReactToTrigger() override;
    
    // Blueprint Native Event override
    bool ReactToTrigger_Implementation() override;
};

CPP

#include "Trap.h"
 
bool ATrap::ReactToTrigger()
{
    return false;
}
 
// Blueprint Native Event override implementation
bool ATrap::ReactToTrigger_Implementation() 
{
    return false;
}

Usage: check if an actor implements an interface.

if(Trap->GetClass()->ImplementsInterface(UReactToTriggerInterface::StaticClass()))
{
    bool bReacted = IReactToTriggerInterface::Execute_ReactToTrigger(Trap);
}

Reference:
Unreal Interfaces

Async Task

Quoted from UE5\Engine\Plugins\AI\MLAdapter\Source\MLAdapter\Public\MLAdapterAsync.h

// sends InFunction to be called on the GameThread and waits for the result
template<typename RetType>
RetType CallOnGameThread(TFunction<RetType()> InFunction)
{
    TOptional<RetType> Result;
    TAtomic<bool> bCompleted(false);
    AsyncTask(ENamedThreads::GameThread, [&Result, &bCompleted, &InFunction]
    {
        Result = InFunction();
        bCompleted = true;
    });
    while (!bCompleted);
    return MoveTemp(Result.GetValue());
}

// fire (InFunction to be called on the GameThread) and forget
template<>
FORCEINLINE void CallOnGameThread(TFunction<void()> InFunction)
{
    AsyncTask(ENamedThreads::GameThread, InFunction);
}
UE::Tasks::Launch (UE5)

Example:

ThreadName = FString(TEXT("DatasmithWorkerHandler_")) + FString::FromInt(Id);
IOTask = UE::Tasks::Launch(*ThreadName, [this]() { Run(); } );
Shared Pointer in Async Task

Quoted from engine source FileSourceControlContextMenu.cpp.

Step 1: inherit class TSharedFromThis.

class FFileSourceControlContextMenuState : public TSharedFromThis<FFileSourceControlContextMenuState>
{
}

Step 2: create shared pointer using MakeShared().

void FFileSourceControlContextMenu::MakeContextMenu()
{
    InnerState = MakeShared<FFileSourceControlContextMenuState>();
    InnerState->Initialize();
}

Step 3: create a “temporary” TSharedPtr for the lambda function using [this, This = AsShared()], this new shared ref (TSharedRef) will release the ref automatically when lambda function execution finished.

void FFileSourceControlContextMenuState::NextAsyncState()
{
    if (TrySetAsyncState(EAsyncState::None, EAsyncState::TryCacheCanExecute))
    {
        Async(EAsyncExecution::ThreadPool, [this, This = AsShared()]()
        {
            TryCacheCanExecuteVars(SelectedFiles, &PathsWithUnknownState);

            // Source control operations have to be started from the main thread
            Async(EAsyncExecution::TaskGraphMainThread, [this, This = AsShared()]() { NextAsyncState(); });
        });
    }
}

This action ensures the current object (this pointer) still be valid if shared ref counter reach to 0 outside the lambda function. Otherwise this pointer will goes to wild pointer and crashes on executing lambda function.

DataTable

Define:

#include "Engine/DataTable.h"

USTRUCT(BlueprintType)
struct ACLIENT_API FMyTableRow : public FTableRowBase
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")
    int32 Damage;

    FMyTableRow() {}
};

Invoke:

FString ID = FString::Printf(TEXT("%d"), 1001);

if (UDataTable* DataTable = GetDropDataTable())
{
    if (FMyTableRow* DropUnitRow = DataTable->FindRow<FMyTableRow>(FName(*ID), "", true))
    {
        int32 Damage = FMyTableRow->Damage;
    }
}
GAS (Gameplay Ability System)

Documents:
Your First 60 Minutes with Gameplay Ability System - dev.epicgames.com
Lyra Sample Game - dev.epicgames.com


几时归去,作个闲人。对一张琴、一壶酒、一溪云。----宋·苏轼《行香子》