MCRO
C++23 utilities for Unreal Engine.
Loading...
Searching...
No Matches
MCRO

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.

++
#include "Mcro/Error.h"
FCanFail FK4ADevice::Tick_Sync()
{
using namespace Mcro::Error;
ASSERT_RETURN(Device.is_valid())
->AsCrashing()
->BreakDebugger()
->WithCppStackTrace()
->Notify(LastError);
try
{
k4a::capture capture;
ASSERT_RETURN(Device.get_capture(&capture))->Notify(LastError);
if (auto color = capture.get_color_image(); ColorStream->Active)
{
PROPAGATE_FAIL(ColorStream->Push(color))->Notify(LastError);
}
if (auto depth = capture.get_depth_image(); DepthStream->Active)
{
PROPAGATE_FAIL(DepthStream->Push(depth))->Notify(LastError);
}
if (auto ir = capture.get_ir_image(); IRStream->Active)
{
PROPAGATE_FAIL(IRStream->Push(ir))->Notify(LastError);
}
}
catch (k4a::error const& exception)
{
return IError::Make(new TCppException(exception))
->AsFatal()->WithCppStackTrace()->WithLocation()
->Notify(LastError);
}
return Success();
}
#define PROPAGATE_FAIL(expression)
#define ASSERT_RETURN(condition)
Definition Error.h:606
FORCEINLINE FCanFail Success()
Definition Error.h:590

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:

  1. The potentially lengthy delegate types are no longer needed to be repeated.
  2. 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)
{
// etc ...
}
// etc...
}
  • We can do:

    ++
    struct FListener : TSharedFromThis<FListener>
    {
    TMulticastDelegate<void(FObservable const&)> PropagateEvent;
    void OnFirstStateReset(FObservable const& observable, FString const& capturedData)
    void BindTo(FObservable& observable)
    {
    // Repeating the delegate type on call site is not necessary
    observable.SetDefaultInitializer(From(this, [this](FObservable const&) -> FString
    {
    return TEXT("Some default value");
    }));
    // Chaining events like this is a single line
    observable.OnStateResetTheFirstTimeEvent.Add(From(this, PropagateEvent));
    // No need to decide on the method of binding, the most suitable one is chosen via templating
    observable.OnStateResetTheFirstTimeEvent.Add(From(this, &FListener::OnFirstStateReset, TEXT("Capture this")));
    }
    }

  • Equivalent vanilla Unreal delegates:

    ++
    struct FListener : TSharedFromThis<FListener>
    {
    TMulticastDelegate<void(FObservable const&)> PropagateEvent;
    void OnFirstStateReset(FObservable const& observable, FString const& capturedData)
    void BindTo(FObservable& observable)
    {
    // Delegate API in these situations is pretty verbose
    observable.SetDefaultInitializer(FDefaultInitializerDelegate::CreateSPLambda(this, [this](FObservable const&) -> FString
    {
    return TEXT("Some default value");
    }));
    // Chaining events is a new Feature introduced with From
    observable.OnStateResetTheFirstTimeEvent.AddSPLambda(this, [this](FObservable const& observableArg)
    {
    PropagateEvent.Broadcast(observableArg);
    });
    // Ok here the difference is almost nothing
    observable.OnStateResetTheFirstTimeEvent.AddSP(this, &FListener::OnFirstStateReset, TEXT("Capture this"));
    }
    }

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:

++
// TBelatedEventDelegate is an alias for TEventDelegate<Signature, BelatedInvoke>
TBelatedEventDelegate<void(int)> SomeEvent;
// Broadcast first
SomeEvent.Broadcast(42);
// Late subscribers will not be left behind
SomeEvent.Add(From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("The last argument this event broadcasted with: %d"), value);
}));
// -> The last argument this event broadcasted with: 42
// This event doesn't have this behavior set by default
TEventDelegate<void(int)> SomeOtherEvent;
// Broadcast first
SomeOtherEvent.Broadcast(1337);
// Late subscribers still can explicitly ask to be notified
SomeOtherEvent.Add(
From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("The last argument this event broadcasted with: %d"), value);
}),
);
// -> The last argument this event broadcasted with: 1337

