[UE4]Gameplay Dev Notes
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;
}
}
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
几时归去,作个闲人。对一张琴、一壶酒、一溪云。----宋·苏轼《行香子》