UnrealCLR icon indicating copy to clipboard operation
UnrealCLR copied to clipboard

Creation of properties from managed code

Open OlsonDev opened this issue 3 years ago • 25 comments

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?

OlsonDev avatar Nov 03 '20 20:11 OlsonDev

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.

nxrighthere avatar Nov 03 '20 20:11 nxrighthere

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?

OlsonDev avatar Nov 03 '20 20:11 OlsonDev

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.

nxrighthere avatar Nov 03 '20 21:11 nxrighthere

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?

OlsonDev avatar Nov 04 '20 02:11 OlsonDev

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.

nxrighthere avatar Nov 04 '20 09:11 nxrighthere

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 avatar Nov 06 '20 20:11 Dreamescaper

@Dreamescaper Interesting, thanks for the link.

nxrighthere avatar Nov 06 '20 21:11 nxrighthere

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);
}

riddlemd avatar Dec 24 '20 18:12 riddlemd

I'm planning to do something similar with aspects injection, but automatically.

nxrighthere avatar Dec 25 '20 08:12 nxrighthere

Just a quick question: Has there been any progress on this ? I guess there are two parts,

  1. Redirecting properties to engine properties via aspect weaving
  2. Creating engine properties from normal C# properties without having to create blueprint base classes..

BernhardGlueck avatar Apr 09 '21 01:04 BernhardGlueck

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.

nxrighthere avatar Apr 09 '21 08:04 nxrighthere

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.

chismar avatar May 27 '21 22:05 chismar

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.

nxrighthere avatar May 28 '21 08:05 nxrighthere

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 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.

@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?

chaojian-zhang avatar Jun 05 '21 13:06 chaojian-zhang

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.

nxrighthere avatar Jun 06 '21 15:06 nxrighthere

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?

chismar avatar Jun 09 '21 22:06 chismar

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.

nxrighthere avatar Jun 10 '21 08:06 nxrighthere

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?

chismar avatar Jun 10 '21 19:06 chismar

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.

nxrighthere avatar Jun 12 '21 13:06 nxrighthere

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?

chismar avatar Jun 20 '21 16:06 chismar

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.

nxrighthere avatar Jun 20 '21 17:06 nxrighthere

Right. Yeah, that clears it up. Thanks for explaining. Never thought that to be an issue.

chismar avatar Jun 20 '21 17:06 chismar

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.

nxrighthere avatar Jul 07 '21 18:07 nxrighthere

Any progress on this front?

riddlemd avatar Oct 21 '21 20:10 riddlemd

You can find some info here.

nxrighthere avatar Oct 23 '21 15:10 nxrighthere