Or your thing only needs to do its tasks on the first time a frequently invoked event is triggered?

++
TEventDelegate<void(int)> SomeFrequentEvent;
// This function should only be called the first time after its binding
SomeFrequentEvent.Add(
From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("This value is printed only once: %d"), value);
}),
InvokeOnce
);
SomeFrequentEvent.Broadcast(1);
// -> This value is printed only once: 1
SomeFrequentEvent.Broadcast(2);
// (nothing is executed here)

Chaining events?

++
TMulticastDelegate<void(int)> LowerLevelEvent;
TEventDelegate<void(int)> HigherLevelEvent;
HigherLevelEvent.Add(From([](int value)
{
UE_LOG(LogTemp, Display, TEXT("Value: %d"), value);
}));
LowerLevelEvent.Add(HigherLevelEvent.Delegation(this /* optionally bind an object */));
LoverLevelEvent.Broadcast(42);
// -> Value: 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

++
#include "Mcro/TypeName.h"
using namespace Mcro::TypeName;
struct FMyType
{
FStringView GetTypeString() { return TTypeName<FMyType>; } // -> "FMyType" as view
}

even better with C++ 23 deducing this

++
#include "Mcro/TypeName.h"
using namespace Mcro::TypeName;
struct FMyBaseType
{
// returns const-ref to a templated global variable
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());
// -> This is `FMyDerivedType`
// BUT!
FMyBaseType const& myBaseVar = myVar;
UE_LOG(LogTemp, Display, TEXT("This is `%s`"), *myBaseVar.GetTypeString());
// -> This is `FMyBaseType`

MCRO provides a base type IHaveType for storing the final type as an FName to avoid situations like above

++
#include "Mcro/Types.h"
using namespace Mcro::Types;
struct FMyBaseType : IHaveType {}
struct FMyDerivedType : FMyBaseType
{
FMyDerivedType() { SetType(); }
}
FMyDerivedType myVar;
UE_LOG(LogTemp, Display, TEXT("This is as expected a `%s`"), *myVar.GetType().ToString());
// -> This is as expected `FMyDerivedType`
FMyBaseType const& myBaseVar = myVar;
UE_LOG(
LogTemp, Display,
TEXT("But asking the base type will still preserve `%s`"),
*myBaseVar.GetType().ToString()
);
// -> But asking the base type will still preserve `FMyDerivedType`

Auto modular features

One use of TTypeName is making modular features easier to use:

++
#include "Mcro/AutoModularFeatures.h"
using namespace Mcro::AutoModularFeatures;
// In central API module
class IProblemSolvingFeature : public TAutoModularFeature<IProblemSolvingFeature>
{
public:
virtual void SolveAllProblems() = 0;
};
// In another implementation specific module
// Define implementation first
class FProblemSolver : public IProblemSolvingFeature, public IFeatureImplementation
{
public:
FProblemSolver() { Register(); }
virtual void SolveAllProblems() override;
};
// Create one global instance
FProblemSolver GProblemSolver;
// In some other place, which doesn't know about the above implementation, the feature is used:
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?

