Added Multi-PIE support:

- Added ImGui Context Manager to create and manage ImGui Context Proxies.
- Changed ImGui Context Proxy to dynamically create context and allow pairing with input state.
- Changed ImGui Module Manager to create one widget per context.
- Changed ImGui Widget to work in different input modes.
- Changed ImGui Input State to allow partial reset (only mouse or keyboard).
This commit is contained in:
Sebastian 2017-08-28 20:29:07 +01:00
parent 53387cf88b
commit 77bb73dbce
13 changed files with 469 additions and 69 deletions

View File

@ -0,0 +1,71 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#include "ImGuiContextManager.h"
FImGuiContextProxy& FImGuiContextManager::GetWorldContextProxy(UWorld& World)
{
const int32 Index = Utilities::GetWorldContextIndex(World);
checkf(Index != Utilities::INVALID_CONTEXT_INDEX, TEXT("Couldn't resolve context index for world %s: WorldType = %d"),
*World.GetName(), World.WorldType);
#if WITH_EDITOR
// Make sure that PIE worlds don't try to use editor context.
checkf(!GEngine->IsEditor() || Index != Utilities::DEFAULT_CONTEXT_INDEX, TEXT("Index for world %s "
"was resolved to the default context index %d, which in editor is reserved for editor context. PIE worlds "
"should use values that start from 1. WorldType = %d, NetMode = %d"), *World.GetName(),
Utilities::DEFAULT_CONTEXT_INDEX, World.WorldType, World.GetNetMode());
#endif // WITH_EDITOR
FContextData& Data = FindOrAddContextData(Index);
// Track worlds to make sure that different worlds don't try to use the same context in the same time.
if (!Data.World.IsValid())
{
Data.World = &World;
}
else
{
checkf(Data.World == &World, TEXT("Two different worlds, %s and %s, resolved to the same world context index %d."),
*Data.World->GetName(), *World.GetName(), Index);
}
return Data.ContextProxy;
}
void FImGuiContextManager::Tick(float DeltaSeconds)
{
FContextData* Data = Contexts.Find(1);
if (!Data || !Data->World.IsValid())
{
return;
}
for (auto& Entry : Contexts)
{
FImGuiContextProxy& ContextProxy = Entry.Value.ContextProxy;
ContextProxy.SetAsCurrent();
// Tick context proxy to end the old frame and starts a new one.
ContextProxy.Tick(DeltaSeconds);
}
}
FImGuiContextManager::FContextData& FImGuiContextManager::FindOrAddContextData(int32 Index)
{
FContextData* Data = Contexts.Find(Index);
if (!Data)
{
Data = &Contexts.Add(Index);
if (!Data->ContextProxy.OnDraw().IsBoundToObject(&ImGuiDemo))
{
Data->ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls);
}
}
return *Data;
}

View File

@ -0,0 +1,57 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#pragma once
#include "ImGuiContextProxy.h"
#include "ImGuiDemo.h"
#include "Utilities/WorldContextIndex.h"
// Manages ImGui context proxies.
class FImGuiContextManager
{
public:
FImGuiContextManager() = default;
FImGuiContextManager(const FImGuiContextManager&) = delete;
FImGuiContextManager& operator=(const FImGuiContextManager&) = delete;
FImGuiContextManager(FImGuiContextManager&&) = delete;
FImGuiContextManager& operator=(FImGuiContextManager&&) = delete;
// Get or create default ImGui context proxy. In editor this is the editor context proxy and in standalone game
// context proxy for the only world and the same value as returned from GetWorldContextProxy.
//
// If proxy doesn't exist then it will be created and initialized.
FImGuiContextProxy& GetDefaultContextProxy() { return FindOrAddContextData(Utilities::DEFAULT_CONTEXT_INDEX).ContextProxy; }
// Get or create ImGui context proxy for given world.
//
// If proxy doesn't yet exist then it will be created and initialized. If proxy already exists then associated
// world data will be updated.
FImGuiContextProxy& GetWorldContextProxy(UWorld& World);
// Get context proxy by index, or null if context with that index doesn't exist.
FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex)
{
FContextData* Data = Contexts.Find(ContextIndex);
return Data ? &(Data->ContextProxy) : nullptr;
}
void Tick(float DeltaSeconds);
private:
struct FContextData
{
TWeakObjectPtr<UWorld> World;
FImGuiContextProxy ContextProxy;
};
FContextData& FindOrAddContextData(int32 Index);
TMap<int32, FContextData> Contexts;
FImGuiDemo ImGuiDemo;
};

View File

