BlobAsset with tree structure of arbitrary data

I have a global tree structure I’d like to access in my system. The tree structure is generated once and stays static during the game. It can become pretty deep, and the data that sits at its leaves is the same interface but different types (the tree is a kd-tree and the data are colliders of different types).

I figured that storing the tree as a BlobAssetReference is the ECS way to go, but obviously that needs a struct of deterministic size, which for my different collider data types is not the case.

So I’m exploring how to store this in the most efficient way. One possibility would be to save just int references for the colliders, making the tree size deterministic, and use some kind of manager to retrieve the actual data via the id?

Are there any best practices? I’ve looked at the DOTS physics source, and the collider code is one mayhem of unsafe code and switch statements, so I’m hoping that there is a simpler solution?

The struct is simply the root of the blob. Blobs are actually tree-like in nature, and you can use BlobPtr and BlobArray to extend your tree. The last part of this talk breaks it down:

Also there’s an example here: https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/ECSSamples/Assets/Advanced/BlobAsset

1 Like

Apologies if I wasn’t clear enough, my problem is not how to create a BlobAsset in general, it’s that the type of the data is non-deterministic. Basically, the struct of my BlobAsset would look like this:

public struct QuadTree
{
    public BlobArray<QuadTree> Children;
    public BlobArray<ICollider> Colliders; // can't do that!
    public bool IsLeaf;
}

Which doesn’t work, because ICollider could be anything. What I could do is add a BlobArray member for every known implementation of ICollider (they are all structs), but that gets immensely complex. Hence my question if there are simpler solutions.

A BlobArray of BlobPtr. You can cast a BlobPtr to a base type struct that just has your header field for determining the type.

The main value of blobs at runtime is so they can participate in ECS, ie you can stick them on components. But your own spatial structure I would think you would just pass into a job directly. In which case I see no real value in using blobs.

blob of colliders could be like this:

struct QuadTreeColliders
{
    public BlobArray<int> ColliderTypes;
    public BlobArray<int> ColliderPointerOffsets;
    public BlobArray<byte> ColliderDataBlob;
}

And you can create it by steps:

  1. create an array of BlobAssetReference which store all your collider data.
  2. create a blob of QuadTreeColliders
  3. iterate blob array of collider data created at 1 and fill it into QuadTreeColliders

The downside of this approach:

  1. for any type of collider there must be a correspond int id of type.
  2. it is a little bit tricky to call method of interface like ICollider
  3. collider must be able to serialize into format of BlobAsset

