Getting Started with Blob Asset

Blob asset is a concept used in Unity’s DOTS. Basically, it’s an asset that’s stored in unmanaged memory that can be accessed inside components. The term “asset” here does not mean asset in the traditional sense like a prefab, texture, 3D model, audio clip, etc. The term is used to mean data. It could be as simple as an integer to complex like an array of structs, or a lookup table in the form of NativeHashMap. Only non nullable types are allowed, so no reference types like classes and delegates. A blob asset is suggested to be used to only hold immutable data… for now.

One obvious usage to this is to store a database of static values, a reference of which can be added inside a component. (You can’t store vanilla collections inside components. You can have collections but they’re different from native collections and have limitations.)

Basic Usage

Let’s do a basic usage of a blob asset. Let’s make a blob asset that stores a single integer value and let’s access it in components via Entites.ForEach(). Here’s code of the component that has the reference and the system that prepares the blob asset and the entities:

private struct TestComponent : IComponentData {
    public BlobAssetReference<int> reference;
}

private class BlobAssetSystem : SystemBase {
    private const int DATA_VALUE = 111;
    
    private BlobAssetReference<int> reference;
    
    protected override void OnCreate() {
        base.OnCreate();
        
        // Prepare the blob asset
        BlobBuilder builder = new BlobBuilder(Allocator.TempJob);
        ref int data = ref builder.ConstructRoot<int>();
        data = DATA_VALUE;
        this.reference = builder.CreateBlobAssetReference<int>(Allocator.Persistent);
        builder.Dispose();

        Entity entity = this.EntityManager.CreateEntity(typeof(TestComponent));
        this.EntityManager.SetComponentData(entity, new TestComponent() {
            reference = this.reference
        });
    }

    protected override void OnDestroy() {
        this.reference.Dispose();
    }

    protected override void OnUpdate() {
        // Used non burst here so that Assert will work
        // Rest assured that ScheduleParallel() works here when Assert.IsTrue()
        // is removed
        this.Entities.ForEach(delegate(in TestComponent component) {
            Debug.Log($"value: {component.reference.Value}");
            Assert.IsTrue(component.reference.Value == DATA_VALUE);
        }).WithoutBurst().Run();
    }

    public void CreateNewEntity() {
        Entity entity = this.EntityManager.CreateEntity(typeof(TestComponent));
        this.EntityManager.SetComponentData(entity, new TestComponent() {
            reference = this.reference
        });
    }
}

Creating a blob asset really only means having BlobAssetReference that will point to such asset or data. To create one, we create a BlobBuilder. Use ConstructRoot() and get a variable by reference. Store the value into the variable and call CreateBlobAssetReference() which already returns the BlobAssetReference.

In BlobAssetSystem.OnCreate(), we created an entity that adds a test component which uses the BlobAssetReference that we created. In OnUpdate(), we display the value from the BlobAssetReference and also assert that it is still equal to the value that we have set to it (DATA_VALUE).

I added a method CreateNewEntity() so we can see that a BlobAssetReference can be used by multiple entities. Here’s the test code:

[Test]
public void BasicUsageTest() {
    BlobAssetSystem system = this.World.GetOrCreateSystem<BlobAssetSystem>();
    system.Update();
    system.CreateNewEntity();
    system.Update();
}

This test passes and displays:

There are 3 debug logs as an entity is already created when the system is created. On call of the first Update(), it displays the first row. Then we call CreateNewEntity() which creates a new similar entity. Now there are two. On the second call of Update(), it displays the last two rows of Debug.Log().

Blob asset to a lookup table

Now let’s try a semi real game usage. Say we have a database of weapon stats. A single weapon data entry looks like this:

public readonly struct WeaponData {
    public readonly int damage;
    public readonly float projectileSpeed;

    public WeaponData(int damage, float projectileSpeed) {
        this.damage = damage;
        this.projectileSpeed = projectileSpeed;
    }
}

A collection of WeaponData would be stored in a NativeHashMap<FixedString64, WeaponData> where the FixedString64 is the ID of the weapon. In other words, it’s a lookup table of WeaponData. We can create a blob asset of this hash map and use it in component. Say the component looks like this:

