Arch icon indicating copy to clipboard operation
Arch copied to clipboard

Unity support

Open gaozhou opened this issue 2 years ago • 16 comments

Can you provide a unity example?

gaozhou avatar May 05 '23 02:05 gaozhou

I will do that once i have some time :) Pretty busy rn with my thesis. Just out of curiosity, have you tried integrating it via a nuget unity plugin?

Arch sources can not be used, nugets however should be usable even if they make use of higher language features.

genaray avatar May 05 '23 22:05 genaray

In Decentraland we are already doing a PoC using Arch as an ECS framework. it's totally possible to use Arch DLLs compiled for netstandard2.1. There is one limitation though, Marshal.SizeOf in the static ctor of ComponentType does not work on IL2CPP for non-blittable (e.g. with reference type fields) and generic structures. @genaray It would be great if you could give advice if it's possible to replace Marshal.SizeOf with something else.

mikhail-dcl avatar May 09 '23 09:05 mikhail-dcl

First of all, it's probably the coolest thing I've heard in a long time and makes myself kind of proud too :) Also saw that you created a SystemGroups framework, I'll link it on the github page, maybe others are interested in it too ^^

To the second... there we, unfortunately, come across a limitation of C#. As an alternative there would be Unsafe.SizeOf, but this does not provide a type overload and is therefore only limited usable.

The easiest option here is to never use managed structs. Or... and here comes your solution... register them yourself beforehand. ComponentRegistry has methods to pre-register components and so this hurdle is avoided :)

public struct ManagedStruct{ public List<int> SomeInts{ get; set; } }

// Register
var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), 8, false); // 8 = Size in bytes of the managed struct.
ComponentRegistry.Add(componentType);

// Use
var entity = world.Create(new ManagedStruct());

genaray avatar May 09 '23 10:05 genaray

// Register
var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), 8, false); // 8 = Size in bytes of the managed struct.
ComponentRegistry.Add(componentType);

Great, it may work out, I will give it a try. Thank you

Also saw that you created a SystemGroups framework, I'll link it on the github page, maybe others are interested in it too

Sure, feel free

mikhail-dcl avatar May 09 '23 11:05 mikhail-dcl

// Register
var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), 8, false); // 8 = Size in bytes of the managed struct.
ComponentRegistry.Add(componentType);

Great, it may work out, I will give it a try. Thank you

Also saw that you created a SystemGroups framework, I'll link it on the github page, maybe others are interested in it too

Sure, feel free

Perfect! Lemme know if it works for your case :)

genaray avatar May 09 '23 13:05 genaray

I can confirm that the proposed solution works nicely with Unity IL2CPP with both a manually calculated size and Unsafe.SizeOf

var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), 8, false); // 8 = Size in bytes of the managed struct.
ComponentRegistry.Add(componentType);

var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), Unsafe.SizeOf<ManagedStruct>(), false); 
ComponentRegistry.Add(componentType);

mikhail-dcl avatar May 17 '23 17:05 mikhail-dcl

I can confirm that the proposed solution works nicely with Unity IL2CPP with both a manually calculated size and Unsafe.SizeOf

var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), 8, false); // 8 = Size in bytes of the managed struct.
ComponentRegistry.Add(componentType);

var componentType = new ComponentType(ComponentRegistry.Size - 1, typeof(ManagedStruct), Unsafe.SizeOf<ManagedStruct>(), false); 
ComponentRegistry.Add(componentType);

Great to see that it works! :) 33013b9484a88b114c0911466d2e7c38d35b2878 recently added automatic size detection for managed structs. That makes use of reflection during the component registration. Should also work on unity and will reduce the boilerplate a bit in the future ^^

genaray avatar May 17 '23 17:05 genaray

That's actually great so there will be no boilerplate.

IL2CPP though will not generate the generic version of Unsafe for structures if it was not mentioned explicitly anywhere in conjunction with Unsafe so

private static int SizeOf(Type type)
    {
        if (type.IsValueType)
        {
            return (int) typeof(Unsafe)
                .GetMethod(nameof(Unsafe.SizeOf))!
                .MakeGenericMethod(type)
                .Invoke(null, null)!;
        }

        return IntPtr.Size;
    }

may not work in Unity. It should not be critical as all usual paths I saw so far were via an explicitly mentioned generic Component

mikhail-dcl avatar May 18 '23 09:05 mikhail-dcl

That's actually great so there will be no boilerplate.

IL2CPP though will not generate the generic version of Unsafe for structures if it was not mentioned explicitly anywhere in conjunction with Unsafe so

