Skip to content

Slow down, upon the teeth of Orange: Heavy is an opinionated, efficient, relatively lightweight, and tightly Lua-integrated game framework for Rust.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

sdleffler/hv-dev

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Heavy - an opinionated, efficient, relatively lightweight, and tightly Lua-integrated game framework for Rust

Slow down, upon the teeth of Orange

Heavy is a mostly backend/platform-agnostic game framework for Rust, with the target of providing efficient primitives for game development while also remaining tightly integrated with Lua scripting.

It consists of two main parts:

  • The hv crate, an aggregation of modular sub-crates which implement a Lua interface and wrap it around a number of common utilities (ECS, math, input mapping, etc.) without being tied in specific to a rendering strategy.
  • Altar, an engine built on hv for building top-down 2.5D games, using luminance for platform-agnostic graphics and supporting multiple underlying windowing libraries.

At current there is also a local fork of hecs adding support for dynamic queries and exposing some parts of the hecs system which were previously hidden. Hopefully to be upstreamed. The scheduler (yaks) is dependent on hecs, so we have a local fork of it too.

Why?

Current Rust game frameworks don't have first-class support for things like Lua integration, and integrating a crate like mlua with external crates is made complicated by Rust's orphan rules; it's a compiler error to try to implement mlua::UserData for say, hecs::World. By forking mlua, we get the ability to add first-class support for the notion of a Rust type reified as userdata (through hv::lua::Lua::create_type_userdata.) This is done through the incredibly cursed hv-alchemy crate.

In other words, the goal of Heavy is to provide a Rusty, efficient interface, which is also tightly integrated with scripting for fast iteration and moddability down the road, as well as for working with non-coding artists and less-technical team members who find working with a scripting language like Lua easier to deal with than a language with a high learning curve and domain knowledge requirement (Rust.)

Also, I'm an idiot, so I like making game frameworks/engines.
- sleff

hv - Features

These are all in progress/goals:

  • hecs-based ECS with yaks-based executor/system scheduler.
  • Lua integration based on a custom fork of mlua, providing easy and powerful integration with Rust traits and types thanks to hv-alchemy.
    • hv- crates integrated w/ Lua by default, w/ runtime reflection support for creating and manipulating Rust types from Lua with minimal (but present) boilerplate:
      • hv-math (nalgebra and goodies)
      • hecs (ECS, entity spawning and querying)
      • hv-filesystem (virtual filesystem)
      • hv-alchemy (runtime trait object registration and manipulation)
      • hv-input (input mappings and state)
    • Support for "Rust type userdata objects" through hv-alchemy and Alchemical reflection on AnyUserData objects.
  • Synchronization primitives and other goodies useful for interfacing with Lua.
  • (TODO) audio through FMOD.
  • Portability limited only by the Rust standard library and Lua (and eventually FMOD).

altar - Features

  • Implemented with hv at its core.
  • Abstracted external events.
  • Abstracted rendering provided by luminance.
  • Portability limited only by the Rust standard library, Lua, and luminance.

Motivating example: defining a Lua interface for a component type, spawning entities in and querying the ECS from Lua

Connecting a component type w/ hv is done through the UserData trait. Here's a contrived but ultra-simple example implementation for a component which just wraps an i32:

/// A component type wrapping an `i32`; for technical reasons, primitives cannot be viewed as
/// components from Lua (because they can't implement `UserData`.)
#[derive(Debug, Clone, Copy)]
struct I32Component(i32);

// The `UserData` impl defines how the type interacts with Lua and also what methods its type object
// has available.
impl UserData for I32Component {
    // We allow access to the internal value via a Lua field getter/setter pair.
    #[allow(clippy::unit_arg)]
    fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
        fields.add_field_method_get("value", |_, this| Ok(this.0));
        fields.add_field_method_set("value", |_, this, value| Ok(this.0 = value));
    }

    // Rust simply does not have compile-time reflection. The `on_metatable_init` method provides
    // the ability to register traits we need at run-time for this type; it also doubles as a way of
    // requiring Rust to generate the code for the vtables of those traits (which would not
    // otherwise happen if they were not actually used.) `.mark_component()` comes from the
    // `LuaUserDataTypeExt` trait which provides convenient shorthand for registering required
    // traits; in this case, `mark_component` registers `dyn Send` and `dyn Sync` impls which are
    // sufficient to act as a component.
    fn on_metatable_init(t: Type<Self>) {
        t.add_clone().add_copy().mark_component();
    }

    // The following methods are a bit like implementing `UserData` on `Type<Self>`, the userdata
    // type object of `Self`. This one just lets you construct an `I32Component` from Lua given a
    // value convertible to an `i32`.
    fn add_type_methods<'lua, M: UserDataMethods<'lua, Type<Self>>>(methods: &mut M) {
        methods.add_function("new", |_, i: i32| Ok(Self(i)));
    }

    // We want to generate the necessary vtables for accessing this type as a component in the ECS.
    // The `LuaUserDataTypeTypeExt` extension trait provides convenient methods for registering the
    // required traits for this (`.mark_component_type()` is shorthand for
    // `.add::<dyn ComponentType>()`.)
    fn on_type_metatable_init(t: Type<Type<Self>>) {
        t.mark_component_type();
    }
}

