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() { {{ #include ../../../tests/events.rs:4:27 }} }
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() { {{ #include ../../../tests/events.rs:32:54 }} }
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(TransformBundle::default()) .set(name(), "My Entity".into()) .spawn(world); // Get the `Resitution` component assert_eq!(*world.get(entity, position()).unwrap(), Vec3::ZERO); } }
Bundles
The following bundles are provided:
ObjectBundle
- Renderable objects with position and meshRbBundle
- Rigidbody obejctRbColliderBundle
- Rigidbody object with a colliderWidgetBundle
- Base UI element, similar to htmldiv
TextBundle
- UI text elementImageBundle
- UI image elementConnectionBundle
- Declare physical relationships between entitiesTransformBundle
- Position an object with Position, Rotation, and Scale. A matchingTransformQuery
and.into_matrix()
are provided as well.ConstraintBundle
- UI constraints bundle, part ofWidgetBundle
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 }} }