ME.BECS (Bursted Entity Component System)

repository·main·Indexed 42 days ago

https://github.com/chromealex/me.becs

A high-performance, deterministic ECS framework for Unity, optimized for Burst compilation and zero GC allocations. Designed for scenarios requiring fast cloning and serialization, such as networking and rollbacks. Version 2025.11.27 includes a command system for unit management and a high-performance K-Nearest Neighbour (KNN) library using K-D trees for spatial queries.

Tokens
8K
Snippets
30
Records
31
Agent score
53%

What's inside ME.BECS

  1. Use the KNN library for K-Nearest Neighbour queries

    main

    The com.akb.knn library provides a high-performance K-Nearest Neighbour (KNN) implementation for Unity using the DOTS technology stack (Burst compiler and Unity Job System). It utilizes K-D trees to accelerate spatial queries and is designed to be allocation-free and multi-threaded.

    Core Workflow

    1. Prepare Data: Create a NativeArray<float3> containing your point cloud.
    2. Initialize Container: Create a KnnContainer using your points. You can choose whether to build the tree immediately via the second constructor argument.
    3. Rebuild (if needed): If the point cloud changes, use KnnRebuildJob to update the container.
    4. Query: Use specific jobs to perform queries (single point, batch points, or range-based queries) to leverage Burst acceleration and multi-threading.
    // Example setup
    var points = new NativeArray<float3>(100000, Allocator.Persistent);
    // ... fill points ...
    
    // Create container (false = don't build immediately)
    var knnContainer = new KnnContainer(points, false, Allocator.TempJob);
    
    // Rebuild the tree
    var rebuildJob = new KnnRebuildJob(knnContainer);
    rebuildJob.Schedule().Complete();
  2. Initialize a ME.BECS project

    main

    To set up a new ME.BECS project in Unity, follow these steps:

    1. Create a csc.rsp file: In your Assets directory, create a file named csc.rsp and add the following preprocessor defines to enable exception handling and context:
    -define:EXCEPTIONS_CONTEXT
    -define:EXCEPTIONS_THREAD_SAFE
    -define:EXCEPTIONS_COLLECTIONS
    -define:EXCEPTIONS_COMMAND_BUFFER
    -define:EXCEPTIONS_ENTITIES
    -define:EXCEPTIONS_QUERY_BUILDER
    -define:EXCEPTIONS_INTERNAL
    -define:EXCEPTIONS_ASPECTS
    -define:EXCEPTIONS
    1. Optional Debugging Defines: You can add these to csc.rsp for additional safety and performance testing:

      • -define:ENABLE_BECS_COLLECTIONS_CHECKS
      • -define:LEAK_DETECTION
      • -define:MEMORY_ALLOCATOR_BOUNDS_CHECK
      • -define:ENABLE_BECS_FLAT_QUERIES
    2. Create Default Project: Use the Unity menu Create/ME.BECS/Create Project to generate the default project structure.

  3. Perform a single K-Nearest Neighbour query

    main

    To perform a single query for the $k$ nearest neighbours of a specific position using Burst acceleration, use the QueryKNearestJob.

    Note: While knnContainer.QueryKNearest exists for basic usage, it is not Burst accelerated. For performance-critical applications, always use the job-based approach.

    const int kNeighbours = 10;
    float3 queryPosition = float3.zero;
    var result = new NativeArray<int>(kNeighbours, Allocator.TempJob);
    
    // Use the job for Burst acceleration
    var queryJob = new QueryKNearestJob(knnContainer, queryPosition, result);
    queryJob.Schedule().Complete();
    
    // 'result' now contains indices into the original points array
  4. Use Aspects to group components

    main

    Aspects allow you to group multiple components into a single interface for easier access, often used in queries. Implement IAspect and define properties that expose components using RefRW<T> (Read/Write) or RefRO<T> (Read Only). Use the [QueryWith] attribute to specify which components are required for the aspect's query.

    public struct MyAspect : IAspect {
        public Ent ent { get; set; }
    
        [QueryWith]
        private RefRW<MyComponent1> component1Data;
        private RefRW<MyComponent3> component2Data;
        
        public ref MyComponent1 component1 => ref this.component1Data.Get(this.ent.id);
        public ref MyComponent2 component2 => ref this.component2Data.Get(this.ent.id);
    }
    
    // Usage:
    var aspect = ent.GetOrCreateAspect<MyAspect>();
    aspect.component1.data = 123;
  5. Define and create components

    main

    Components in ME.BECS must implement the IComponent interface. They can contain unmanaged data or ObjectReference<T> for persistent UnityEngine.Object references. You can optionally provide a Default static property to initialize components with specific data and use [EditorComment] for documentation in the Components Viewer.

    [EditorComment("My component help description")]
    [ComponentGroup(typeof(MyComponentGroup))]
    public struct Component : IComponent {
        public static Component Default => new Component() { data = 100 };
        public int data;
        public ObjectReference<UnityEngine.Mesh> unityObjectReference;
    }
  6. Clone, Copy, and Serialize Worlds

    main

    ME.BECS supports efficient world state management for networking, rollbacks, and saving:

    • Clone: Creates a completely new world instance from an existing one.
    • Copy: Copies the state from a source world into the current world.
    • Serialize: Converts the world state into a byte array.
    • Deserialize: Creates a new world from a byte array.
    // Clone
    var newWorld = world.Clone();
    
    // Copy
    world.CopyFrom(sourceWorld);
    
    // Serialize
    var bytes = world.Serialize();
    
    // Deserialize
    var world = World.Create(bytes);
  7. Run parallel queries with Jobs

    main

    ME.BECS provides specialized job interfaces for high-performance parallel processing of entities and components. You can schedule these from a system using API.Query(world, dependsOn).

    • Aspect Parallel Query: Use IJobForAspects<TAspect>.
    • Component Parallel Query: Use IJobForComponents<T1, T2, ...>.
    • Mixed Parallel Query: Use IJobFor1Aspects2Components<TAspect, T1, T2>.
    [BurstCompile]
    private struct MyJob : IJobForAspects<MyAspect> {
        public void Execute(in JobInfo jobInfo, in Ent ent, ref MyAspect aspect) {
            // Logic
        }
    }
    
    // In a system:
    var query = API.Query(world, dependsOn).AsParallel().Schedule<MyJob, MyAspect>(new MyJob());
    context.SetDependency(query);
  8. Perform range-based queries

    main

    Range queries find all points within a specific radius of a position. The library provides two ways to do this:

    Single Range Query

    Use QueryRangeJob to find points within a radius and store them in a NativeList<int>.

    Batch Range Queries

    Use QueryRangeBatchJob for multiple points. Because of allocation limitations within jobs, you must pre-allocate a NativeArray<RangeQueryResult> where each element specifies a maximum number of allowed neighbours.

    Note: You must manually Dispose() each RangeQueryResult and the parent arrays to avoid memory leaks.

    // Batch Range Query Example
    var queryPositions = new NativeArray<float3>(queryPoints, Allocator.TempJob);
    var rangeResults = new NativeArray<RangeQueryResult>(queryPoints, Allocator.TempJob);
    
    // Pre-allocate max neighbours (e.g., 128) for each query result
    for (int i = 0; i < rangeResults.Length; ++i) {
        rangeResults[i] = new RangeQueryResult(128, Allocator.TempJob);
    }
    
    var batchRange = new QueryRangeBatchJob(knnContainer, queryPositions, 2.0f, rangeResults);
    batchRange.ScheduleBatch(queryPoints, queryPoints / 32).Complete();
    
    // Cleanup
    foreach (var r in rangeResults) {
        r.Dispose();
    }
    rangeResults.Dispose();
  9. Implement ECS systems

    main

    Systems are implemented by defining a struct that implements one of the lifecycle interfaces. You can use [BurstCompile] to optimize the system's lifecycle methods or [WithoutBurst] to exclude specific methods from Burst compilation.

    Supported lifecycle interfaces:

    • IAwake: OnAwake(ref SystemContext context)
    • IStart: OnStart(ref SystemContext context)
    • IUpdate: OnUpdate(ref SystemContext context)
    • IDestroy: OnDestroy(ref SystemContext context)
    • IDrawGizmos: OnDrawGizmos(ref SystemContext context)
    [BurstCompile]
    public struct TestSystem : IUpdate {
        [WithoutBurst]
        public void OnUpdate(ref SystemContext context) {
            // System logic here
        }
    }
  10. Perform batch K-Nearest Neighbour queries

    main

    To query multiple points simultaneously across multiple threads, use the QueryKNearestBatchJob. This is highly efficient for large datasets.

    Requirements:

    • A NativeArray<float3> containing all query positions.
    • A NativeArray<int> for results, sized as queryPoints.Length * kNeighbours.
    const int kNeighbours = 10;
    const int queryPoints = 100000;
    var results = new NativeArray<int>(queryPoints * kNeighbours, Allocator.TempJob);
    var queryPositions = new NativeArray<float3>(queryPoints, Allocator.TempJob);
    
    // ... fill queryPositions ...
    
    var batchQueryJob = new QueryKNearestBatchJob(knnContainer, queryPositions, results);
    
    // Schedule with batch size (e.g., queryPoints / 32)
    batchQueryJob.ScheduleBatch(queryPositions.Length, queryPoints / 32).Complete();
  11. Access component data on an entity

    main

    Use the following methods on an Ent instance to manage component data:

    MethodDescription
    ent.Set(new Component() { ... })Sets component data
    ref var comp = ref ent.Get<Component>()Gets data; creates component if it doesn't exist
    ref readonly var comp = ref ent.Read<Component>()Reads data; returns empty data if it doesn't exist
    ent.Remove<Component>()Removes the component (returns true if removed)
    bool has = ent.Has<Component>()Checks if component exists
    bool has = ent.HasStatic<Component>()Checks if static component exists (from EntityConfig)
    var comp = ent.ReadStatic<Component>()Reads static data
    ent.RemoveShared<Component>([hash])Removes shared component (returns true if removed)
    ent.SetShared(new Component())Sets shared component
    bool has = ent.HasShared<Component>()Checks if shared component exists
    ref readonly var comp = ref ent.ReadShared<Component>([hash])Reads shared component
    ref var comp = ref ent.GetShared<Component>([hash])Gets shared component