++
using namespace Mcro::Observable;
struct FMyStuff
{
FMyStuff()
{
// Get previous values as well when `StorePrevious` flag is active
State.OnChange([](int next, TOptional<int> previous)
{
// If `StorePrevious` flag is active we should always have a value in `previous`
// so we should never see -2
UE_LOG(LogTemp, Display, TEXT("Changed from %d to %d"), previous.Get(-2), next);
});
// Listen to change only the first time
State.OnChange(
[](int next) { UE_LOG(LogTemp, Display, TEXT("The first changed value is %d"), next); },
);
}
void Update(int input)
{
if (State.HasChangedFrom(input))
{
// Do things only when input value has changed from previous update
// Combine it with short-circuiting maybe ;)
}
}
};
struct FFoobar : TSharedFromThis<FFoobar>
{
FMyStuff MyStuff;
void Thing(TChangeData<T> const& change, FString const& capture)
{
UE_LOG(LogTemp, Display, TEXT("React to %d with %s"), change.Next, *capture);
}
void Do()
{
MyStuff.Update(1);
// -> Changed from -1 to 1
// -> The first changed value is 1
MyStuff.Update(2);
// -> Changed from 1 to 2
MyStuff.Update(2);
// (nothing is logged as previous update was also 2)
MyStuff.OnChange(
[](int next) { UE_LOG(LogTemp, Display, TEXT("Arriving late %d"), next); },
);
// -> Arriving late 2
MyStuff.Update(3);
// -> Changed from 2 to 3
// -> Arriving late 3
MyStuff.OnChange(From(this, &FFoobar::Thing, TEXT("a unicorn")));
MyStuff.Update(4)
// -> Changed from 3 to 4
// -> Arriving late 4
// -> React to 4 with a unicorn
}
}
TInferredDelegate< Function, Captures... > From(Function func, const Captures &... captures)
virtual FDelegateHandle OnChange(TDelegate< void(TChangeData< T > const &)> onChange, EInvokeMode invokeMode=DefaultInvocation) override
Definition Observable.h:330

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)

++
#include "Mcro/FunctionTraits.h" // it also brings in Concepts
{
using namespace Mcro::FunctionTraits;
template <
// CFunctorObject constraints to a lambda function
// use CFunctionLike to allow anything callable including function pointers
CFunctorObject Initializer,
// Get the first argument of `Initializer`
typename Argument = TFunction_Arg<Initializer, 0>,
// That argument must be a UObject, and also cache its type
CUObject Result = std::decay_t<Argument>
>
// That argument must also be an l-value ref
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";
using namespace Mcro::Slate;
// Define a reusable block of attributes
auto Text(FString const& text) -> TAttributeBlock<STextBlock>
{
return [&](STextBlock::FArguments& args) -> auto&
{
return args
. Text(FText::FromString(text))
. Font(FCoreStyle::GetDefaultFontStyle("Mono", 12));
};
}
// Nest these blocks
auto OptionalText(FString const& text) -> TAttributeBlock<STextBlock>
{
return [&](STextBlock::FArguments& args) -> auto&
{
return args
. Visibility(IsVisible(!text.IsEmpty()))
/ Text(text);
};
}
// Accept attribute blocks as arguments
auto ExpandableText(
FText const& title,
FString const& text,
TAttributeBlock<STextBlock> const& textAttributes
{
return [&](SExpandableArea::FArguments& args) -> auto&
{
return args
. Visibility(IsVisible(!text.IsEmpty()))
. AreaTitle(title)
. InitiallyCollapsed(true)
. BodyContent()
[
SNew(STextBlock) / textAttributes
];
};
}
TUniqueFunction< TArgumentsOf< T > &(TArgumentsOf< T > &)> TAttributeBlock
Definition Slate.h:83
MCRO_API EVisibility IsVisible(bool visible, EVisibility hiddenState=EVisibility::Collapsed)

Or add slots right in the Slate declarative syntax derived from an input array:

++
// This is just a convenience function so we don't repeat ourselves
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";
using namespace Mcro::Text;
// Accept many string types at once
template <CStringOrViewOrName String>
bool GetSomethingCommon(String&& input) { ... }
// Work with std::string types
template <CStdStringOrViewInvariant String>
size_t GetLength(String&& input) { return input.size(); }
++
// Type alias for choosing a std::string which is best matching the current TCHAR.
FStdString foo(TEXT("bar")); // -> std::wstring (on Windows at least)
std::wstring FStdString
Definition Text.h:30
++
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