keywords: UE4, Gameplay Development Notes

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;
    }
}
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 "PyWrapperBase.generated.h"

UINTERFACE(MinimalApi, BlueprintType)
class UPythonResourceOwner : public UInterface
{
    GENERATED_UINTERFACE_BODY()
};

class IPythonResourceOwner
{
    GENERATED_IINTERFACE_BODY()

public:
    /**
     * Release all Python resources owned by this object.
     * Called during Python shutdown to free resources from lingering UObject-based instances.
     */
    virtual void ReleasePythonResources() = 0;
};

Usage: check if an actor implements an interface.

if(MyActor->GetClass()->ImplementsInterface(UMyInterface::StaticClass()))
{ }

if(IMyInterface* Interface = Cast<IMyInterface>(MyActor))
{
    Interface->OnDamage(DamageValue, DamagePosition);
}

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);
}
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.


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