Now given this, we can write something like this:

// Create a Lua context.
let lua = Lua::new();
// Load some builtin `hv` types into the Lua context in a global `hv` table (this is going to 
// change; I'd like a better way to do this)
let hv = hv::lua::types(&lua)?;

// Create userdata type objects for the `I32Component` defined above as well as a similarly defined
// `BoolComponent` (exercise left to the reader)
let i32_ty = lua.create_userdata_type::<I32Component>()?;
let bool_ty = lua.create_userdata_type::<BoolComponent>()?;

// To share an ECS world between Lua and Rust, we'll need to wrap it in an `Arc<AtomicRefCell<_>>`.
// Heavy provides other potentially more efficient ways to do this sharing but this is sufficient
// for this example.
let world = Arc::new(AtomicRefCell::new(World::new()));
// Clone the world so that it doesn't become owned by Lua. We still want a copy!
let world_clone = world.clone();

// `chunk` macro allows for in-line Lua definitions w/ quasiquoting for injecting values from Rust.
let chunk = chunk! {
    // Drag in the `hv` table we created above, and also the `I32Component` and `BoolComponent` types,
    // presumptuously calling them `I32` and `Bool` just because they're wrappers around the fact we
    // can't just slap a primitive in there and call it a day.
    local hv = $hv
    local Query = hv.ecs.Query
    local I32, Bool = $i32_ty, $bool_ty

    local world = $world_clone
    // Spawn an entity, dynamically adding components to it taken from userdata! Works with copy,
    // clone, *and* non-clone types (non-clone types will be moved out of the userdata and the userdata
    // object marked as destructed)
    local entity = world:spawn { I32.new(5), Bool.new(true) }
    // Dynamic query functionality, using our fork's `hecs::DynamicQuery`.
    local query = Query.new { Query.write(I32), Query.read(Bool) }
    // Querying takes a closure in order to enforce scope - the queryitem will panic if used outside that
    // scope.
    world:query_one(query, entity, function(item)
        // Querying allows us to access components of our item as userdata objects through the same interface
        // we defined above!
        assert(item:take(Bool).value == true)
        local i = item:take(I32)
        assert(i.value == 5)
        i.value = 6
        assert(i.value == 6)
    end)

    // Return the entity we spawned back to Rust so we can examine it there.
    return entity
};

// Run the chunk and get the returned entity.
let entity: Entity = lua.load(chunk).eval()?;

// Look! It worked!
let borrowed = world.borrow();
let mut q = borrowed
    .query_one::<(&I32Component, &BoolComponent)>(entity)
    .ok();
assert_eq!(
    q.as_mut().and_then(|q| q.get()).map(|(i, b)| (i.0, b.0)),
    Some((6, true))
);

Without hv, doing this would require a massive amount of boilerplate for the component types, wrapping a hecs::World in your own custom userdata type that supports this type-based manipulation since as a foreign type you can't impl mlua::UserData directly, wrapping hecs::Entity etc., writing dynamic wrappers around everything required to insert a component of a given type on a per-component basis (Heavy boils this all down into .add::<dyn ComponentType>()), wrappers for a borrowed world, ensuring that the Lua code transparently shows where it borrows and unborrows the world, etc.

So while there's still some unavoidable boilerplate, it's a lot less of a mess and allows for writing Lua interaction code as if you're directly interacting with a given type rather than with your own custom wrapper boilerplate.

Compiling

This repository has two submodules, hv-ecs (our hecs fork) and hv-lua (our mlua fork.) These two crates are kept in separate repositories per fork so that it's easier to pull changes from upstream/rebase onto upstream. Packages in this repository will depend on them via path dependencies, and they depend back on this repository via path dependencies, assuming that they live in the same directory structure as here; so if you're hacking on them it's probably a good idea to start by cloning hv-dev. As a result, the first thing you need to do is:

git clone --recursive https://github.com/sdleffler/hv-dev

Next, by default we depend on LuaJIT. You will want to install LuaJIT 2.0.5 (latest stable as of the time of writing) and set environment variables LUA_INC, LUA_LIB, and LUA_LIBDIR accordingly. LUA_INC will need to be the path to your Lua header files, which is likely the src subfolder of your LuaJIT download; LUA_LIBDIR will be the same path as long as you don't move anything after compiling LuaJIT; and LUA_LIB will be lua51 if you use LuaJIT, or potentially lua52 if using LuaJIT partial 5.2 compatibility.

Once you have this, you should be good to go. In the future, FMOD will be added as a dependency and will also need to be installed locally.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Slow down, upon the teeth of Orange: Heavy is an opinionated, efficient, relatively lightweight, and tightly Lua-integrated game framework for Rust.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •