Welcome to Ivy

Ivy is a modular game engine for data driven Rust game development using Vulkan.

This guide will aim to familiarize the user with the workings of the Ivy game engine.

Compiling

The best method of incorporating Ivy into a project is either by [crates.io] or as a submodule.

In addition to registered crates Ivy depends on additional system level libraries.

For a successful compilation the following dependencies need to be met:

  • Vulkan Development Files
  • Windowing libraries (X11/Wayland/WINAPI)
  • Vulkan validation layers for debug builds

Linux

For compilation of glfw the following libraries need to be present:

  • libxi-dev
  • libxcursor-dev
  • libxinerama-dev
  • libxrandr-dev
  • libx11-dev

Fedora

sudo dnf install libXi-devel libXcursor-devel libXinerama-devel libXrandr-devel
libX11-devel mesa-vulkan-devel vulkan-validation-layers glslc

Debian

sudo apt install libxi-dev libxcursor-dev libxinerama-dev libXrandr-devel
libx11-dev libvulkan-dev

Fundamentals

This chapter will familiarize the reader with the fundamentals of the engine and what is needed to create a game and start your journey.

Layers

The core of the program is an application, which defines the update loop.

The update loop dispatches to several layers, which which are mostly self contained units of logic. The layers get exlusive access to the world, resources, and events and may execute any logic in on_update. They also get exclusive access to self which enables them to both read and modify their own state.

This is useful for games where the main game logic can be contained in one or more layers and store the state without interfering with other layers such as physics.

The layered design allows several high level concepts to work together in unison and separately and allows for logic to be added or removed.

An example would be a game which makes use of a client and server. The binaries can share most of the code, and the client and server can be separated into separate layers which allows the client to use all the same game logic as the server, and vice versa. The server and client layers can also be present at the same time which allows a self hosted client.

The on_update function takes three parameters:

The return type is of anyhow::Result and allows for any error to be propogated.

For a layer to be used it needs to be pushed into the App.

Example Usage

The layer is a trait which must define an on_update function

The following examples shows the basic usage of a layer, as well how to create an application using the layer.

