Abiodun Solomon I’m a software developer that is curious about modern technologies. I love contributing to the growth of knowledge for the betterment of humanity.

Comparing Rust scripting languages in game development

6 min read 1823

comparing-rust-scripting-languages-game-development

Rust has been something of a rockstar language in recent times as it continues to grow in popularity, thanks to its numerous benefits; like being very close to C/C++ in terms of performance and its ability to compile to WebAssembly.

The current state of gaming industry has primarily focused on programming languages such as C/C++, Java, and C# for both development and for game engines.

That was until the rise of Rust, which came with a different approach insofar as creating suitable programming languages that are flexible enough for games with respect to high performance, interfacing with other languages, and much more.

Terms to know

  • Scripting language: A scripting language is a language for a runtime system mostly used for automating the execution of tasks — in other words, scripting languages have fast execution because they are interpreted at runtime
  • Game engine: A game engine is a software framework developed with large support libraries for game development; it solves the problems of building game components from scratch which could be time consuming
  • WebAssembly: WebAssembly (abbreviated Wasm) is a concept of interfacing software built with languages such as Rust and Go with the output of a portable binary code format to a different environment, such as browser via JavaScript
  • Lua: Lua is a scripting language which is used in game development for fast execution, its short learning curve, and its ability to easily embed into an existing system
  • Lisp: (Game Oriented Assembly Lisp (GOAL)): This is a dialect of Lisp, which is also used for game development. In addition, Lisp as a language can be embedded in other languages to improve their performance

I will walk you through eight scripting languages developed for gaming in Rust which are categorized based on the following:

  • Community growth based on the GitHub stars
  • Stability based on release
  • Major features

GameLisp

GameLisp is a scripting language that has a unique garbage collector designed to run once per frame without causing latency spikes.

It works by loading the glsp file with the .glsp extension in Rust code. GameLisp is used for building 2D games as it also comes with common features available in modern languages.

Features include the following:

No garbage collection pauses

GameLisp was developed to handle garbage collection; this way it’s able to collect or gain memory back which has been allocated to objects.

Ease of Rust API integration

GameLisp is a Rust crate that can be installed and instantiated in a Rust program by loading the GameLisp file — this process makes it easy for GameLisp to be integrated in Rust code.

Memory-safe

GameLisp doesn’t implement unsafe code since the core logic of GameLisp is entirely written in Rust with few dependencies.

We made a custom demo for .
No really. Click here to check it out.

Easy entity scripting

The other benefit of GameLisp is the object system that is built around mixins and state machines specifically for game development entities.

  • Community: small
  • Stable: No

Example of GameLisp:

# GameLisp
(let fibs (arr 0 1))

(while (< (len fibs) 31)
  (push! fibs (+ \[fibs -1\] [fibs -2])))

(prn [fibs -1]) ; 
// Rust
use glsp::prelude::*;

fn main() {
    let app = Runtime::new();
    app.run(|| {
        glsp::load("external.glsp")?;
        Ok(())
    });
}

Throne

Throne is a scripting language for game prototyping and story logic; in other words, Throne is used to create syntax rules for a game which can be embedded in other engines.

It is developed to interface with JavaScript via WebAssembly. Throne can be used for storytelling and prototyping the concept of a game. It has the file extension .throne.

Its features includes:

Prototyping

Throne prototyping uses a variety of contexts, including semantics, syntax, etc., to express and evaluate the precision of game development analysis.

Story logic

Logic in storytelling is based on a narrative, character’s actions, events, etc., of a user’s point of view for a particular game.

  • Community: Small
  • Stability: No

Example of Throne:

// Throne file
// initialize state
fall-time 5 . default-fall-time 5 . block-id 0 . gravity-shape-id 0 . max-width 10 . max-height 20

#update . !shape _X _Y _BLOCKS . gravity-shape-id ID . + ID 1 ID' = gravity-shape-id ID' . #new-shape

#new-shape . $max-height H = new-shape 4 H ((block -1 0) ((block 0 0) ((block 1 0) ((block 0 1) (cons))))) . #shape-to-blocks (#input-lr)
// Rust
#[cfg(not(target_arch = "wasm32"))]
fn main() {
  let mut context = throne::ContextBuilder::new()
  .text(include_str!("blocks.throne"))
  .build()
  .unwrap_or_else(|e| panic!("{}", e));
}

Wgpu

Wgpu is a cross-platform Rust graphics WebGPU standard API that runs on Vulkan, Metal, D3D12, D3D11, and OpenGLES — and likewise on WebGPU via WebAssembly (wasm).

It is built to allow web code access to GPU functionality in a safe and reliable paradigm, which also has binding support for languages via wgpu-native.

The features include:

Internal WebGPU implementation

Wgpu comes with WebGL as its core standard API, which is also used for rendering graphics such as 2D and 3D on a web browser without external plugins.

Internal GPU API abstraction layer

The performance of Wgpu solely relies on a GPU (graphics processing unit) abstract layer designed to augment the rendering process of graphics.

  • Community: Rising
  • Stability: Yes

Example of Wgpu:

// wgsl file
@vertex
fn main_vs(
  @location(0) particle_pos: vec2<f32>,
) -> @builtin(position) vec4<f32> {
  let angle = -atan2(particle_vel.x, particle_vel.y);
  let pos = vec2<f32>(
  position.x * cos(angle) - position.y * sin(angle),
  position.x * sin(angle) + position.y * cos(angle)
);
return vec4<f32>(pos + particle_pos, 0.0, 1.0);
}
@fragment
fn main_fs() -> @location(0) vec4<f32> {
  return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}


