UnrealCLR
UnrealCLR copied to clipboard
Creation of properties from managed code
I have a class deriving from Actor
, with a constructor that looks like:
public class Card : Actor {
public Card(string name = null) : base(name) {
// Set up mesh, material, register events
AddTag("MyTag");
SetInt("MeaningOfLife", 42);
var meaningOfLife = 9001;
var gotIt = GetInt("MeaningOfLife", ref meaningOfLife);
Debug.AddOnScreenMessage(1, 3.0f, Color.Red, $"Meaning of life: {gotIt}; {meaningOfLife}");
}
}
When I play in the editor, it outputs False; 9001
when I'd expect True; 42
.
I've played around and moved the GetInt
and SetInt
calls all over, thinking perhaps it was an object lifetime issue, but I can't seem to get this to ever print out my expectations. Currently I'm instantiating a Card
in OnWorldPostBegin()
. I've also tried GetBool
and SetBool
and those didn't work either -- I'm guessing this is broken for all of the method pairs.
The AddTag()
call seems to work -- I can see it when I inspect the Details pane with the Card actor selected.
Is this a bug or am I doing something wrong?
The property MeaningOfLife
is not created. You need to explicitly create a blueprint with the property and use it as a base class in the constructor of an actor passed to the base. SetInt()
in this case should return false
indicating failure.
Properties can't be created from managed code at the moment.
Thank you! I just figured that out on my own. :-)
Is this the recommended way to maintain state on a subclassed Actor? I'd rather not make Blueprint classes that are just property bags, if you will. It doesn't feel like the native C# way of doing things.
For example, I handle OnActorBeginCursorOver(ActorReference actor)
and I do actor.ToActor<Card>().SomeProperty
and SomeProperty
is reset to its default value because you're calling FormatterServices.GetUninitializedObject(...)
, and thus the instance you give me isn't the same one as I originally instantiated. Do I need to maintain a... dictionary of objects by ID and then take the actor you give me and then just look it up and use that? Or should I actually create blueprint instances for each object and dynamically look up/set properties in C++ land each time?
Is this the recommended way to maintain state on a subclassed Actor? I'd rather not make Blueprint classes that are just property bags, if you will. It doesn't feel like the native C# way of doing things.
If you don't need to pass it back and forth to the engine (and later in future for network synchronization with a few other things), then you can just declare managed variables/properties as you normally do.
For example, I handle
OnActorBeginCursorOver(ActorReference actor)
But in this case, yes, it becomes a problem since object references come from the engine, so data should be stored there as well. Yes, the only option is to use workarounds with object IDs, at the moment, since properties is the only intermediate type-safe data storage.
I wish we have aspect-oriented interceptors for properties as a language feature like how it's done in PostSharp, so I could transparently hook and redirect managed properties to Unreal ones.
That's a good point about network sync.
I'll give it a think and maybe come up with some abstraction so I can deal more with POCOs than with all this glue. Is it possible to create Blueprints programmatically/at runtime? I can't imagine a way to make Source Generators work.. because it'd more so be trying to generate stuff the engine can consume instead of managed code consuming.. or possibly generating both -- think INotifyPropertyChanged
generation, where it generates calls to getting/setting Blueprint properties from an otherwise auto-prop. Either way.. the main problem would be generating Blueprints, I think.
I noticed you flagged this as an enhancement, did you want me to keep this issue open?
Is it possible to create Blueprints programmatically/at runtime?
Yes, it's possible, and I'm working on it, but it's a very tricky thing.
I noticed you flagged this as an enhancement, did you want me to keep this issue open?
Yes, please, keep it open. This is one of the high-priority tasks in my list for a few months already.
I wish we have aspect-oriented interceptors for properties as a language feature like how it's done in PostSharp, so I could transparently hook and redirect managed properties to Unreal ones.
You might wanna check https://github.com/pamidur/aspect-injector - Apache-licensed compile-time AOP framework. (I have no relation to this project, just something I've used previously)
@Dreamescaper Interesting, thanks for the link.
For anyone that wants a short term solution that will allow them to use properties to feed stuff in and out of their blueprints (without a lot of boilerplate code) you can use these bits of code (You still need to create the properties in the blueprint, but now you can use them in a more natural to c# way):
Create a file called ActorExtensions.cs and put this class in it:
public static class ActorExtensions
{
public static bool GetBoolOrDefault(this Actor actor, string name)
{
bool output = default;
actor.GetBool(name, ref output);
return output;
}
public static byte GetByteOrDefault(this Actor actor, string name)
{
byte output = default;
actor.GetByte(name, ref output);
return output;
}
public static double GetDoubleOrDefault(this Actor actor, string name)
{
double output = default;
actor.GetDouble(name, ref output);
return output;
}
public static T GetEnumOrDefault<T>(this Actor actor, string name) where T : Enum
{
T output = default(T);
actor.GetEnum<T>(name, ref output);
return output;
}
public static float GetFloatOrDefault(this Actor actor, string name)
{
float output = default;
actor.GetFloat(name, ref output);
return output;
}
public static int GetIntOrDefault(this Actor actor, string name)
{
int output = default;
actor.GetInt(name, ref output);
return default;
}
public static long GetLongOrDefault(this Actor actor, string name)
{
long output = default;
actor.GetLong(name, ref output);
return output;
}
public static short GetShortOrDefault(this Actor actor, string name)
{
short output = default;
actor.GetShort(name, ref output);
return output;
}
public static string GetTextOrDefault(this Actor actor, string name)
{
string output = default;
actor.GetText(name, ref output);
return output;
}
public static uint GetUIntOrDefault(this Actor actor, string name)
{
uint output = default;
actor.GetUInt(name, ref output);
return output;
}
public static ulong GetULong(this Actor actor, string name)
{
ulong output = default;
actor.GetULong(name, ref output);
return output;
}
public static ushort GetUShortOrDefault(this Actor actor, string name)
{
ushort output = default;
actor.GetUShort(name, ref output);
return output;
}
}`
Then when you define a property simply do:
public string DisplayName
{
get => this.GetTextOrDefault(nameof(DisplayName));
set => SetText(nameof(DisplayName), value);
}
I'm planning to do something similar with aspects injection, but automatically.
Just a quick question: Has there been any progress on this ? I guess there are two parts,
- Redirecting properties to engine properties via aspect weaving
- Creating engine properties from normal C# properties without having to create blueprint base classes..
Yes, the second part is tricky. The way how engine work with properties that created at runtime is hard to wrap around in a flexible way.
Have you considered generating C++ code based on defined C#? I mean, for property/components definition. I'm not that knowlegeable in UE as in .NET development, but I've used Roslyn for build-time C# code generation with great success and don't see why it wouldn't work for generating C++ bindings for that particular purpose. It will allow typesafe and fast interop as well, instead of relying on strings.
Have you considered generating C++ code based on defined C#?
Yes, and I even implemented this for testing purposes. This approach has several caveats on the engine side. For example, as soon as you make more advanced C++ code touching blueprints it becomes impossible to dynamically reload the plugin with generated code, the entire editor has to be restarted to reflect changes.
It will allow typesafe and fast interop as well, instead of relying on strings.
The current implementation for accessing properties is type-safe and relatively fast. The primary goal with the generation of properties with aspect injection is to improve usability and workflow.
Do I need to maintain a... dictionary of objects by ID and then take the actor you give me and then just look it up and use that?
@OlsonDev How do you make use of object IDs? I mean, which UnrealCLR functions work with Object IDs?
For example, I handle
OnActorBeginCursorOver(ActorReference actor)
and I doactor.ToActor<Card>().SomeProperty
andSomeProperty
is reset to its default value because you're callingFormatterServices.GetUninitializedObject(...)
, and thus the instance you give me isn't the same one as I originally instantiated.
@OlsonDev If I understand it correctly, were you suggesting that ActorReference.ToActor<T>()
's return result is the default rather than the actual instance you are passing in? That's wild isn't it? What do you mean by "isn't the same one as I originally instantiated" - were you referring to the ActorReference you passed in through OnActorBeginCursorOver()
?
@nxrighthere Why wouldn't ActorReference.ToActor<T>()
return the reference to the "actual" actor? What's the use of this function if it doesn't?
Why wouldn't ActorReference.ToActor<T>() return the reference to the "actual" actor? What's the use of this function if it doesn't?
As long as it's not null it will always return a reference to the actual actor. This function converts a blittable pointer to a managed reference, this intermediate conversion is required due to interop limitations of .NET, see https://github.com/dotnet/runtime/issues/40484#issuecomment-694381376.
For example, as soon as you make more advanced C++ code touching blueprints it becomes impossible to dynamically reload the plugin with generated code, the entire editor has to be restarted to reflect changes.
I mean, I don't know much about UE, but wouldn't the generated code be part of the would-be game logic cpp project? And if so, how would it result in engine restart requirement?
I mean, I don't know much about UE, but wouldn't the generated code be part of the would-be game logic cpp project?
The game code is a module essentially, similarly to plugins it's being reloaded after you make changes to C++. Once you add generated code as a blueprint function library the same limitations apply to it.
Uhm, I don't follow. What's the difference between adding .cpp file by yourself and via File.WriteAllText() or something? Manually added classes certainly don't require engine restarts. do they?
It's not about how do you add code, it's about how the engine's runtime work with modules. Generation and compilation is not an issue, the issue is reflecting the changes in the editor dynamically.
I don't want to annoy you by continuing to ask the same question, but it's still very confusing. Does Unreal have the same issue you describing when you add .cpp files manually? Doesn't it already reflect the changes dynamically, based on the new cpp code without requiring to restart? If so, what's the difference would be to add .cpp files automatically?
What I mean is that, if, say, I want to write a .cs file with a class with several intended-to-be UPROPERTY's, that would look like
public class MyClass { [UProperty]public int myInt; }
And then a Roslyn based codegen would pop up and generate a .cpp file with the same layout and some helpful boilerplate to help connect my class and the native ones.
Then .cpp project gets rebuilt as per new changes, .net embedding host is restarted and everything's good, aint't?
Does Unreal have the same issue you describing when you add .cpp files manually?
Yes.
Doesn't it already reflect the changes dynamically, based on the new cpp code without requiring to restart?
Yes it does, but hot-reload has limitations, for example, you can't reload a module with a blueprint function library.
I don't want to annoy you by continuing to ask the same question, but it's still very confusing.
You have to understand how inconsistent and limited hot-reload in Unreal, which makes code generation useless.
Right. Yeah, that clears it up. Thanks for explaining. Never thought that to be an issue.
Yes, the only option is to use workarounds with object IDs, at the moment, since properties is the only intermediate type-safe data storage.
It seems that now we have Data Registries which can be used as an intermediate type-safe data storage. It's introduced in Unreal Engine 5 and 4.27.0, so I'm going to investigate it.
Any progress on this front?
You can find some info here.