@ -4,17 +4,26 @@
#include "ImGuiContextProxy.h"
#include "ImGuiImplementation.h"
#include "ImGuiInteroperability.h"
static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f;
static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f;
FImGuiContextProxy::FImGuiContextProxy()
{
// Create context.
Context = ImGui::CreateContext();
// Set this context in ImGui for initialization (any allocations will be tracked in this context).
SetAsCurrent();
// Start initialization.
ImGuiIO& IO = ImGui::GetIO();
// Use pre-defined canvas size.
IO.DisplaySize.x = DEFAULT_CANVAS_WIDTH;
IO.DisplaySize.y = DEFAULT_CANVAS_HEIGHT;
IO.DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT };
// Load texture atlas.
unsigned char* Pixels;
@ -28,12 +37,44 @@ FImGuiContextProxy::FImGuiContextProxy()
BeginFrame();
}
FImGuiContextProxy::~FImGuiContextProxy()
FImGuiContextProxy::FImGuiContextProxy(FImGuiContextProxy&& Other)
: Context(std::move(Other.Context))
, DrawEvent(std::move(Other.DrawEvent))
, InputState(std::move(Other.InputState))
, DrawLists(std::move(Other.DrawLists))
{
ImGui::Shutdown();
Other.Context = nullptr;
}
void FImGuiContextProxy::Tick(float DeltaSeconds, const FImGuiInputState* InputState)
FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other)
{
Context = std::move(Other.Context);
Other.Context = nullptr;
DrawEvent = std::move(Other.DrawEvent);
InputState = std::move(Other.InputState);
DrawLists = std::move(Other.DrawLists);
return *this;
}
FImGuiContextProxy::~FImGuiContextProxy()
{
if (Context)
{
// Set this context in ImGui for de-initialization (any de-allocations will be tracked in this context).
SetAsCurrent();
// Shutdown to save data etc.
ImGui::Shutdown();
// Destroy the context.
ImGui::DestroyContext(Context);
// Set default context in ImGui to keep global context pointer valid.
ImGui::SetCurrentContext(&GetDefaultContext());
}
}
void FImGuiContextProxy::Tick(float DeltaSeconds)
{
if (bIsFrameStarted)
{
@ -49,10 +90,10 @@ void FImGuiContextProxy::Tick(float DeltaSeconds, const FImGuiInputState* InputS
}
// Begin a new frame and set the context back to a state in which it allows to draw controls.
BeginFrame(DeltaSeconds, InputState);
BeginFrame(DeltaSeconds);
}
void FImGuiContextProxy::BeginFrame(float DeltaTime, const FImGuiInputState* InputState)
void FImGuiContextProxy::BeginFrame(float DeltaTime)
{
if (!bIsFrameStarted)
{

View File

@ -10,8 +10,7 @@
class FImGuiInputState;
// Represents a single ImGui context. All the context updates should be done through this proxy. During update it
// broadcasts draw events to allow listeners draw their controls. After update it stores produced draw data.
// TODO: Add dynamically created contexts, so we can have a better support for multi-PIE.
// broadcasts draw events to allow listeners draw their controls. After update it stores draw data.
class FImGuiContextProxy
{
public:
@ -22,30 +21,42 @@ public:
FImGuiContextProxy(const FImGuiContextProxy&) = delete;
FImGuiContextProxy& operator=(const FImGuiContextProxy&) = delete;
FImGuiContextProxy(FImGuiContextProxy&&) = delete;
FImGuiContextProxy& operator=(FImGuiContextProxy&&) = delete;
FImGuiContextProxy(FImGuiContextProxy&& Other);
FImGuiContextProxy& operator=(FImGuiContextProxy&& Other);
// Get draw data from the last frame.
const TArray<FImGuiDrawList>& GetDrawData() const { return DrawLists; }
// Get input state used by this context.
const FImGuiInputState* GetInputState() const { return InputState; }
// Set input state to be used by this context.
void SetInputState(const FImGuiInputState* SourceInputState) { InputState = SourceInputState; }
// Is this context the current ImGui context.
bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; }
// Set this context as current ImGui context.
void SetAsCurrent() { ImGui::SetCurrentContext(Context); }
// Delegate called right before ending the frame to allows listeners draw their controls.
FSimpleMulticastDelegate& OnDraw() { return DrawEvent; }
// Tick to advance context to the next frame.
// @param DeltaSeconds - Time delta in seconds (will be passed to ImGui)
// @param InputState - Input state for ImGui IO or null if there is no input for this context
void Tick(float DeltaSeconds, const FImGuiInputState* InputState = nullptr);
void Tick(float DeltaSeconds);
private:
void BeginFrame(float DeltaTime = 1.f / 60.f, const FImGuiInputState* InputState = nullptr);
void BeginFrame(float DeltaTime = 1.f / 60.f);
void EndFrame();
void UpdateDrawData(ImDrawData* DrawData);
TArray<FImGuiDrawList> DrawLists;
FSimpleMulticastDelegate DrawEvent;
ImGuiContext* Context = nullptr;
bool bIsFrameStarted = false;
FSimpleMulticastDelegate DrawEvent;
const FImGuiInputState* InputState = nullptr;
TArray<FImGuiDrawList> DrawLists;
};

View File

@ -20,3 +20,11 @@
#if PLATFORM_WINDOWS
#include <HideWindowsPlatformTypes.h>
#endif // PLATFORM_WINDOWS
// This is exposing ImGui default context for the whole module.
// This is assuming that we don't define custom GImGui and therefore have GImDefaultContext defined in imgui.cpp.
ImGuiContext& GetDefaultContext()
{
return GImDefaultContext;
}

View File

@ -0,0 +1,9 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#pragma once
#include <imgui.h>
// Get default context created by ImGui framework.
ImGuiContext& GetDefaultContext();

View File

@ -45,15 +45,24 @@ void FImGuiInputState::SetMouseDown(uint32 MouseIndex, bool bIsDown)
}
}
void FImGuiInputState::ResetState()
void FImGuiInputState::Reset(bool bKeyboard, bool bMouse)
{
if (bKeyboard)
{
ClearCharacters();
ClearKeys();
}
if (bMouse)
{
ClearMouseButtons();
ClearMouseAnalogue();
}
if (bKeyboard && bMouse)
{
ClearModifierKeys();
}
}
void FImGuiInputState::ClearUpdateState()

View File

@ -98,7 +98,13 @@ public:
void SetAltDown(bool bIsDown) { bIsAltDown = bIsDown; }
// Reset state and mark as dirty.
void ResetState();
void ResetState() { Reset(true, true); }
// Reset keyboard state and mark as dirty.
void ResetKeyboardState() { Reset(true, false); }
// Reset mouse state and mark as dirty.
void ResetMouseState() { Reset(false, true); }
// Clear part of the state that is meant to be updated in every frame like: accumulators, buffers and information
// about dirty parts of keys or mouse buttons arrays.
@ -106,6 +112,8 @@ public:
private:
void Reset(bool bKeyboard, bool bMouse);
void ClearCharacters();
void ClearKeys();
void ClearMouseButtons();

View File

@ -11,8 +11,8 @@
FImGuiModuleManager::FImGuiModuleManager()
{
// Bind ImGui demo to proxy, so it can draw controls in its context.
ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls);
// Make sure that default ImGui context is setup.
ContextManager.GetDefaultContextProxy();
// Typically we will use viewport created events to add widget to new game viewports.
ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated);
@ -117,8 +117,8 @@ void FImGuiModuleManager::Tick(float DeltaSeconds)
{
if (IsInUpdateThread())
{
// Update context proxy to advance to next frame.
ContextProxy.Tick(DeltaSeconds, ViewportWidget.IsValid() ? &ViewportWidget->GetInputState() : nullptr);
// Update context manager to advance all ImGui contexts to the next frame.
ContextManager.Tick(DeltaSeconds);
// Inform that we finished updating ImGui, so other subsystems can react.
PostImGuiUpdateEvent.Broadcast();
@ -141,15 +141,24 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport)
checkf(GameViewport, TEXT("Null game viewport."));
checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports."));
const int32 ContextIndex = Utilities::GetWorldContextIndex(GameViewport);
// This makes sure that context for this world is created.
auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld());
// Get widget for this world.
auto& ViewportWidget = ViewportWidgets.FindOrAdd(ContextIndex);
if (!ViewportWidget.IsValid())
{
SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this);
checkf(ViewportWidget.IsValid(), TEXT("Failed to create SImGuiWidget."));
SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this).ContextIndex(ContextIndex);
check(ViewportWidget.IsValid());
}
// Bind widget's input to context for this world.
Proxy.SetInputState(&ViewportWidget->GetInputState());
// High enough z-order guarantees that ImGui output is rendered on top of the game UI.
constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000;
GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(ViewportWidget), IMGUI_WIDGET_Z_ORDER);
}

View File