fn main() {
  let draw = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
            label: None,
            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("draw.wgsl"))),
        });
}

Mlua

Mlua is a high-level binding for the Lua language to provide a safe and flexible API. It has the following features:

Multiple Lua version support

This allows other Lua versions API modules to be accessed in Rust as well as Lua in a standalone environment.

Async/await support

It enables async/await support in which executors like Tokio and asyn-std can be used.

Serialization

Support for serialization and deserialization that support mlua types with Serde.

Standalone mode

The standalone mode is the declaration of Lua’s code directly in the Rust program, without the need for external import.

  • Community: Small
  • Production-ready: No

Example of Mlua:

fn main() {
    let mut lua = hlua::Lua::new();

    lua.execute_from_reader::<()>(File::open(&Path::new("script.lua")).unwrap())

    lua.set("add", hlua::function2(add));
    lua.execute::<()>("local c = add(2, 4)");   // calls the `add` function above
    let c: i32 = lua.get("c").unwrap(); 
    //standalone mode
    lua.set("a", 12);
    let val: i32 = lua.execute(r#"return a * 5;"#).unwrap();

}

Dyon

Dyon is a dynamically-typed language designed for game engines and interactive applications with the extension .dyon.

It is designed to work around limited memory model because it lacks a garbage collector. It has the following features:

Built-in support for 4D vectors; HTML hex colors

This allows more flexibility with types which can be used in Rust.

Go-like coroutines

This is a multi-threaded concept but with Go-like style which specifies go before the function name.

Macros

It also comes set of macros which makes embedding code with Rust much easier.

  • Community: Rising
  • Stability: Yes

Example of Dyon:

fn main() {
    shape := unwrap(load("src/shape.dyon"))
    game := unwrap(load(source: "src/game.dyon", imports: [spape]))
    call(game, "main", [])
}

Ketos

Ketos is a scripting language developed to provide an interface for accessing Lisp APIs. It is compiled into bytecode and interpreted by Rust code. It has the .ket extension. with the following features:

Compiles expression into bytecode objects

Ketos supports the compilation of .ket files to bytecode, which improves performance during runtime.

Provides a helper macro

It provides macro support for extracting arguments from Ketos values and value conversion from Rust.

Implements encoding and decoding

It also provides support for modification of bytecode instructions by dissecting data into opcodes.

  • Community: small
  • Stability: No

Example of Ketos:

//ket
(define (range a :optional b (step 1))
  (cond
    ((null b)   (range 0 a))
    ((> step 0) (range-pos a b step ()))
    ((< step 0) (range-neg a b step ()))
    (else       (panic "`range` got 0 step"))))
// Rust
fn main() {
    let interp = Interpreter::new();
    interp.run_code(r#"
        (define (factorial n)
          (cond
            ((< n 0) (panic "factorial got negative integer"))
            ((<= n 1) 1)
            (else (* n (factorial (- n 1))))))
        "#, None).unwrap();
}

Mun

Mun is an innovation of Lua JIT designed for productivity and speed. It is built to easily detect errors during compiling ahead of time (AOT), as opposed to being interpreted or compiled with JIT (just in time) in mind.

It has the .munlib extension with the following features:

Ahead of time compilation

It is compiled ahead of time (AOT) by detecting errors in the code during this process; with this, developers can easily debug runtime errors on their IDEs (integrated development environment).

Statically typed

It resolves type check at compilation time instead of runtime which results in instant feedback for correction when not properly specified.

Performance

Ahead of time (AOT) with static typing and LLVM (low-level virtual machine) for optimization enforces Mun to be compiled to machine code and be executed cross-platform for optimal runtime performance.

IDE integration

It has support for IDE integrations for code completion and refactoring tools, which makes life easier for developers.

Cross compilation

Mun offers support for compiling to all targeted platforms from any supported compiler.

  • Community: rising
  • Stability: No

Example of Mun:

// munlib
extern fn random() -> i64;

pub fn random_bool() -> bool {
    random() % 2 == 0
}
// Rust
fn main() {
    let app = RuntimeBuilder::new("main.munlib")
        .spawn()
        .expect("Failed to spawn Runtime");

    let app_ref = app.borrow();
    let result: bool = invoke_fn!(app_ref, "random_bool").unwrap();
    println!("random bool: {}", result);
}

LuaJIT RS

LuaJIT is developed to interface Lua code from Rust by easily accessing the functions that correspond directly to the underlying Lua C native-code API. It has the following features:

Cross platform

It supports other languages via native binding, since the native API is written in C.

Macro

LuaJIT also provides support for macro to easily communicate/interface directly with C.

  • Community: small
  • Stability: Yes

Example of LuaJIT:

pub fn main() {
    let mut state = State::new();
    state.open_libs();
    state.do_string(r#"print("Hello world!")"#);

    state.push(lua_fn!(return_42));
    state.set_global("return_42");
    state.do_string(r#"print(return_42())"#);
}

Conclusion

We’ve been able to look at several scripting languages for gaming in Rust, most of which are still in their infancy.

The most significant and popular one on this list is Wgpu, because it serves as the core of the WebGPU integration in Firefox, Servo, and Deno.

Elsewhere, Dyon serves as a dynamic language built with support for 4D vectors data type to augment 2D and 3D programming, mathematical iterations and Unicode symbols to improve readability and so much more. Others are built for interfacing with other platforms for performance optimization and more.

What Rust scripting language do you use for your game projects and why do you use it? I’d love to know in the comments section. Gracias! ☺️

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Abiodun Solomon I’m a software developer that is curious about modern technologies. I love contributing to the growth of knowledge for the betterment of humanity.

Leave a Reply