Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creation of properties from managed code #61

Open
OlsonDev opened this issue Nov 3, 2020 · 25 comments
Open

Creation of properties from managed code #61

OlsonDev opened this issue Nov 3, 2020 · 25 comments
Labels
enhancement New feature or request

Comments

@OlsonDev
Copy link
Contributor

OlsonDev commented Nov 3, 2020

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?

@nxrighthere nxrighthere added the enhancement New feature or request label Nov 3, 2020
@nxrighthere nxrighthere changed the title Get{Type}(string name) and Set{Type}(string name, ref {Type} value) method pairs not working Creation of properties from managed code Nov 3, 2020
@nxrighthere
Copy link
Owner

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.

@OlsonDev
Copy link
Contributor Author

OlsonDev commented Nov 3, 2020

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?

@nxrighthere
Copy link
Owner

nxrighthere commented Nov 3, 2020

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.

@OlsonDev
Copy link
Contributor Author

OlsonDev commented Nov 4, 2020

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?

@nxrighthere
Copy link
Owner

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.

@Dreamescaper
Copy link

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)

@nxrighthere
Copy link
Owner

@Dreamescaper Interesting, thanks for the link.

@riddlemd
Copy link

riddlemd commented Dec 24, 2020

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

@nxrighthere
Copy link
Owner

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

@BernhardGlueck
Copy link

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

@nxrighthere
Copy link
Owner

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.

@chismar
Copy link

chismar commented May 27, 2021

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.

@nxrighthere
Copy link
Owner

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.

@chaojian-zhang
Copy link

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?

@nxrighthere
Copy link
Owner

Why wouldn't ActorReference.ToActor() 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 dotnet/runtime#40484 (comment).

@chismar
Copy link

chismar commented Jun 9, 2021

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?

@nxrighthere
Copy link
Owner

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.

@chismar
Copy link

chismar commented Jun 10, 2021

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?

@nxrighthere
Copy link
Owner

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.

@chismar
Copy link

chismar commented Jun 20, 2021

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?

@nxrighthere
Copy link
Owner

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.

@chismar
Copy link

chismar commented Jun 20, 2021

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

@nxrighthere
Copy link
Owner

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.

@riddlemd
Copy link

Any progress on this front?

@nxrighthere
Copy link
Owner

You can find some info here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants