A C++23 utility proto-plugin for Unreal Engine, for a more civilised age.
- Attention
- This library is far from being production ready and is not recommended to be used yet at all
Find the source code at
What's a proto-plugin?
This repository is not meant to be used as its own full featured Unreal Plugin, but rather other plugins can compose this one into themselves using the Folder Composition feature provided by Nuke.Unreal (via Nuke.Cola). The recommended way to use this is via using a simple Nuke.Cola build-plugin which inherits IUseMcro
for example:
using Nuke.Common;
using Nuke.Cola;
using Nuke.Cola.BuildPlugins;
using Nuke.Unreal;
[ImplicitBuildInterface]
public interface IUseMcroInMyProject : IUseMcro
{
Target UseMcro => _ => _
.McroGraph()
.DependentFor<UnrealBuild>(b => b.Prepare)
.Executes(() =>
{
UseMcroAt(this.ScriptFolder(), "MyProjectSuffix");
});
}
If this seems painful please blame Epic Games who decided to not allow marketplace/Fab plugins to depend on each other, or at least depend on free and open source plugins from other sources. So I had to come up with all this "Folder Composition" nonsense so end-users might be able to use multiple of my plugins sharing common dependencies without module name conflicts.
To use MCRO as its own plugin without the need for Nuke.Unreal, see this repository: (DOESN'T EXIST YET)
What's up with _Origin suffix everywhere?
When this proto plugin is imported into other plugins, this suffix is used for disambiguation, in case the end-user uses multiple pre-compiled plugins depending on MCRO. If this seems annoying please refer to the paragraph earlier.
What MCRO can do?
Here are some code appetizers without going too deep into their details. The demonstrated features usually can do a lot more than what's shown here.
(symbols in source code are clickable!)
Error handling
An elegant workflow for dealing with code which can and probably will fail. It allows the developer to record the circumstances of an error, record its propagation accross components which are affected, add suggestions to the user or fellow developers for fixing the error, and display that with a modal Slate window, or print it into logs following a YAML structure.
++
FCanFail FK4ADevice::Tick_Sync()
{
->AsCrashing()
->BreakDebugger()
->WithCppStackTrace()
->Notify(LastError);
try
{
k4a::capture capture;
if (auto color = capture.get_color_image(); ColorStream->Active)
{
}
if (auto depth = capture.get_depth_image(); DepthStream->Active)
{
}
if (auto ir = capture.get_ir_image(); IRStream->Active)
{
}
}
catch (k4a::error const& exception)
{
->AsFatal()->WithCppStackTrace()->WithLocation()
->Notify(LastError);
}
}
#define PROPAGATE_FAIL(expression)
#define ASSERT_RETURN(condition)
FORCEINLINE FCanFail Success()
Delegate type inferance
In Mcro::Delegates
namespace there's a bunch of overloads of From
function which can infer a delegate type and its intended binding only based on its input arguments. It means a couple of things:
- The potentially lengthy delegate types are no longer needed to be repeated.
- Creating an alias for each (multicast) delegate is no longer a must have.
For example given an imaginary type with a terrible API for some reason
++
struct FObservable
{
using FOnStateResetTheFirstTimeEvent = TMulticastDelegate<void(FObservable const&)>;
FOnStateResetTheFirstTimeEvent OnStateResetTheFirstTimeEvent;
using FDefaultInitializerDelegate = TDelegate<FString(FObservable const&)>;
void SetDefaultInitializer(FDefaultInitializerDelegate const& defaultInit)
{
}
}
There's also a dynamic / native (multicast) delegate interop including similar chaining demonstrated here.
Advanced TEventDelegate
Did your thing ever load after an event which your thing depends on, but now you have to somehow detect that the event has already happened and you have to execute your thing manually? With Mcro::Delegates::TEventDelegate
this situation has a lot less friction:
++
SomeEvent.Broadcast(42);
SomeEvent.Add(From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("The last argument this event broadcasted with: %d"), value);
}));
SomeOtherEvent.Broadcast(1337);
SomeOtherEvent.Add(
From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("The last argument this event broadcasted with: %d"), value);
}),
);
Or your thing only needs to do its tasks on the first time a frequently invoked event is triggered?
++
SomeFrequentEvent.Add(
From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("This value is printed only once: %d"), value);
}),
InvokeOnce
);
SomeFrequentEvent.Broadcast(1);
SomeFrequentEvent.Broadcast(2);
Chaining events?
++
TMulticastDelegate<void(int)> LowerLevelEvent;
HigherLevelEvent.Add(From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("Value: %d"), value);
}));
LowerLevelEvent.Add(HigherLevelEvent.Delegation(this ));
LoverLevelEvent.Broadcast(42);
Of course the above chaining can be combined with belated~ or one-time invocations.
TTypeName
C++ 20 can do string manipulation in compile time, including regex. With that, compiler specific "pretty function" macros become a key tool for simple static reflection. Based on that MCRO has TTypeName
++
struct FMyType
{
FStringView GetTypeString() { return TTypeName<FMyType>; }
}
even better with C++ 23 deducing this
++
struct FMyBaseType
{
template <typename Self>
FString const& GetTypeString(this Self&&) const { return TTypeString<Self>; }
}
struct FMyDerivedType : FMyBaseType {}
FMyDerivedType myVar;
UE_LOG(LogTemp, Display, TEXT("This is `%s`"), *myVar.GetTypeString());
FMyBaseType const& myBaseVar = myVar;
UE_LOG(LogTemp, Display, TEXT("This is `%s`"), *myBaseVar.GetTypeString());
MCRO provides a base type IHaveType
for storing the final type as an FName
to avoid situations like above
++
struct FMyDerivedType : FMyBaseType
{
FMyDerivedType() { SetType(); }
}
FMyDerivedType myVar;
UE_LOG(LogTemp, Display, TEXT("This is as expected a `%s`"), *myVar.GetType().ToString());
FMyBaseType const& myBaseVar = myVar;
UE_LOG(
LogTemp, Display,
TEXT("But asking the base type will still preserve `%s`"),
*myBaseVar.GetType().ToString()
);
Auto modular features
One use of TTypeName
is making modular features easier to use:
++
#include "Mcro/AutoModularFeatures.h"
using namespace Mcro::AutoModularFeatures;
class IProblemSolvingFeature : public TAutoModularFeature<IProblemSolvingFeature>
{
public:
virtual void SolveAllProblems() = 0;
};
class FProblemSolver : public IProblemSolvingFeature, public IFeatureImplementation
{
public:
FProblemSolver() { Register(); }
virtual void SolveAllProblems() override;
};
FProblemSolver GProblemSolver;
IProblemSolvingFeature::Get().SolveAllProblems();
Notice how the feature name has never needed to be explicitly specified as a string, because the type name is used under the hood.
Observable TState
You have data members of your class, but you also want to notify others about how that's changing?
++
struct FMyStuff
{
FMyStuff()
{
State.
OnChange([](
int next, TOptional<int> previous)
{
UE_LOG(LogTemp, Display, TEXT("Changed from %d to %d"), previous.Get(-2), next);
});
State.OnChange(
[](int next) { UE_LOG(LogTemp, Display, TEXT("The first changed value is %d"), next); },
);
}
void Update(int input)
{
if (State.HasChangedFrom(input))
{
}
}
};
struct FFoobar : TSharedFromThis<FFoobar>
{
FMyStuff MyStuff;
{
UE_LOG(LogTemp, Display, TEXT(
"React to %d with %s"), change.
Next, *capture);
}
void Do()
{
MyStuff.Update(1);
MyStuff.Update(2);
MyStuff.Update(2);
MyStuff.OnChange(
[](int next) { UE_LOG(LogTemp, Display, TEXT("Arriving late %d"), next); },
);
MyStuff.Update(3);
MyStuff.OnChange(
From(
this, &FFoobar::Thing, TEXT(
"a unicorn")));
MyStuff.Update(4)
}
}
TInferredDelegate< Function, Captures... > From(Function func, const Captures &... captures)
virtual FDelegateHandle OnChange(TDelegate< void(TChangeData< T > const &)> onChange, EInvokeMode invokeMode=DefaultInvocation) override
Function Traits
Make templates dealing with function types more readable and yet more versatile via the Mcro::FunctionTraits
namespace. This is the foundation of many intricate teemplates Mcro can offer.
Constraint/infer template parameters from the signature of an input function. (This is an annotated exempt from Mcro::UObjects::Init
)
++
{
template <
CFunctorObject Initializer,
CUObject Result = std::decay_t<Argument>
>
requires std::is_lvalue_reference_v<Argument>
Result* Construct(FConstructObjectParameters&& params, Initializer&& init)
{ ... }
}
typename TFunctionTraits< std::decay_t< T > >::template Arg< I > TFunction_Arg
Concepts
There's a copy of the C++ 20 STL Concepts library in Mcro::Concepts
namespace but with more Unreal friendly nameing. Also it adds some conveniance concepts, like *Decayed
versions of type constraints, or some Unreal specific constraints like CSharedRefOrPtr
or CUObject
Extending the Slate declarative syntax
Mcro::Slate
adds the /
operator to be used in Slate UI declarations, which can work with functions describing a common block of attributes for given widget.
++
#include "Mcro/Slate";
{
return [&](STextBlock::FArguments& args) -> auto&
{
return args
. Text(FText::FromString(text))
. Font(FCoreStyle::GetDefaultFontStyle("Mono", 12));
};
}
{
return [&](STextBlock::FArguments& args) -> auto&
{
return args
/ Text(text);
};
}
auto ExpandableText(
FText const& title,
FString const& text,
{
return [&](SExpandableArea::FArguments& args) -> auto&
{
return args
. AreaTitle(title)
. InitiallyCollapsed(true)
. BodyContent()
[
SNew(STextBlock) / textAttributes
];
};
}
TUniqueFunction< TArgumentsOf< T > &(TArgumentsOf< T > &)> TAttributeBlock
MCRO_API EVisibility IsVisible(bool visible, EVisibility hiddenState=EVisibility::Collapsed)
Or add slots right in the Slate declarative syntax derived from an input array:
++
auto Row() -> SVerticalBox::FSlot::FSlotArguments
{
return MoveTemp(SVerticalBox::Slot()
. HAlign(HAlign_Fill)
. AutoHeight()
);
}
void Construct(FArguments const& inArgs)
{
ChildSlot
[
SNew(SVerticalBox)
+ Row()[ SNew(STextBlock) / Text(TEXT("Items:")) ]
+
TSlots(inArgs._Items.Get(), [&](FString
const& item)
{
return MoveTemp(
Row()[ SNew(STextBlock) / Text(item) ]
);
})
+ Row()[ SNew(STextBlock) / Text(TEXT("Fin.")) ]
];
}
Text interop
The Mcro::Text
namespace provides some handy text templating and conversion utilities and interop between Unreal string and std::strings for third-party libraries.
++
#include "Mcro/Text";
template <CStringOrViewOrName String>
bool GetSomethingCommon(String&& input) { ... }
template <CStdStringOrViewInvariant String>
size_t GetLength(String&& input) { return input.size(); }
++
FString foo(TEXT("bar"));
std::string fooNarrow = StdConvertUtf8(foo);
ISPC parallel tasks support
McroISPC
module gives support for task
and launch
keywords of the ISPC language
task void MakeLookupUVLine(
uniform uint32 buffer[],
uniform uint32 width
) {
uniform uint32 lineStart = taskIndex0 * width;
foreach (i = 0 ... width)
{
varying uint32 address = lineStart + i;
buffer[address] = (i << 16) | taskIndex0;
}
}
export void MakeLookupUV(
uniform uint32 buffer[],
uniform uint32 width,
uniform uint32 height
) {
launch [height] MakeLookupUVLine(buffer, width);
}
Last but not least
Legal
Third-party components used by MCRO
When using MCRO in a plugin distributed via Fab, these components must be listed upon submission.
In addition the following tools and .NET libraries are used for build tooling:
This library is distributed under the Mozilla Public License Version 2.0 open-source software license. Each source code file where this license is applicable contains a comment about this.
Modifying those files or the entire library for commercial purposes is allowed but those modifications should also be published free and open-source under the same license. However files added by third-party for commercial purposes interfacing with the original files under MPL v2.0 are not affected by MPL v2.0 and therefore can have any form of copyright the third-party may choose.
Using this library in any product without modifications doesn't limit how that product may be licensed.
Full text of the license