@ -2,8 +2,7 @@
#pragma once
#include "ImGuiContextProxy.h"
#include "ImGuiDemo.h"
#include "ImGuiContextManager.h"
#include "SImGuiWidget.h"
#include "TextureManager.h"
@ -16,8 +15,8 @@ class FImGuiModuleManager
public:
// Get ImGui context proxy.
FImGuiContextProxy& GetContextProxy() { return ContextProxy; }
// Get ImGui contexts manager.
FImGuiContextManager& GetContextManager() { return ContextManager; }
// Get texture resources manager.
FTextureManager& GetTextureManager() { return TextureManager; }
@ -56,18 +55,14 @@ private:
// Event that we call after ImGui is updated.
FSimpleMulticastDelegate PostImGuiUpdateEvent;
// Proxy controlling ImGui context.
FImGuiContextProxy ContextProxy;
// ImWidget that draws ImGui demo.
FImGuiDemo ImGuiDemo;
// Manager for ImGui contexts.
FImGuiContextManager ContextManager;
// Manager for textures resources.
FTextureManager TextureManager;
// Slate widget that we attach to created game viewports (widget without per-viewport state can be attached to
// multiple viewports).
TSharedPtr<SImGuiWidget> ViewportWidget;
// Slate widgets that we attach to game viewports.
TMap<int32, TSharedPtr<SImGuiWidget>> ViewportWidgets;
FDelegateHandle TickDelegateHandle;
FDelegateHandle ViewportCreatedHandle;

View File

@ -4,6 +4,7 @@
#include "SImGuiWidget.h"
#include "ImGuiContextManager.h"
#include "ImGuiContextProxy.h"
#include "ImGuiInteroperability.h"
#include "ImGuiModuleManager.h"
@ -16,6 +17,7 @@ void SImGuiWidget::Construct(const FArguments& InArgs)
{
checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument"));
ModuleManager = InArgs._ModuleManager;
ContextIndex = InArgs._ContextIndex;
ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate);
}
@ -95,6 +97,48 @@ FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEven
return FReply::Handled();
}
FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent)
{
Super::OnFocusReceived(MyGeometry, FocusEvent);
// If widget has a keyboard focus we always maintain mouse input. Technically, if mouse is outside of the widget
// area it won't generate events but we freeze its state until it either comes back or input is completely lost.
UpdateInputMode(true, true);
return FReply::Handled();
}
void SImGuiWidget::OnFocusLost(const FFocusEvent& FocusEvent)
{
Super::OnFocusLost(FocusEvent);
UpdateInputMode(false, IsHovered());
}
void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
Super::OnMouseEnter(MyGeometry, MouseEvent);
// If mouse enters while input is active then we need to update mouse buttons because there is a chance that we
// missed some events.
if (InputMode != EInputMode::None)
{
for (const FKey& Button : { EKeys::LeftMouseButton, EKeys::MiddleMouseButton, EKeys::RightMouseButton, EKeys::ThumbMouseButton, EKeys::ThumbMouseButton2 })
{
InputState.SetMouseDown(ImGuiInterops::GetMouseIndex(Button), MouseEvent.IsMouseButtonDown(Button));
}
}
UpdateInputMode(HasKeyboardFocus(), true);
}
void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent)
{
Super::OnMouseLeave(MouseEvent);
UpdateInputMode(HasKeyboardFocus(), false);
}
void SImGuiWidget::CopyModifierKeys(const FInputEvent& InputEvent)
{
InputState.SetControlDown(InputEvent.IsControlDown());
@ -102,21 +146,58 @@ void SImGuiWidget::CopyModifierKeys(const FInputEvent& InputEvent)
InputState.SetAltDown(InputEvent.IsAltDown());
}
void SImGuiWidget::CopyModifierKeys(const FPointerEvent& MouseEvent)
{
if (InputMode == EInputMode::MouseOnly)
{
CopyModifierKeys(static_cast<const FInputEvent&>(MouseEvent));
}
}
void SImGuiWidget::UpdateInputMode(bool bNeedKeyboard, bool bNeedMouse)
{
const EInputMode NewInputMode =
bNeedKeyboard ? EInputMode::MouseAndKeyboard :
bNeedMouse ? EInputMode::MouseOnly :
EInputMode::None;
if (InputMode != NewInputMode)
{
// We need to reset input components if we are either fully shutting down or we are downgrading from full to
// mouse-only input mode.
if (NewInputMode == EInputMode::None)
{
InputState.ResetState();
}
else if (InputMode == EInputMode::MouseAndKeyboard)
{
InputState.ResetKeyboardState();
}
InputMode = NewInputMode;
}
}
void SImGuiWidget::OnPostImGuiUpdate()
{
if (InputMode != EInputMode::None)
{
InputState.ClearUpdateState();
}
}
int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const
{
if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex))
{
// Calculate offset that will transform vertex positions to screen space - rounded to avoid half pixel offsets.
const FVector2D VertexPositionOffset{ FMath::RoundToFloat(MyClippingRect.Left), FMath::RoundToFloat(MyClippingRect.Top) };
// Convert clipping rectangle to format required by Slate vertex.
const FSlateRotatedRect VertexClippingRect{ MyClippingRect };
for (const auto& DrawList : ModuleManager->GetContextProxy().GetDrawData())
for (const auto& DrawList : ContextProxy->GetDrawData())
{
DrawList.CopyVertexData(VertexBuffer, VertexPositionOffset, VertexClippingRect);
@ -145,6 +226,7 @@ int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeo
FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0);
}
}
}
return LayerId;
}

