[Question] Under the Hood


photo

Recommended Posts

I'm trying to get an understanding of what Unigine is doing internally related specifically to the C# layer. I'm running into a few cases where I'm getting failures. Some of these produce fatal errors that get swallowed and/or popup briefly in the console before it terminates. Often with with only a generic runtime error. I can sometimes guess at why some of these occur, but understanding the basic C#/Net layer architecture would be helpful. Are there any resources for this now or planned?

I do have two questions for now. I run into cases that seem to require a class have a parameterless constructor. And, trying to create a Component instance with "new" fails (not sure if this is always the case, haven't dug in).  These cases are similar to behavior encountered when using `Activator` or `ActivatorUtilities` or DI. Programming using this assumption seems to avoid many of these cases, but I'm still just guessing at what is happening.

Is the layer Activator (or DI/Activator) based? Or more generally, why do I encounter the need for parameterless constructors?

What is the basic (quick overview) lifecycle process for a Component from a C# internal perspective (Under the Hood)?  

Link to post

I'll put some code and a project together that shows some of the things I'm seeing. It many take me a couple of days to collect things. 

Some of these are less about it not doing what I expected, but more me not knowing what to expect lol.

Link to post

I'm figuring some things out by forcing errors and looking and the stack. It is Activator based.

I've hit a strange error that I can't reproduce, but I've captured it in the attached file. The approach in the project is a little different (Service  Locators, etc.), but I don't think that's part of the problem - I've encountered this and similar things before in vanilla projects. I'm returning to some work with a different approach where I stopped when hit problems at the same point. 

The effect of the case was I started getting a ground plane in my project when I had no nodes or code creating one. -I thought I had a Ghost Node! Looks like a script got inserted into my launch path but I can't trace where it is. It only happens when running from VS and not from the Editor.

This is what is running now.

Unigine~# world_load "Template"
Script loading "core/unigine.usc" 2ms
... (console/visualizer/fps)
Script loading "editor2/resources/template.usc" 3ms
World loading "editor2/resources/template.world" 70ms

 The system change it. Before I had

Unigine~# world_load "PersonControllers"
Script loading "core/unigine.usc" 2ms
... (console/visualizer/fps)
World loading "PersonControllers.world" 12ms

The problem is somehow related to Components. I think it has to do with initializing fields in Components or maybe "newing" them, possibly newing with overloaded constructors.

 I have the component `HotSwap`. I was trying to create "free" instances (with new()), configure them, and then attach them to running/active nodes "Hot Swapping".
I'm trying to do the what is in the commented (and unsupported) line //anchor.AddComponent<HotSwap>(weapon);

The calling code is in init(). I had just added "?? throw new exception"

HotSwap weapon = new(1, new SwapConfiguration());
weapon.SwapConfiguration.FactorA = 4;

NodeDummy n = new();

HotSwap h = n.AddComponent<HotSwap>() ?? throw new Exception();
  h.SwapConfiguration.FactorA = 6;

On the component side, the code is

public class HotSwap : Component {
    public SwapConfiguration SwapConfiguration;
    public readonly List<SwapConfiguration> swaps = new ();

    public HotSwap() { }

    public HotSwap(int mountPosition, SwapConfiguration config) {
        SwapConfiguration = config;
    }
}

public struct SwapConfiguration {
    public int FactorA;
    public int FactorB;
}

I had just added the List<SwapConfiguration> line. 

I've been running the rest of the code in other projects for a couple of days with no problems. The reason I suspect this line is because when I stopped here before, it was because my fields initialized at declaration were coming up null.  I wasn't "newing" so I had to use init(). I had other things to work on so I moved on.

349373116_PersonControllers-Bugged.zip

Link to post

Hi Tessalator,

 

Quote

The effect of the case was I started getting a ground plane in my project when I had no nodes or code creating one. -I thought I had a Ghost Node! Looks like a script got inserted into my launch path but I can't trace where it is. It only happens when running from VS and not from the Editor.

 

This happens cause of when you are running from VS, there's launch arguments in "debug" section of project settings. You can remove or modify argument "-console_command "world_load \"Template\""" to load correct scene. Also template.world is internal scene used for creation of new scenes, it should not be modified.

While in editor, there's option "Run Current World" in running options. You can uncheck it, if you are loading world in some other script/config

 

Link to post

Hi vvvaseckiy.

The thing I'm seeing is Unigine is somehow modified that command. I know it seems strange. But if you look a the logs you'll see there is only one minute between the two runs giving different results. The only thing I was doing was editing the C# lines I posted. I've had that command altered by Unigine in other cases, but those were when I was digging around at a lower level and not doing normal work. In this case I was just writing C# code. Could there be some "soft fail" code that might modify that?  

Thanks for pointing out "Run Current World".

Link to post

This thread was originally about understanding internals and I like to get back on that path.

How can I find out what the CSPropertyGenerator is doing? I'm finding things I can't do in Unigine C# that work otherwise. It's not that Unigine is broken, just limited.  I'm trying to figure out what those limitations are by knowing what is happening inside.

As I originally thought, Components get instantiated by Activator, which wasn't too surprising. But it seems that many (all?) other things are too. And in some unexpected places. Take this code:

    [Component(PropertyGuid = "33b00673a02c193666fdbf1d3b4c35d8ded9bf57")]
    public class Controller : Component {
        public Pawn? pawn;
    }

    public class Pawn {
        private readonly Node node;

        public Pawn(Node node) {
            this.node = node;
        }
    }

  Pretty standard and straight forward C#. But it fails in Unigine. 

1>CSPropertyGenerator started
1>Unhandled exception. System.MissingMethodException: No parameterless constructor defined for type 'UnigineApp.Pawn'.
1>   at System.RuntimeType.CreateInstanceDefaultCtorSlow(Boolean publicOnly, Boolean wrapExceptions, Boolean fillCache)
1>   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
1>   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
1>   at System.Activator.CreateInstance(Type type)
1>   at Unigine.ComponentClassData.SaveProperty(String file_name)
1>   at Unigine.ComponentSystem.SaveProperties()
1>   at UnigineUtils.CSPropertyGenerator.Main(String[] args) in D:\BA\work\214de2b1df7f6eca\source\csharp\tools\CSPropertyGenerator\main.cs:line 106
1>G:\Games\Check\Check.csproj(59,3): error MSB3073: The command ""dotnet" "bin\cspropgen_x64d.dll" -p "bin\Check_x64d.dll" -data_path ../data/" exited with code -532462766.
1>Done building project "Check.csproj" -- FAILED.

I can understand what something like a component might use Activator, but not why a vanilla class would. More curious is that my code doesn't try to create an instance of `Pawn` but the error comes from trying to instantiate `Pawn`. Why is any instance being created at all? It may be helpful to have a section in the Doc's where "Moving for Unity" and "Moving from Unreal" are that talks about how Unigine works with C#.  

Link to post

Hi Tessalator,

Why is any instance being created at all?
Each component generates a property that doesn't have "null" parameters (because we can't show "nothing" in the Parameters window). Each field must have a default value. We use the Activator to get these default parameters from components (Reflection only doesn't show the full picture - only for simple types). Maybe we will use something like FormatterServices.GetUninitializedObject to get parameters from "complex" classes that doesn't have parameterless constructors in future SDK releases. Or... We will ignore these classes.

Have you tried using the [HideInEditor] attribute?

[HideInEditor]
public Pawn pawn;


Best regards,
Alexander

Link to post

Thanks for the details Alexander, that was helpful. [Hide in Editor] works and covers a lot of my cases.

So, Activator is only used in the "build editor" process and not part of normal execution - correct?

 

New question (let me know if I should start a new thread for new questions).

The editor doesn't show the base class properties for derived inner classifiers. Derived and inner work individually, but not together.

Is it by design, bug, or just not implemented?

[Component(PropertyGuid = "2496ff482fd89a07027c8fba07305bd1d5e1e2fc")]
public class Controller : BaseComponent {

    public Inner Child;
    public DerivedInner StepChild;

    public class Inner {
        // Shows in Editor
        public string Child;
    }

    public class DerivedInner : DataShape {
        // Shows in Editor
        public string StepChild;
      	
      	// Missing: Does not show in editor
      	[public string DataShapeString;]
    }

}

public class DataShape {
    // Doesn't show in editor in derived class
    public string DataShapeString;
}

public class BaseComponent : Component {
    // Shows in Editor
    public string str;
    public int num;
}

image.png.e670165e92285b74ccfee476000b684f.png

Link to post

So, Activator is only used in the "build editor" process and not part of normal execution - correct?
We are using Activator during runtime also. Every time after adding a new property or calling AddComponent<>.
I think we could change it to ILGenerator.Emit (that we use to deep clone components). Maybe it would be faster a little bit.

Is it by design, bug, or just not implemented?
Oh, thank you so much. It is a bug. It should work. I've created a task in our bug tracker. I hope we'll fix it for the next version of the SDK (2.15).

Best regards,
Alexander

Link to post

Minimizing Activator would be good. Where it is used it would be nice to have ActivatorUtilities also.

The engine really lends itself to component based architectures. I'm building a framework/layer that sits between developers and the engine to support this. In my C# "non-editor" code I'm using the built in Dependency Injection components and take a reference to `Microsoft.Extensions.DependencyInjection`. I'm using a simple Service Locator pattern. It plays well with the engine so far. I am hitting a bump again with CSPropertyGenerator. It's nothing I can't work around. It's bit annoying.

This code works fine.

public class ServiceComponent : Component {
        
    private IServiceProvider? serviceProvider;
    protected IServiceProvider Services => serviceProvider ?? throw new Exception("...");

    protected override void OnReady() {
        base.OnReady();
        serviceProvider = Program.Provider ?? throw new Exception("...");
    }
}

I use NRT's and some standard patterns. Normally I would set the service provider in the constructor so I could make it read only and not nullable.

My preferred code would be:

public class ServiceComponent : Component {

    protected readonly IServiceProvider services;

    protected ServiceComponent() {
        services = Program.Provider ?? throw new Exception("...");
    }
}

Unigine let's me get close, but the generator yacks at this (parameterless) constructor with a ref error:

System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.Extensions.DependencyInjection

Could we get a fix? Would just adding a reference to the DI assemblies to the CSPropertyGenerator be enough?

Link to post

Would just adding a reference to the DI assemblies to the CSPropertyGenerator be enough?
Yeah, I think so.

Could we get a fix?
You can try to remove <Target Name="PostBuild" AfterTargets="PostBuildEvent">  in your .csproj file to disable the CSPropertyGenerator and emulate its work directly in your code. Just type in AppSystemLogic.Init():

ComponentSystem.SaveProperties();

That's all.

  • Like 1
Link to post
Posted (edited)

That's what I needed alexander. Works perfectly.  Thanks!

Looking at the docs  for ComponentSystem it says "for all registered components" in many places. Curious about component "registration". Would you please give me a quick overview of what is happening there?

Edited by Tessalator
Link to post

Would you please give me a quick overview of what is happening there?
Oops. This is a mistake in the documentation. It was just copy-pasted from the C++ side (it has a registerComponent<> method and a REGISTER_COMPONENT() macro).
On C# side ComponentSystem automatically searches for all classes inherited from "Component" in the current assembly.

Link to post