private static int SizeOf(Type type)
    {
        if (type.IsValueType)
        {
            return (int) typeof(Unsafe)
                .GetMethod(nameof(Unsafe.SizeOf))!
                .MakeGenericMethod(type)
                .Invoke(null, null)!;
        }

        return IntPtr.Size;
    }

may not work in Unity. It should not be critical as all usual paths I saw so far were via an explicitly mentioned generic Component

I assume that's because Unitys IL2CPP strips out most reflection code, especially if not mentioned before... correct? Well that's actually a pitty, but quite understandable. Unity really has a advantage here, since their implement their own "Runtime" they have access to all type data needed without reflection.

For the future i could look into unity specific way. If i saw that correctly, unity has their own SizeOf API.

genaray avatar May 19 '23 12:05 genaray

You can read more about IL2CPP Code stripping here. Essentially, the most relevant part is the "Annotate roots using a Link XML file" section with generics examples. This is how the limitation can be circumvented.

When it comes to generics with value types it's not like they are stripped out, it's rather the AOT compiler does not generate them at all. For every value type, the size needed to allocate the type itself and the values on the stack (for methods invocations for instance) is different. Unlike the ref type when it can be always treated as an object and then casted to the required type (this is exactly how IL2CPP produces c++ code).

For example, Unsafe.SizeOf<T> returns a compile-time constant:

[NonVersionable]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>() => sizeof (T);

It's a perfect example of the necessity for the AOT compiler to know about the generic type ahead. If the code is not generated (and without JIT it can't be generated at runtime) the runtime has no idea about the size of the type

mikhail-dcl avatar May 20 '23 18:05 mikhail-dcl

When I have time I will create a Unity preprocessor to generate a link.xml with every Arch-related component. I don't know how I will find them through reflection at the moment though 😄 , so if you have any ideas you are very welcome to share 😉

mikhail-dcl avatar May 20 '23 18:05 mikhail-dcl

You can read more about IL2CPP Code stripping here. Essentially, the most relevant part is the "Annotate roots using a Link XML file" section with generics examples. This is how the limitation can be circumvented.

When it comes to generics with value types it's not like they are stripped out, it's rather the AOT compiler does not generate them at all. For every value type, the size needed to allocate the type itself and the values on the stack (for methods invocations for instance) is different. Unlike the ref type when it can be always treated as an object and then casted to the required type (this is exactly how IL2CPP produces c++ code).

For example, Unsafe.SizeOf<T> returns a compile-time constant:

[NonVersionable]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>() => sizeof (T);

It's a perfect example of the necessity for the AOT compiler to know about the generic type ahead. If the code is not generated (and without JIT it can't be generated at runtime) the runtime has no idea about the size of the type

Ah i see. However a link.xml or the [Preserve] attribute. Have you ever tried the [Preserve] attribute? If understood correctly it could also be used any might be a bit easier compared to the link.xml.

In the example with the Unsafe API, we could theoretically create a Unsafe.SizeOf wrapper, add that attribute and call it. Wouldnt that work?

Might be easier ^^ But haven't tried it yet.

genaray avatar May 21 '23 14:05 genaray

[Preserve] will not work with generics as it still does not hint the AOT compiler with the type arguments. It will not instruct it to generate the annotated generic for every possible value type

mikhail-dcl avatar May 22 '23 08:05 mikhail-dcl

@mikhail-dcl i've copied minimal struct of your project ( plugins folder with arch, and meta files, and copy dll of source generators and their meta files ) but faced exception (maybe problem with source generator)

using System.Runtime.CompilerServices; using Arch.Core; using Arch.System;

public partial class TestTagSystem : BaseSystem<World, float> { public TestTagSystem(World world) : base(world) { }

[Query]
[All(typeof(TestTag))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void TestUpdate(in Entity entity, ref TestTag testTag)
{
    testTag.IntValue += 3;
}

}

Arch.System.SourceGenerator\Arch.System.SourceGenerator.QueryGenerator\TestTagSystem.g.cs(10,34): error CS0115: 'TestTagSystem.Update(in float)': no suitable method found to override,

maybe you faced something like that?

Sorrowful-free avatar Jul 26 '23 12:07 Sorrowful-free

sorry guys i found the answer if my system class doesn't have namespace it will be appear

Sorrowful-free avatar Jul 26 '23 15:07 Sorrowful-free

I have developed Arch.Unity as a library to integrate Arch into Unity. This includes support for Conversion workflows (GameObject to Entity), fast queries integrated with the C# Job System, Hierarchy Windows and Inspectors that can track Entity, and a unique layer to integrate Arch.System with PlayerLoop.

I hope this helps Arch support Unity. (Sorry if this is not what I should be posting here...)

nuskey8 avatar Mar 02 '24 02:03 nuskey8