shipyard

repository·master·Indexed 21 days ago

https://github.com/leudz/shipyard

A high-performance Entity Component System (ECS) for Rust focused on usability and speed. It utilizes a SparseSet-based data structure and grouping model inspired by EnTT to enable highly-parallel software development through workloads, parallel iterators, and flexible component tracking.

Tokens
69.9K
Snippets
224
Records
327
Agent score
72%

What's inside shipyard

  1. What is a Workload and how to use it

    master

    A Workload is a collection of systems. Workloads are automatically run across multiple threads, which can improve performance.

    While you can have a single large Workload for small projects, it is common practice to organize systems into smaller, modular Workloads that are then composed into larger ones. You can compose systems into a workload using the .into_workload() method on a tuple of systems.

    fn main_loop() -> Workload {
        ( 
            move_player, 
            move_friends, 
            collision, 
            render 
        ).into_workload()
    }
  2. Iterating over multiple components in Shipyard

    master

    When you iterate over multiple components (multiple sparse sets), Shipyard optimizes the process by:

    1. Identifying the shortest dense array among the requested component sets.
    2. Iterating over the dense array of that shortest set.
    3. For each EntityId in the shortest set, checking if that ID exists in all other requested sparse sets using the $O(1)$ lookup (dense[sparse[id]] == id).
    4. Yielding only the entities that satisfy all component requirements.

    This approach ensures that the iteration complexity is bounded by the number of entities possessing the rarest component in the query.

  3. Define components and unique components

    master

    To use types within a World, you must mark them with specific derive macros:

    • Use #[derive(Component)] for data that belongs to multiple entities. Entities are identified by an EntityId and can be composed of multiple components.
    • Use #[derive(Unique)] for data that exists only once in the World (e.g., a single Player instance).

    Note: This guide is based on shipyard v0.10. While the core concepts of Component and Unique are fundamental to shipyard, verify compatibility with your current version.

    #[derive(Component)]
    struct Friend(Square);
    
    #[derive(Unique)]
    struct Player {
        square: Square,
    }
  4. Define and run Systems

    master

    A System in Shipyard is a way to organize code, typically implemented as a function that takes views (queries) as arguments. You can also use closures as systems. To execute a system, you pass it to the execution runner (e.g., via a workload or direct execution call).

    // Example of a system function
    fn create_ints(mut query: ViewMut<(i32,)>) {
        // logic here
    }
    
    // Running a system
    world.run(create_ints);
  5. How to use !Send and !Sync components in Shipyard

    master

    By enabling the thread_local feature, World can store components that do not implement the Send or Sync traits. However, these components have strict access restrictions based on their trait implementations:

    Component TraitsAccess Rules
    !SendCan only be added to the World from the thread that owns it.
    Send + !SyncCan only be accessed from one thread at a time.
    !Send + SyncCan be accessed immutably from other threads.
    !Send + !SyncCan only be accessed in the specific thread they were added in.

    To interact with these components, you must use the specialized borrow types: NonSend, NonSync, or NonSendSync.

  6. What are Custom Views and how to categorize them

    master

    Custom views are types that you can borrow (similar to View or UniqueView) but are not provided by shipyard out of the box. They allow you to group related data or perform setup/teardown logic automatically when a system runs.

    Custom views fall into two categories:

    1. View Bundles: These contain only other views (e.g., a struct containing multiple ViewMut or UniqueView fields).
    2. Wild Views: These can contain any other types (e.g., a struct containing a raw u64 or a wgpu::CommandEncoder).
  7. Organize execution with Workloads

    master

    A Workload is a collection of systems grouped together. Workloads are stored in the World and can be executed repeatedly.

    When a workload runs, it executes its constituent systems in the order they were added (first to last) and attempts to run them in parallel whenever possible (a concept known as outer-parallelism).

    // Example of creating a workload
    let mut workload = Workload::new()
        .add_system(system_a)
        .add_system(system_b);
    
    world.add_workload(workload);
  8. Analyze System and Component relationships in the Visualizer

    master

    The Visualizer's first panel allows you to inspect the relationship between systems and components to identify potential bottlenecks or forced sequential access.

    • Systems: Clicking on a system highlights the components it borrows.
      • Red highlights: Indicate exclusive access.
      • Blue highlights: Indicate shared access.
    • Components: Clicking on a component highlights all systems that borrow it.

    Use Case: Use these features to identify if a specific component (e.g., AllStorages) is preventing parallelism. If a system borrows a large component exclusively, you might improve performance by having it borrow smaller, individual components instead.

  9. Understand Custom Views

    master

    Custom views are types that you can borrow (similar to View or UniqueView) but are not provided by shipyard out of the box. They allow you to group related data or perform setup/teardown logic automatically when a system requests them.

    Custom views fall into two categories:

    1. View Bundles: These contain only other views (e.g., a struct containing multiple ViewMut or EntitiesViewMut fields).
    2. Wild Views: These can contain any other types (e.g., a struct containing a primitive like u64 or a third-party type like wgpu::TextureView).
  10. Use Uniques (Resources) for single-instance components

    master

    Uniques (also known as resources) are used for components that only ever have a single instance in the World. Unlike standard components, Uniques are not attached to specific entities, making them ideal for global data like cameras, timers, or configuration settings.

    To use a Unique, you must first initialize it using add_unique. Once added, you can access it within a world.run system using UniqueView<T> for read-only access or UniqueViewMut<T> for mutable access.

    let world = World::new();
    
    // Initialize the unique resource
    world.add_unique(Camera::new()).unwrap();
    
    // Access the unique resource in a system
    world
        .run(|camera: UniqueView<Camera>| {
            // Use camera here
        })
        .unwrap();