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

Overview

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:

) { t.add_clone().add_copy().mark_component(); } // The following methods are a bit like implementing `UserData` on `Type `, 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 >>(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:: ()`.) fn on_type_metatable_init(t: Type >) { t.mark_component_type(); } } ">
/// 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
        
         `, 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::
        
         ()`.)
        
    fn on_type_metatable_init(t: 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::
   ()?;

   let bool_ty 
   = lua.
   create_userdata_type
   ::
   
    ()?;


    // To share an ECS world between Lua and Rust, we'll need to wrap it in an `Arc
     
      <_>>`.
     

    // 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:: () ), 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.

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.

You might also like...
A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it techinically works
A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it techinically works

rustetris A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it tech

Wasm game of life - A Rust and WebAssembly tutorial implementing the Game of Life

wasm_game_of_life Conway's Game of Life in Rust and WebAssembly Contributing | Chat Built with 🦀 🕸 by The Rust and WebAssembly Working Group About T

A small, portable and extensible game framework written in Rust.
A small, portable and extensible game framework written in Rust.

What is This? Crayon is a small, portable and extensible game framework, which loosely inspired by some amazing blogs on bitsquid, molecular and flooo

A safe, fast and cross-platform 2D component-based game framework written in rust

shura shura is a safe, fast and cross-platform 2D component-based game framework written in rust. shura helps you to manage big games with a component

Managed game servers, matchmaking, and DDoS mitigation that lets you focus on building your game
Managed game servers, matchmaking, and DDoS mitigation that lets you focus on building your game

Managed game servers, matchmaking, and DDoS mitigation that lets you focus on building your game. Home - Docs - Twitter - Discord 👾 Features Everythi

A framework for saving and loading game state in Bevy.

Bevy_save A framework for saving and loading game state in Bevy. Features bevy_save is primarily built around extension traits to Bevy's World. Serial

A game of snake written in Rust using the Bevy game engine, targeting WebGL2

Snake using the Bevy Game Engine Prerequisites cargo install cargo-make Build and serve WASM version Set your local ip address in Makefile.toml (loca

Conway's Game of Life implemented for Game Boy Advance in Rust

game-of-life An implementation of a Conway's Game of Life environment on GBA. The ultimate game should have two modes: Edit and Run mode which can be

🎮 A simple 2D game framework written in Rust

Tetra Tetra is a simple 2D game framework written in Rust. It uses SDL2 for event handling and OpenGL 3.2+ for rendering. Website Tutorial API Docs FA

Comments
  • hv-boxed triggers an ICE when compiling on nightly-x86_64-unknown-linux-gnu rustc 1.61.0-nightly (8d60bf427 2022-03-19)

    hv-boxed triggers an ICE when compiling on nightly-x86_64-unknown-linux-gnu rustc 1.61.0-nightly (8d60bf427 2022-03-19)

    Trying to run cargo check or cargo build from within hv-boxed or hv-dev triggers an internal compiler error in rustc, causing it to panic. This was found on nightly-x86_64-unknown-linux-gnu rustc 1.61.0-nightly (8d60bf427 2022-03-19), running EndeavorOS. Repro: Swap to nightly and swap to 2022-03-19 cargo build or cargo check from within hv-boxed or hv-dev

    opened by maximveligan 0
  • integrate tealr

    integrate tealr

    How are we going to do this?

    Right now, the fork supports the add_type_methods method in hv_lua's userdata. This is done in such a way that it doesn't require changes in the tealr_generate_docs crate.

    However, I'm not sure if I did the TypeBody implementation for the new types introduced by hv_lua correctly and I am probably missing some of those as well.

    Further things: tealr is normally compatible with both mlua and rlua. Right now, that compatibility is broken in the fork but not entirely removed.

    tealr + branch that I use as "fork" for FishFight : https://github.com/lenscas/tealr/pull/39

    edit: It might be possible to reverse the dependency, tealr doesn't care if you have enabled one of the backends or not. And the TypeWalker struct only cares about things having implemented A: 'static + TypeName + TypeBody.

    Then it might be possible to add support for everything that hv_lua does to tealr without actually forking tealr.

    TealData can then simply be merged with the UserData from hv_lua. So, I don't think that this is a problem is it is just some extra bounds on UserData that are needed.

    TypeName has a new (default) method to generate the name of a Userdata type (with a Userdata type I mean the userdata that can be used to create dynamic queries and which also gets methods through the add_type_methods method)

    TypeWalker has a new method (process_type_as_marker) to get all the information for a Userdata type.

    Both of those would require another solution. I think it is possible to solve both of them by working with a Type<T> instead of a T though it would mean that you won't be able to modify what the name is of a Userdata type.

    Lastly, some things would need to be reimplemented on hv_lua's side regardless as they are only accessible if a backend is selected. These are: The generic macro (and optionally, the premade generics) https://github.com/lenscas/tealr/blob/master/tealr/src/mlu/generics.rs

    the Union macro https://github.com/lenscas/tealr/blob/master/tealr/src/mlu/picker_macro.rs

    and lastly, the TypedFunction https://github.com/lenscas/tealr/blob/master/tealr/src/mlu/typed_function.rs

    The types that hv_lua exposes need to implement TypeName and unless they are built in to lua also need to implement TypeBody.

    opened by lenscas 2
Owner
Shea Leffler
Programmer and violinist.
Shea Leffler
Hierarchical Task Network Planning deeply integrated with bevy, which I use in my games :P

Note that CI currently tests against a matrix of (windows, mac, linux) * (toolchain stable, nightly) * (cargo build, test, clippy), which ensures vali

null 29 Oct 16, 2024
An opinionated 2D game engine for Rust

Coffee An opinionated 2D game engine for Rust focused on simplicity, explicitness, and type-safety. Coffee is in a very early stage of development. Ma

Héctor Ramón 946 Jan 4, 2023
Victorem - easy UDP game server and client framework for creating simple 2D and 3D online game prototype in Rust.

Victorem Easy UDP game server and client framework for creating simple 2D and 3D online game prototype in Rust. Example Cargo.toml [dependencies] vict

Victor Winbringer 27 Jan 7, 2023
Solana Game Server is a decentralized game server running on Solana, designed for game developers

Solana Game Server* is the first decentralized Game Server (aka web3 game server) designed for game devs. (Think web3 SDK for game developers as a ser

Tardigrade Life Sciences, Inc 16 Dec 1, 2022
play NES rpgs with fceux, lua, and rust

rpg-bot play NES rpgs with fceux, lua, and rust running server

Jonathan Strickland 4 May 21, 2022
An opinionated, monolithic template for Bevy with cross-platform CI/CD, native + WASM launchers, and managed cross-platform deployment.

??️ Bevy Shell - Template An opinionated, monolithic template for Bevy with cross-platform CI/CD, native + WASM launchers, and managed cross-platform

Kurbos 218 Dec 30, 2022
An opinionated 2D sparse grid made for use with Bevy. For storing and querying entities

bevy_sparse_grid_2d An opinionated 2D sparse grid made for use with Bevy. For storing and querying entities. Personally, I'm using it for simple stupi

Johan Klokkhammer Helsing 5 Feb 26, 2023
Blossom is an opinionated MUD engine written in Rust.

?? Blossom Blossom is an opinionated MUD engine written in Rust. This is still a VERY early work-in-progress and there will be sweeping, breaking chan

Rob 1 Dec 20, 2022
A lightweight job framework for Bevy.

bevy_jobs A lightweight job framework for Bevy. Getting started Defining a job: pub struct FetchRequestJob { pub url: String, } impl bevy_jobs::J

Corey Farwell 3 Aug 31, 2022
2-player game made with Rust and "ggez" engine, based on "Conway's Game of Life"

fight-for-your-life A 2-player game based on the "Conway's Game of Life", made with Rust and the game engine "ggez". Create shapes on the grid that wi

Petros 3 Oct 25, 2021