View File

@ -19,12 +19,17 @@ public:
SLATE_BEGIN_ARGS(SImGuiWidget)
{}
SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager)
SLATE_ARGUMENT(int32, ContextIndex)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
~SImGuiWidget();
// Get index of the context that this widget is targeting.
int32 GetContextIndex() const { return ContextIndex; }
// Get input state associated with this widget.
const FImGuiInputState& GetInputState() const { return InputState; }
//----------------------------------------------------------------------------------------------------
@ -49,9 +54,28 @@ public:
virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override;
virtual void OnFocusLost(const FFocusEvent& FocusEvent) override;
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
private:
enum class EInputMode
{
None,
MouseOnly,
MouseAndKeyboard
};
FORCEINLINE void CopyModifierKeys(const FInputEvent& InputEvent);
FORCEINLINE void CopyModifierKeys(const FPointerEvent& MouseEvent);
// Determine new input mode based on requirement hints.
void UpdateInputMode(bool bNeedKeyboard, bool bNeedMouse);
void OnPostImGuiUpdate();
@ -64,5 +88,9 @@ private:
mutable TArray<FSlateVertex> VertexBuffer;
mutable TArray<SlateIndex> IndexBuffer;
int32 ContextIndex = 0;
EInputMode InputMode = EInputMode::None;
FImGuiInputState InputState;
};

View File

@ -0,0 +1,72 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#pragma once
#include <Core.h>
#include <Engine.h>
// Utilities mapping worlds to indices that we use to identify ImGui contexts.
// Editor and standalone games have context index 0 while PIE worlds have indices starting from 1 for server and 2+ for
// clients.
namespace Utilities
{
// Default context index for non PIE worlds.
static constexpr int32 DEFAULT_CONTEXT_INDEX = 0;
// Invalid context index for parameters that cannot be resolved to a valid world.
static constexpr int32 INVALID_CONTEXT_INDEX = -1;
#if WITH_EDITOR
template<typename T>
FORCEINLINE int32 GetWorldContextIndex(const TWeakObjectPtr<T>& Obj)
{
return Obj.IsValid() ? GetWorldContextIndex(*Obj.Get()) : INVALID_CONTEXT_INDEX;
}
template<typename T>
FORCEINLINE int32 GetWorldContextIndex(const T* Obj)
{
return Obj ? GetWorldContextIndex(*Obj) : INVALID_CONTEXT_INDEX;
}
FORCEINLINE int32 GetWorldContextIndex(const FWorldContext& WorldContext)
{
// In standalone game (WorldType = Game) we have only one context with index 0 (see DEFAULT_CONTEXT_INDEX).
// In editor, we keep 0 for editor and use PIEInstance to index worlds. In simulation or standalone single-PIE
// sessions PIEInstance is 0, but since there is only one world we can change it without causing any conflicts.
// In single-PIE with dedicated server or multi-PIE sessions worlds have PIEInstance starting from 1 for server
// and 2+ for clients, what maps directly to our index.
return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : DEFAULT_CONTEXT_INDEX;
}
FORCEINLINE int32 GetWorldContextIndex(const UGameInstance& GameInstance)
{
return GetWorldContextIndex(GameInstance.GetWorldContext());
}
FORCEINLINE int32 GetWorldContextIndex(const UGameViewportClient& GameViewportClient)
{
return GetWorldContextIndex(GameViewportClient.GetGameInstance());
}
FORCEINLINE int32 GetWorldContextIndex(const UWorld& World)
{
return GetWorldContextIndex(World.GetGameInstance());
}
#else
template<typename T>
constexpr int32 GetWorldContextIndex(const T&)
{
// The only option is standalone game with one context.
return DEFAULT_CONTEXT_INDEX;
}
#endif // #if WITH_EDITOR
}