After starting this project i discovered that there is another project with the same name (naming stuff is hard) - UnrealSharp and also another project with almost the same name - UnrealCSharp. Since these projects are already doing what I aimed to do, and also have very similar names, I feel that I will have to rename the project and figure out where it is positioned in relation to the projects that already exist.
I have learned a lot from this project thought:
- Writing plugins for UHT
- Embedding dotnet in C++
- Reading the source code of UBT
- It lead to a PR to the Unreal source code
UnrealSharp attempts to add a dotnet core
integration to some parts of Unreal Engine. It does this by creating what the dotnet
documentation calls a "native host" in the context of a subsystem, and exposes functionality by generating P/Invoke bindings for dotnet
using a header tool plugin. You can read more about this in my other repository dotnet-host-example where I play around with an example from Microsoft, and document some cases that were not really covered in the official channels (as far as I could tell).
The plugin is currently in an early stage and is probably not very useful at the moment.
The scope for version 1.0 is actor (and maybe character) integration and reducing boilerplate. In the future other classes may be supported as well.
My dream is to have a plugin where the user (developer) can work with his/her own dotnet
code, using any nuget
package they want etc., and using this without touching any of the internals of the plugin (this is currently impossible).
The UHT plugin that generates the bindings is currently using vyaml to read yaml comfiguration files. However, currently nuget packages do not work with UHT plugins (as far as I can tell).
I have made a pull request to the Unreal repo that should fix the issue, but if it is not accepted I will have to think of something else. The fix required is only one line though, so it's not too hard to do, but requiring users of a plugin to modify the build tool doesn't feel user friendly to me :)
Currently, it is possible to register an actor with the subsystem, and have it tick each frame using code written in csharp
. The Actor
class in dotnet
has properties for individually setting and getting the translation, rotation and scale of the C++ Actor
, as well as getting and setting the Transform
. The example provided uses dotnet
to make an Actor
float up and down:
public class TestActor : Actor
{
private bool _shouldGoUp = true;
private readonly float _speed = 100f;
public override void Tick(float deltaTime)
{
Vector vector = Translation;
float direction = _shouldGoUp ? 1f : -1f;
vector.Z += (_speed * deltaTime * direction);
if (vector.Z >= 400f || vector.Z <= 50f)
{
_shouldGoUp = !_shouldGoUp;
}
Translation = vector;
}
}
This is achieved by creating extern "C"
bindings which can then be used to P/Invoke back into the executable. We keep track of which C++ Actor
instance is bound to which C# instance by storing a pointer to the actor, which is in turn used to get and set things on the native side. This requires some ugly things but these can be hidden in properties in the base class in C":
public Vector Translation
{
get
{
ActorManager.GetTranslation(ActorPtr, out Vector vector);
return vector;
}
set
{
ActorManager.SetTranslation(ActorPtr, ref value);
}
}
As can be seen in the example, this allows us to expose a fairly intuitive api to the C# user:
Vector vector = Translation;
...
Translation = vector;
I am currently working on a Unreal Header Tool plugin that will generate the bindings when building the C++ code. This is done by specifying additional information in the meta
tag of the UPROPERTY
macro like so:
UPROPERTY(meta = (DotnetReadWrite = "true"))
uint64 Int64Prop;
This will generate binding code assuming that the property is public:
extern "C" __declspec(dllexport) inline void Get_Int64Prop(AUnrealSharpDemoActor const* Instance, void* Parameter) {
auto* TypedPtr = static_cast<uint64*>(Parameter);
*TypedPtr = Instance->Int64Prop;
}
Maybe we don't want to expose our properties for everyone to read and modify, in which case we can either use the reflection system:
UPROPERTY(meta = (DotnetReadWrite = "true", DotnetAccess = "Reflection"))
uint32 Int32Prop;
Which leads to the following bindings getting generated:
extern "C" __declspec(dllexport) inline void Get_Int32Prop(AUnrealSharpDemoActor const* Instance, void* Parameter) {
auto* TypedPtr = static_cast<uint32*>(Parameter);
static FProperty* Property = AUnrealSharpDemoActor::StaticClass()->FindPropertyByName("Int32Prop");
uint32 Val;Property->GetValue_InContainer(Instance, &Val);
*TypedPtr = Val;
}
We can also provide our own member function for getting and setting:
private:
UPROPERTY(meta = (DotnetReadWrite = "true", DotnetAccess = "DoubleProp"))
double DoubleProp;
public:
double GetDoubleProp() const { return {}; };
void SetDoubleProp(double Prop) {};
"DoubleProp" forms the base for the member function name and will generate bindings for GetDoubleProp
and SetDoubleProp
:
extern "C" __declspec(dllexport) inline void Get_DoubleProp(AUnrealSharpDemoActor const* Instance, void* Parameter) {
auto* TypedPtr = static_cast<double*>(Parameter);
*TypedPtr = Instance->GetDoubleProp();
}
At the current stage, these haven't been tested yet, so may work like a charm or not at all. I will update this README as I progress.
To control the source generation, the plugin expects a yaml cofiguration file called unrealsharp.config.yml
to be placed in the project directory.
This is currently a WIP (as many other things in here) and currently it looks something like this:
namespace-settings:
default-namespace: 'LambdaSnail.UnrealSharp.Samples'
namespace-perclass: true
class-namespaceoverrides: # Not in use
- 'override-1'
- 'override-2'
dotnet-projectdirectory: 'C:\Users\blomb\Desktop\unrealsharp\'
type-mappings:
map-overrides:
FVector:
dotnet-typename: Vector
dotnet-namespace: LambdaSnail.UnrealSharp
is-primitive: false
However, it is currently under heavy development and will be subject to change.
- Extend UHT: Design a way to automate the boilerplate required to set up a read/write of some data.
More functionality will be coming when I have the time :)
- Clean up the
Host
files and incorporate into the subsystem where approproate. "Unrealify" the coding style. - See if we can build the
UnrealSharpCore
assembly automatically as part of Unreal's normal buid process. - Set up communication between
dotnet
andcpp
to allow developers to read and write common data (e.g., transform data etc). - Decide how to handle common math operations etc that will be needed on the
dotnet
side. E.g., do we makeFVector
member functions available through callbacks, do we provide some external package for math operations, or do we leave the users to their own fates in this regard?- Thought: Since we are on dotnet core, the user could in theory use any
nuget
package they wish.
- Thought: Since we are on dotnet core, the user could in theory use any
- Decide what data to support (e.g., do we allow
dotnet
to get pointers to somehow subsystems, do we make it possible to manipulate animations etc.) - Set up a way for developers to provide their own
dotnet
assembly with their own types and register that in the system. -
Nice to have
Design a clean way for developers to extend the integration, by adding their own read and write callbacks for custom data. - Some example classes and documentation
This project is licensed under the MIT license. It contains modified code from the official dotnet samples repository found here: https://github.com/dotnet/samples/tree/main/core/hosting