#![allow(unused)]
fn main() {
{{#include ../../../examples/layer.rs}}
}

Events

To facilitate intra and interlayer communication a deferred bus like event system is provided.

Any user can subscribe to an event by using a channel. Every layer of type T will be broadcaasted to every subscribed sender. The event can thereafter be read by iterating the receiving half.

By default, std::mpsc::channel, flume, and crossbeam-channel (feature = "crossbeam-channel") implement the EventSender trait.

The events will not be cleared between different frames but rather consumed when iterated. This allows layers which execute occasionally to not miss any events.

Any 'static Send + Sync + Clone type can be used as an event. However, if cloning is expensive, consider wrapping the event in an Arc or referring to it by other means as the event will be cloned for every subscribed sender.

Example

#![allow(unused)]
fn main() {
fn events() {
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct MyEvent(String);
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct OtherEvent(String);

    // Normally, events will be supplied by App
    let mut events = Events::new();
    let my_events = events.subscribe::<MyEvent>();

    let other_events = events.subscribe::<OtherEvent>();

    // Send some events
    events.send(MyEvent(String::from("Hello, World!")));

    // Receive events
    for event in my_events.try_iter() {
        let other = OtherEvent(event.0.to_uppercase());
        events.send(other)
    }

    assert!(other_events
        .try_iter()
        .map(|val| val.0)
}

Intercepting

Sometimes it is necessary to intercept events, either absorbing them or re-emitting them. This can be accomplished in two main ways.

Events of a certain type can be sent and consumed, to then be resent using a different type. The final consumers should then subscribe to the latter type.

Sometimes however, it is not possible to re-emit events; either because of already existing architecture, or that the intercepting component may not always be present and thus requiring a mockup intercepter that simply re-emits.

For these use cases, the use of the intercept API is necessary.

#![allow(unused)]
fn main() {
fn intercept_events() {
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct MyEvent(String);

    // Normally, events will be supplied by App
    let mut events = Events::new();
    let (tx, my_events) = flume::unbounded();
    events.intercept::<MyEvent, _>(tx).unwrap();

    let other_events = events.subscribe::<MyEvent>();

    // Send some events
    events.send(MyEvent(String::from("Hello, World!")));

    // Receive events
    for event in my_events.try_iter() {
        let other = MyEvent(event.0.to_uppercase());
        events.intercepted_send(other)
    }

    assert!(other_events
        .try_iter()
        .map(|val| val.0)
}

Resources

Entity Component System

The logic of the library is centered around the Entity Component System design pattern.

In short, the ECS pattern describes entities as a collection of components. The principle is tightly coupled with data driven development.

Ivy makes use of hecs for the ecs, with the extension libraries hecs-hierarchy for declaring relationships between entities, and hecs-schedule for system execution abstractions, borrowing, and automatic multithreading.

Behaviors are controlled by the components attached to an entity.

Systems declared in schedules then query and operate on the entities and can modify state.

Bundles

Many of the built in systems require a certain set of components to be present in order to avoid many Option in query and branches.

For example, rendering requires Position, Rotation, Scale, Mesh, Visible, Color, Pass. Remembering to add all these when spawning entities is a chore, and makes it easy to forget some and not having the entity show up.

To fix this several bundles are provided, which has the added benefit of providing sane defaults.

The following snippet shows how to create a new entity which will be moving in the world with an initial velocity.

Note: The entity won't be rendered, since no rendergraph has been setup, and the entity does not have the ObjectBundle bundle. It is recommended to use a custom layer and creating the entities in new or a setup function. However, the raw usage of App is used for brevity.

#![allow(unused)]
fn main() {
fn bundles() {
    let mut app = App::builder().build();

    let world = app.world_mut();

    let entity = Entity::builder()
        .mount(RbBundle {
            vel: vec3(1.0, 0.0, 0.0),
            mass: 5.0,
            ang_mass: 2.0,
            ..Default::default()
        })
        .set(name(), "My Entity".into())
        .spawn(world);

    // Get the `Resitution` component
    assert_eq!(*world.get(entity, restitution()).unwrap(), 0.0,);
}
}

Bundles

The following bundles are provided:

  • ObjectBundle - Renderable objects with position and mesh
  • RbBundle - Rigidbody obejct
  • RbColliderBundle - Rigidbody object with a collider
  • WidgetBundle - Base UI element, similar to html div
  • TextBundle - UI text element
  • ImageBundle - UI image element
  • ConnectionBundle - Declare physical relationships between entities
  • TransformBundle - Position an object with Position, Rotation, and Scale. A matching TransformQuery and .into_matrix() are provided as well.
  • ConstraintBundle - UI constraints bundle, part of WidgetBundle

Rendering and Passes

Shaderpass

Compared to other game engines, Ivy uses a slightly more complicated, though more flexible approach to rendering.

All rendereable entities, hereby referred to as objects have an associated shaderpass. A shaderpass holds a shader and describes at which point it will be rendered in the rendering pipelines.

Each node in the rendering shaderpass has an associated type of shaderpass which it will render. For example, the ImageRenderer will usually be set up to render objects which have a Handle<ImagePass>. The ImagePass, wrapped in an opaque resource handle, will describe the vulkan pipeline and layout used.

Note: The renderer usually expect the different shaderpasses to conform to a single pipeline layout due to descriptor binding.

Objects with meshes usually have a GeometryPass attached to them, with the mesh and/or material describing the specific properties like texture and roughness.

Different objects which belong to the same shaderpass can have different values, I.e; different shaders, which for example can be used for wind affected foliage to be rendered along other objects in the same shaderpass, but different shaders.

The system also allows for multiple shaderpasses to be attached to the same entity, allowing the entity to use different shaders for different shaderpasses. This high customizability allows the same entity to use a textured albedo shader for GeometryPass, and a solid color for a hypothetical MinimapPass. This can be very useful in games where the same object may be required to be rendered multiple times from different viewpoints.

A ShaderPass is a type which wraps a Pipeline and a PipelineLayout, though they can contain other info. The Rust type system is used for differentiating between different kinds of passes.

For reducing boilerplate a convenience macro new_shaderpass is provided for easily creating one or more stronly typed shaderpass types.

Example:

#![allow(unused)]
fn main() {
use ivy::new_shaderpass;


new_shaderpass! {
  pub struct MinimapPass;
  pub struct SolidPass;
}
}

In many cases though, the usage of the included GeometryPass, ImagePass, TextPass, and different post processing passes are enough.

Rendergraph

The rendering graph describes an acyclic graph of rendering nodes, which describe how the scene will be rendered. Each node describes its inputs and outputs, and dependencies will automatically be generated to ensure proper ordering and syncronization with paralellization using Vulkan.

ivy-presets contain common rendergraph setups, such as for PBR rendering. It is also possible to create your own rendergraph to tailor the rendering for your game or application.

The following example shows the raw, unaided setup of a rendergraph rendering a simple unlit model to the screen.

#![allow(unused)]
fn main() {
{{ #include ../../../examples/rendergraph.rs }}
}