take a look at those link for code example:
Blob Structure
Serialize Tree into Blob Structure
Call Interface Method 1
[Call Interface Method 2](http:// return UnsafeUtilityEx.AsRef((void*)ptr).Tick(index, blob, bb):wink:

Thanks guys, appreciate the feedback.

So in my system’s OnCreate() I would create the tree, save it as a member, and in OnUpdate() I’ll access it directly in my Entities.ForEach? Wouldn’t Burst complain about accessing a member?

@quabug I was wondering about simpler, not more complicated :slight_smile: Thanks for the links though, it’s always interesting to read how internals work in order to understand the big picture.

I’ve looked a bit more at the DOTS physics sources, and what they are doing is something similar although they use BlobAssetReferences:

var collider = default(BoxCollider);
collider.Init(geometry, filter, material);
return BlobAssetReference<Collider>.Create(&collider, sizeof(BoxCollider));

Their Collider then casts the struct to the corresponding type before executing methods, based on the common header.

However, I couldn’t get this to work with BlobPtrs. What I’ve tried:

var collider = default(LineCollider);
collider.Init(p1, p2, zLow, zHigh);
return (BlobPtr<Collider>)new BlobPtr<LineCollider> {Value = collider};

Which gives a cast compiler error. @DreamingImLatios do you have a code example of how to cast the struct to my base struct?

@quabug I didn’t realize that the code you linked are from your behavior tree implementation! I’ll have a more detailed look in case I can’t manage to get the struct casts working.

Actually what they are doing is constructing the BlobArrays of ConvexHull manually instead of using the BlobBuilder API. Although to your specific issue, you can use UnsafeUtility.As or UnsafeUtitlityEx.As.

1 Like

Thanks, so now I can create a BlobPtr which is actually a BlobPtr. Now I’ll have to allocate my
BlobArray<BlobPtr>, right? BlobBuilder.Allocate() takes in either a blob array (which allocates a fixed size per element), or a BlobPtr. But if I pass each BlobPtr of my array individually to the latter, it will still allocate the type of the element, which is the base type Collider (that only contains the header), right? How would I allocate different sizes for each element? Skip BlobBuilder altogether and roll my own?

So first you need to allocate the array. Then you need to index each element within the array (elements are ref returned) and then do an UnsafeUtility.As to convert the BlobPtr to BlobPtr. Then you can call Allocate on the new BlobPtr.

1 Like

Thanks a lot @DreamingImLatios , I finally figured it out.

If anyone is interested in the code, here is the tree generation, which calls the base Collider struct which calls the LineCollider which gets it allocated properly. And here is the test.

3 Likes

So I’m actually testing this, and I’m getting weird behavior. Check this below: ptr.Value.Type = Flipper, while collider.Type = Circle. But collider = ptr.Value!

Code in question is here. Also the AABBs I’ve set have different values than the ones I’m getting here in the Job. What’s going on?

That is weird. Is this the managed debugger or native debugger?

Uh, good question. I’m attaching Rider to Unity. I’ve changed Entities.For to WithoutBurst() and .Schedule() to .Run(), so I suppose it should be managed in any case?

How would you debug natively?

If it is broken in managed I wouldn’t expect it to work in native. I can’t say the converse is true because native debugging is still new and somewhat experimental. That was the main reason I asked. There was a video that got uploaded a couple weeks back that described the process for native debugging in Unity with the latest Burst. I don’t remember where.

I’m still not sure why you are getting a default collider after the ref assignment. It might be worth looking at the hex memory view at the root of the blob as well as the stack and see if you can’t track a missing address or something.

Managed I guess, because He is of Rider if I’m not wrong.
Edit: Ah not refreshed page, already answered :slight_smile:

https://discussions.unity.com/t/779272/2

2 Likes

I’ve written a test to reproduce. @DreamingImLatios or @eizenhorn if you have a minute to copy+paste+run this, that would be wonderful. Or if someone has a clue just by looking at it, that would be awesome.

So we’re not even talking tree structure here, it’s just a flat array of structs that can be of different types. Basically what I’m getting out of the blob are just default values instead of the values set with the blob builder.

EDIT: I’m an idiot. Obviously I forgot to ref the collider in the Init(). Sorry for the noise, looks like I’m good now, applying this to the original code and test again. sigh

1 Like

I’ve certainly been guilty of similar bugs. Happens to the best of us. Glad you got it working!:smile:

Okay, hopefully my last question on that topic. So I have my parent Collider struct, and a let’s say PointCollider with the same header but additional data.

So in my Collider API, I have something like that:

public unsafe float HitTest(in BallData ball, float dTime, ref CollisionEventData coll)
{
    fixed (Collider* collider = &this) {
        switch (collider->Type) {
            case ColliderType.Point:
                return ((PointCollider*)collider)->HitTest(in ball, dTime, ref coll);
            case ColliderType.Plane:
                return ((PlaneCollider*)collider)->HitTest(in ball, dTime, ref coll);
            // other colliders
            default: return -1;
        }
    }
}

That allows me to do collider.HitTest() on whichever collider happens to be in the struct.

But that doesn’t seem to work. Basically, my additional data is default or random. However, the reason why I thought this was working is that in my unit test, I directly cast the created struct to Collider*, instead of this. So when I make the method above static and pass in the collider and cast the collider instead of this, it works.

So this seems to be treated differently than a reference to a struct? I got the idea of casting this from the DOTS physics collider code, and I don’t think they do anything significantly different in that aspect. Another gotcha I fell into?