public readonly struct Weapon : IComponentData {
    public readonly FixedString64 weaponId;
    private readonly BlobAssetReference<NativeHashMap<FixedString64, WeaponData>> weaponMapReference;

    public Weapon(FixedString64 weaponId, BlobAssetReference<NativeHashMap<FixedString64, WeaponData>> weaponMapReference) {
        this.weaponId = weaponId;
        this.weaponMapReference = weaponMapReference;
    }

    public int Damage {
        get {
            return this.weaponMapReference.Value[this.weaponId].damage;
        }
    }
}

This is a very contrived example, but bare with me. We have this component that has a weaponId that is used as key to lookup the WeaponData using the BlobAssetReference. For example, the property Damage derives its value from the lookup table via using the weaponMapReference.

Here’s a system that puts this all together:

private class WeaponSystem : SystemBase {
    private NativeHashMap<FixedString64, WeaponData> weaponMap;
    private BlobAssetReference<NativeHashMap<FixedString64, WeaponData>> weaponMapReference;

    protected override void OnCreate() {
        // Populate weapon map
        // You can imagine that the data here may come from a ScriptableObject,
        // or JSON or XML or from another server
        this.weaponMap = new NativeHashMap<FixedString64, WeaponData>(2, Allocator.Persistent);
        this.weaponMap["Bow"] = new WeaponData(1, 5.0f);
        this.weaponMap["Gun"] = new WeaponData(2, 100.0f);
    
        // Prepare BlobAssetReference
        BlobBuilder builder = new BlobBuilder(Allocator.Persistent);
        ref NativeHashMap<FixedString64, WeaponData> blobData =
            ref builder.ConstructRoot<NativeHashMap<FixedString64, WeaponData>>();
        blobData = this.weaponMap;
        this.weaponMapReference = builder.CreateBlobAssetReference<NativeHashMap<FixedString64, WeaponData>>(Allocator.TempJob);
        builder.Dispose();
        
        // Create entities
        Entity bowEntity = this.EntityManager.CreateEntity(typeof(Weapon));
        this.EntityManager.SetComponentData(bowEntity, new Weapon("Bow", this.weaponMapReference));

        Entity gunEntity = this.EntityManager.CreateEntity(typeof(Weapon));
        this.EntityManager.SetComponentData(gunEntity, new Weapon("Gun", this.weaponMapReference));
    }

    protected override void OnUpdate() {
        // This is needed so it can be used inside the ForEach()
        NativeHashMap<FixedString64, WeaponData> localMap = this.weaponMap;
        
        this.Entities.ForEach(delegate(in Weapon weapon) {
            Debug.Log($"{weapon.weaponId}: {weapon.Damage}");
            Assert.IsTrue(localMap[weapon.weaponId].damage == weapon.Damage);
        }).WithoutBurst().Run();
    }

    protected override void OnDestroy() {
        this.weaponMapReference.Dispose();
        this.weaponMap.Dispose();
    }
}

In OnCreate(), we populate weaponMap which will be our lookup table. We then create a BlobAssetReference for such map. After that, we create two entities with Weapon component, one for Bow and one for Gun.

In OnUpdate(), we display the damage of each weapon and compare them to the original NativeHashMap to verify that they’re still the same.

If we run this test:

[Test]
public void NativeCollectionInSystemUsage() {
    WeaponSystem weaponSystem = this.World.GetOrCreateSystem<WeaponSystem>();
    weaponSystem.Update();
}

it will pass and will display each entity’s weapon damage:

Utility Method

While writing this post, I realized I could make a utility method out of this.

public static class BlobAssetUtils {
    public static BlobAssetReference<T> CreateReference<T>(T value, Allocator allocator) where T : struct {
        BlobBuilder builder = new BlobBuilder(Allocator.TempJob);
        ref T data = ref builder.ConstructRoot<T>();
        data = value;
        BlobAssetReference<T> reference = builder.CreateBlobAssetReference<T>(allocator);
        builder.Dispose();

        return reference;
    }
}

The blob asset creation part of the previous systems can then be simplified into:

// Basic usage system
this.reference = BlobAssetUtils.CreateReference(DATA_VALUE, Allocator.Persistent);

// Weapon example system
this.weaponMapReference = BlobAssetUtils.CreateReference(this.weaponMap, Allocator.Persistent);

That’s all I have for now. Until next time!

Like my posts? Subscribe to my mailing list!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s