vibbit! a fun 2d graphics and game framework!

Peek 2023-09-15 04-30

vibbit! a fun 2d graphics and game framework!

I teased this a little bit in my rust thread, and then later on twitter after news around the Unity licensing changed, so now feels like a good time to write a bit more about what this is and what the goals are for it.

Vibbit is a graphics and game framework designed to be hyper lightweight, and act as a solid springboard for larger engines/frameworks. It should feel pretty familiar to anyone with experience with frameworks like love2d, but the api is largely inspired by the minimalism of raylib.

features

graphics!

Peek 2023-09-14 14-31
it draws shapes, lines, and sprites! load custom .ttf fonts, including pixel art ones. it’s quite optimized for 2d sprite games. The above gif shows it rendering two scenes to their own textures, then combining them both for a split screen game. The whole thing is ~120 lines of code, and should be even shorter by the time it actually releases.

In a popular stress test called a bunnymark, it can render about 960 thousand individual textures with updating positions and rotations each frame. I have no clue when you’d need that many but it’s very neat. For comparison, the same test can do about ~120k with raylib and ~200k with love2d. I’d love to talk about some of the tricks and research that went into that another update.

no concepts!

Okay - Well of course there are some concepts. But the primary goal behind the design of vibbit’s API is that you shouldn’t have to learn any sort of structure, or game pattern to start working with it. A lot of code-only frameworks try to bring along all these ideas like a “game world” with a tree of “game objects”, or things like entity-component-systems. Those things sound great at a glance, but if you have a vision of how your game should work, they really only get in the way and force you to circumvent them anyways! vibbit comes out of the box with none of those things, so you can bring one in or write one yourself if you like.

In essence vibbit is just a handful of functions, (i’d like to aim for 1.0 to have a nice clean number like 128, but then what do i do about updates X__X…) each doing exactly what it says. Want to load a texture? load_texture(). want to draw it? draw_texture(). how do you end the frame and display it all? end_frame(). I’d like for all the “How do I do THIS” questions to feel really straightforward just by looking at all the functions available, no need to dig through 20 classes and sub classes before finding what you want.

highly "port"able

The codebase is written in Rust, which I had decided on after spending a while studying and trying out other frameworks made in rust and getting familiar with the language. However it’s been designed in a way that should make things very easy to get it set up with other languages too. Having a super lightweight and performant base is important for slower, interpreted languages like wren or lua (non-JIT). The idea is to let the graphics framework stay out of the way as much as it can so your game logic has plenty of headroom. I got the idea to work on this after spending a lot of time contributing to GitHub - RobLoach/node-raylib: Node.js bindings for Raylib - I really liked the simplicity of the drawing API, and being able to work in Javascript was a lot faster to me than working in C/C++. After a while I got a lot of ideas on how to improve the performance of the bindings, but it involved enough work that it just felt right to put together a new library from scratch with porting in mind. There are two major things that give this an edge for portability:

  • active assets are stored on the side of the framework in a generational arena for easy allocation without working with pointers or a hashtable. Basically, from your scripting language like lua, when you are referencing a texture to draw, you only need a 64 bit “key” that can be stored inside a value like an int, so you don’t have to pass whole large objects between language to language just to do a draw.
  • seperate render thread. a lot of other frameworks, when you request to draw something, immediately begin doing work to send data over to the GPU and perhaps draw or batch it into something. In a scripting language that means on top of the cost of calling the external function, you also have to wait for a bunch of gpu work. vibbit is designed to make use of it’s own thread for all rendering, so when you make calls all it does is prepare a list of commands that get consumed at the end of the frame. so your game logic gets a ton of extra cpu time each frame, and you don’t have to worry about implementing something like that yourself. It does all this in the background while still working exactly how you expect.

non-features

these are things that don’t really fit into the scope, or really are just better as their own library:

  • physics / collision lib. I might add some funcs that do shape overlaps, but physics is something that is usually pretty game dependent, or tied heavily into your engine’s object tree. I DO think that rust needs an arcadey collision lib, (no integration step, just overlap checks in pixel increments like celeste/towerfall) instead of all the box2d kind of stuff. If i worked on that it would be way later and it’s own package though.
  • networking, file reading. Chances are you would just be better off using the standard library for this. though some languages maybe don’t have them, and maybe bindings to those would get custom ones tailored to that lang instead of a general approach.
  • 3d. It should be possible to do some 3d rendering by drawing triangles, and I may include a mesh system so you could in theory roll your own 3d if need be. but 3d scenes require so much setup to actually perform well with instancing and such, compared to the style of 2d “just add some triangles to the batch” approach. By the point you do all that you’d be better off using a larger engine…

goals / todos

These are some major things that I want to get before putting up any public builds or previews:

  • audio. I think I could quickly set up basic sound + music playing, but I’d like to get some feedback from developers on what they actually need for this, particularly music/rhythm devs. let me know if you have thoughts on this element (any frustrations with current things you use? etc)
  • better shader setup. before multithreading you could load and set uniforms on custom shaders, but this needs to be reimplemented
  • joypad input. I need to think a bit about how to handle this in a clean way everyone would like. Smooth unplug/replug support is a must for me personally, and I think handling multiplayer is important too.
  • deltatime isn’t calculated correctly right now, last i checked
  • error output and debug logging. There is SOME rust-specific stuff that needs to be done with it, errors are kind of a special part of rust. I also need a good debug output so users know what’s happening behind the scenes when they make calls from a scripting language
  • decide on a main scripting language to support. I’d like to have at least one supported out of the box on launch (other than rust) to demonstrate a) how simple it should be to create a binding and b) how well they work. lua is the obvious choice but so many other things have it. I quite like statically typed ones like wren, and umka is pretty interesting too.
  • a million other small things. input stuff like mouse support + being able to record what’s been typed in as a string.
  • website + docs. before going fully public and open i want this library to actually feel USABLE instead of just some hobby project. A lot of my hesitation with rust’s ecosystem is that the documentation in other game crates is VERY tech-oriented, and doesn’t match the questions that a game developer just checking it out want answered. So on top of documenting the actual API it really needs a site with written content on basic tasks everyone typically needs, “how to XYZ” pages, and maybe a resources page with other libraries and info on filling in things not implemented in vibbit itself. This may take about as much time as the library itself honestly, but I think it’s vastly important for any amount of longevity for a project like this.

These are some larger concepts that are kind of on the backburner:

  • auto texture atlasing. it would be neat to load in all your game’s textures and then say “hey, batch these all into one texture atlas for me”, but then still be able to use all your original texture objects. It would just check the atlas for that individual texture, then convert the draw command to instead use the subcoordinates of the atlas. This would be a big performance boost for texture rendering if you have a lot of small sprites in their own files, and in theory it should just work seamlessly behind the scenes.
  • default “baked in” font in case you don’t want to bundle and load your own at runtime
  • more modular systems. I do have the groundwork in place for a bit, but I think for longevity sake it should be easy to add new modules to replace the core external libraries for the project. The main offenders for this are your windowing library and the graphics API. Right now it’s on SDL2 and OpenGL for those, but opening up for users to just pick what they like before compiling and not have to worry about it will be nice. That kind of thing is also really important way down the road if this ever needs extra platform/console support
  • Some kind of fully-fledged example game. In addition to little individual code snippet examples I think it needs an actual playable game that isn’t just pong or flappy bird or whatever. The idea would be something small enough to have code you can read through in one sitting, but big enough to use third party things like loading tilemaps + collision. I feel like some kind of older flash-style platformer would be fine scope. nothing too “juicy”

That’s what I got so far! Totally open to suggestions or questions - in fact I’d love to hear if you’ve used barebones frameworks like love2d, monogame, haxeflixel, etc so I can maybe iron out some pain-points people have with those ahead of time. I’ll try to get more images and gifs as I add more flashy stuff

that’s really cool! i love trying out new game framework, and you seem to have a very clear and sensible design for the scope of vibbit!

i’m not the biggest Rust, but i’ll gladly try to make something with it anyway when possible

best of luck :)

sidequestion: will this be open source?

thanks yeah - I’d be interested in letting folks from here work with it a bit earlier if they wanted to help with writing tiny examples / demos, and just generally giving feedback on the ease of use and such. I’d have to think about something i could do or help out with in return probably…

Yup that’s the plan, though I haven’t settled on which license yet. It really doesn’t make any sense for this kind of thing NOT to be FOSS - there are enough alternatives, love/monogame/raylib etc, that are already free why bother buying something proprietary? I really don’t have any model for monetization other than ‘maybe this seems cool and lands me a job down the road’

Screenshot from 2023-03-28 04-03-42Screenshot from 2023-03-28 07-24-13Screenshot from 2023-03-28 04-03-21

originally i wanted the icon/logo thing to be more of a face. having it be something you could draw with the tool itself was important, and the shakey lineart approach is nice because it gives off a vib ribbon look, which is where the name kind of came from. but simplifying it to a more flat lilypad kind of look ends up being a lot easier to draw in a vector programmatic way. and admittedly is a bit less creepy looking

Also, if Godot is any indicator, making your symbol a cartoony face tends to push people away

for me it was never having a face that bothered me, in fact i like mascots when they are kept pretty simple. moreso just that the robot head just screams clipart in my opinion


testing to see if webm uploads work ~~
I put together a little demo this morning that does a pseudo 3d effect. it is definitely legally distinct from any other 3d 2d hybrid games. ideally I’d like to have a lot of small one-screen little projects like this that demonstrate using the framework to do different common effects, and give an example of the breadth of what you can set up even with a simple/barebones toolkit like this


not entirely related, but i’ve been working out fully 3D rendering recently too, while studying slightly more modern opengl techniques. been doing the above all in c++ and i can say for certain i made the right choice writing vibbit in rust! so many fewer code tasks feel like ‘chores’. plus the build system makes it take way more setup to compile on anything other than the host OS.

i’ll probably go back and reimplement some of my data structures to use this part of the API later on. it’s an interesting challenge working on this deciding just how modern of opengl features to support - most resources out there don’t bother talking about anything after 3.3 - and its still a good choice if you wanna support macos - but the DSA api has a lot of cool stuff that makes it easier to deal with your GPU resources outside of the global state. at this point, what I’m considering is having a default mode, with a seperate “3.3-esque” fallback mode that is handled based on compilation platform - right now macos and webgl would share this mode. end users shouldn’t have to configure anything, and the outward API will remain the same. the trick is to do this in a way where i’m hopefully not writing two seperate rendering backends, just ‘diverting’ to fallbacks when it actually matters.

in actual Vibbit news - I’ve alluded to this briefly on twitter, but I’m setting a goal for the project to enter a private test starting sometime in January. The goal of this is to see how far people can get with the framework more-or-less on their own (of course i’ll be around, but there won’t be any docs, and very few examples). im not expecting any real games to necessarily come together, but maybe some more visual demos that could be recycled into example files (credit included of course). I really want to see how intuitive and ergonomic the library is to people going in mostly blind.

My next update is gonna go more in-depth on what the private test will look like, and what i need to get done leading up to that (spoiler: a LOT, there isn’t even an audio/sfx api). But if trying it out a little early sounds interesting, feel free to shoot me a message. this demo will require working in rust, but the plan is for the first official release to have at least 1 scripting language still.

Technical wizardry in this thread! :o I would love to try my hands at this when the time comes, though before I send the fateful message, I need to ask - in your personal estimate, how much of an uphill battle would it be to learn the coding language of this engine for somebody whose sole experience with written code is GML/Javascript?

I recall back in the late 2000s I attempted to learn Rust for work on an extremely ill-fated fangame in RMXP, and failed - naturally because I was 13, but even so.

thanks for reading! I have to be honest - it will be kinda rough… the symbolic hill is an acute angle, and you’re on the inside face…
on one angle, a big challenge coming from traditional engines is this is a ‘bring your own EVERYTHING’ approach. there are no maps, or tiles, or entities, or scenes. you’d have to do a lot of library hunting, or be comfortable trying to write all the small little game engine features you’ve taken for granted from scratch. i’d say the whole thing is mostly intended for tinkerer types who want to figure things out for themselves

for the programming, to put it short:
rust is known for introducing entire new categories of compiler errors that will have you scratching your head, and reading dozens of stackoverflows, for what looks like incredibly normal, reasonable code. and hours later you will find out that the solution to the problem, instead of fixing a silly typo or importing a useful function/data structure, is to completely scrap the idea for how you first planned your function to work, and rewrite it in an entirely different style you’ve never seen before. instead of just syntax errors, the compiler feels as if it has ‘Opinions’ on your code - it is the first language which will make you think it actively disagrees with the program you are making. that’s not to say it is all bad - learning it teaches you a lot about how programs actually work under hood, and has actually helped me a bit with being able to prematurely identify and work around potential bugs when working in other languages too. it Really retrains your brain on a ton of programming concepts
and in my opinion, rust is dozens of times easier to get setup with, and start messing around and trying out libraries compared to the other ‘hard’ languages like c++. the ecosystem of tools you use to work with rust are top-notch, and integrate perfectly with the language

to put it long: i’ve got an in-depth thread on my experience with Rustlang for gamedevs

as sadistic as i made it sound, im not using it to ‘challenge myself’, and i really believe its something that can push smalltime/indie game engines to solve traditional tech debt challenges (multithreading, efficient batching, data throughput, fast scripting languages) with a fraction of the effort of more traditional choices. and if rust isn’t your tea in the end, focusing on porting it directly to other languages like javascript is already in the air, where you can get the chance to learn things in a familiar environment, then swap back to rust if you feel the need to squeeze some more performance, while translating basically 100% of the skills learned from the framework between languages

ehehe - That’s Ruby actually! Of which i’ve worked with a lot in my earlier rpgmaker days, but probably would never try to build something like this with. In the grand scheme of things Rust is an incredibly new language (2015), which is largely why the game development ecosystem is in such flux. also because its a very low level technical language, mostly pure-engineers play with it, so the running joke is that there are literally more game engines in development than there are actual games.

How embarrassing ;__; Yeah you’re absolutely right, that’s what I was thinking of.

Yeaaahhhh… Got it.

I’ve sort of taken to pointedly calling myself a “scripter” rather than a “programmer”, and perhaps I should remember the distinction of the two (at least in my mind) better from henceforth.

Hope some cool people bite and try the engine out, though! >:)


Been a while but have been able to continue some work on vibbit recently! Probably worth taking another look at the todolist and sorting things into a workable order. This one is a pretty sizably useful feature though - user supplied shaders. This is a feature I already had going in my original single-threaded test version of the tool, but implementing it this time around widestepped a few bugs i didn’t know the original ever had, and it’s got a bit nicer functionality overall too.
Things are pretty barebones right now but definitely usable - you can load up a shader just by providing strings for vertex, fragment, and even a geometry shader if you optionally need one. then when you set the graphics to ‘use’ that shader, any type of draw call you make (lines, shapes, sprites) will try to use it. there’s also a convenient ‘reset’ function that puts everything back to its default shaders. Right now there’s also a utility function for working with sprites, where for a ‘sprite shader’ you only provide a fragment shader, and it will populate the other steps with the default code used by vibbit. should be useful for quickly experimenting with things.

here’s a stripped down code example for the above video:

code
fn main() {
	let mut vib = vibbit::init(640, 360, "custom shader");
	let tex = vib.load_texture("./assets/toskr.png");
	let mut bg = Color::new(255, 254, 254, 255);
	let white = Color::new(255, 255, 255, 255);
    let black = Color::new(0, 0, 0, 255);
	let font = vib.load_font("./assets/duster.otf", 13.0);
	// FRAG is just a const string with the shader code
    // load_sprite_shader provides the vertex shader for you
	let shader = vib.load_sprite_shader(FRAG);

	let mut time: f32 = 0.0;

	// main app loop
	while !vib.should_close() {
		time += 0.01;
		vib.clear_screen(bg);

		// [..] update sprite positions

		vib.gfx_set_shader(shader);
        // shader uniform setters are strongly typed
		vib.gfx_set_shader_uniform_float(shader, "time", time);
		vib.draw_texture_scale(tex, pos_a, white, Vec2::new(2.0, 2.0));
		vib.gfx_clear_shader();

		vib.draw_text(&font, -74.0, 0.0, black, "they are vibing", 2.0);

		// when loading shaders consecutively, uniforms are remembered from last use (even across frames)
		// so setting 'time' is not needed more than once per frame
		vib.gfx_set_shader(shader);
		vib.draw_texture_scale(tex, pos_b, white, Vec2::new(2.0, 2.0));
		vib.gfx_clear_shader();

		// end frame
		vib.end_frame();
	}
}

shader-related function names are a little longer than i’d like right now :blush: :grimacing:

When it comes to providing uniforms - the uniforms you provide to a shader are cached and linked to that specific shasder. So you can load in a shader, draw some things, switch to another, and when you come back it remembers every uniform. It caches them across frames as well. This should also be useful for cases where two different shaders have a uniform with the same name but you know the values shouldn’t be linked. This seems like the most ergonomic way to handle it to me (there’s no downside to manually specifying each uniform each frame if you really want to) - but if there are any holes in this idea I’m not seeing now i’d be interested to hear.

I’m also leaving compute shaders off the table for the time being, though I do want to include them eventually. The issue with them is that vibbit defers any GPU work to the very end of a frame, and often times with compute shaders you want to grab the data off the GPU and do something with it after, or use it as a buffer for rendering. Right now vibbit is largely ‘one-way’ in that your main code will send instructions to the render thread, but if that thread needs to send something back its not clear when that will be done by. leaves some questions on whether there should be special cases to have blocking, non-async rendering tasks and how to handle that.

Interested in hearing how easy shaders seems to work with, from the code example and description of how vibbit handles uniforms. Everything mostly make sense from first impressions? I wanna hear how you usually work with shaders (esp. outside of a larger engine like unity). The next major systems to really fold in are audio/music, then probably gamepads. At that point it’d be pretty acheivable to get a functional game out vibbit (if you think small, of course :gms_explosion:)

Nice work! If/when you make the source available I’d definitely be interested to try this out. One application that jumps out at me especially with shader support is 2D video art, which often has similar technical requirements to game frameworks.

I’m curious, what’s the error handling situation like here, e.g. is there a way to catch and detect a mistake in the GLSL? I would normally expect a shader loading method to return a Result since so many things can go wrong.

very cool! glad to see vibbit is going strong. to share how i tend to use shaders, for 2d pixel art games i mostly use vertex shaders for distortion/camera effects and fragment shaders for cool shit such as palette swapping and background effects. having a layer system to apply a fragment shader to an entire layer is a super cool feature (that could be achieved if we can render to textures in vibbit)

and building up on jamie’s reply, how does vibbit handle errors?

I think there could be a good match there - this is definitely very ergonomic for ‘demoscene’ style visual work. For becoming a more solid multimedia framework it’d probably need to open up support for way more filetypes (sound, image formats), or for example I don’t really have plans to integrate video playback. Being able to feed video signal into a shader does sound fun - but that’s usually the type of thing where unless vibbit bundled an in-house decoder (slow?) it would be a lot of platform-dependant work. So i’d probably only tackle it if absolutely necessary for some project

the design focus of vibbit so far is to keep things as non-rusty as possible from the API side. So it currently panics on a compilation error (and the driver handles glsl debug output).
Another issue with returning a Result here is the multithreading means your actual load function doesn’t have a failstate - all it does is reserve a unique ID that a shader should occupy when it’s loaded. You can still use your shader in draw calls that same frame because when the draw queue is consumed it’ll do all the loads first, and that shader uid is shared on the frontend and the render thread.

let my_shader = vib.load_shader("vs", "fs"); // returns 'handle' for shader
//[...] game logic, drawing
vib.end_frame(); // panic! shader compilation error

Internally - every drawing pipeline (like sprites, triangles, so on) have a default shader associated with them. So I could if desired set it up not to panic, and just always defer to the default if some issue exists with a shader you try to use. This is already how it will work if you try to use a shader you’ve deleted on some previous frame. This does make me think, maybe i could have end_frame() return a Result containing any issues it runs into while trying to do it’s work. This would include rendering issues, but also things like if the player tried to close the window.

Should be very possible - though i’ll note here that the default vertex shaders you’d be overriding are set up kind of differently for sprites, which is part of how I squeeze so much rendering optimization for them. You’d need to copy the source from the vibbit files to make use of it. this isn’t actually an issue or anything, just a matter of needing to document it. its also why i made a util function for sprite shaders that only accepts the frag shader.

framebuffers are already implemented and surprisingly they worked alongside shaders without any issues, first-try. It’s how the split-screen gif in the OP works. I’d like to include a utility function for framebuffers where you pass it one and it tries to draw it upscaled to the window size as smoothly as possible, either letterboxed or using a shader to help stretch it while keeping pixels uniform

image

bit of a non-update:
have been away from home this week (catsitting) and… the above shaders update isn’t even pushed to remote git. :gms_explosion:

so rather than work on anything internal i just put together a quick example to test integrating lua, with https://crates.io/crates/mlua
i set up the bare minimum to just get it running, and show how simple it is, in a style pretty similar to love2d:

function init()
    t = 0.0
end

function update()
    t = t + 1.0
    clear_screen()
    draw_text("hello from lua!", -32.0, math.sin(t / 10.0) * 10.0)
    print("test")
end

there’s no reason you couldn’t just hand over the end_frame() and should_close() functions from vibbit to lua, and have the whole update loop controlled via scripting. I may idly add other functions to this sample as I go on with the project just to try out the performance, and make sure my ideas about sharing resources like fonts/textures work properly

something i like about this too, compared to love2d, is if you used some sort of bundler (im sure ones out there for lua) - you could just embed your game code into the binary itself with rust’s include_str! macro - and in theory do your own code signing and such, too?

I’m not 100% sure if my setup is best practice (using thread_local!, every call from lua into rust invokes a borrow) - but here’s the code sample:

embedding lua
use std::cell::{RefCell};
use vibbit::{Vibbit, Color};
use mlua::prelude::*;

thread_local! {
    static VIB: RefCell<Vibbit> = RefCell::new(Vibbit::new(320, 180, ""))
}

struct App {
    lua: Lua
}

impl App {
    pub fn new() -> App {
        let lua = Lua::new();
        let mut app = App { 
            lua
        };
        app.init_module().unwrap();
        app
    }

    fn init_module(&mut self) -> Result<(), LuaError> {
        let globals = self.lua.globals();

        let font = VIB.with_borrow_mut(|vib| {
            vib.load_font("./assets/duster.otf", 13.0)
        });

        let clear_screen = self.lua.create_function(|_lua, ()| {
            VIB.with_borrow_mut(|vib| {
                let c = Color::new(0, 0, 0, 255);
                vib.clear_screen(c);
            });
            Ok(())
        })?;

        let draw_text = self.lua.create_function(move |_, (txt, x, y): (String, f32, f32)| {
            VIB.with_borrow_mut(|vib| {
                let c = Color::new(255, 255, 255, 255);
                vib.draw_text(&font, x, y, c, &txt, 1.0);
            });
            Ok(())
        })?;

        globals.set("clear_screen", clear_screen)?;
        globals.set("draw_text", draw_text)?;
        Ok(())
    }

    fn run(&mut self, script: &str) {
        let globals = self.lua.globals();

        self.lua.load(script).eval::<()>().unwrap();

        let init: LuaFunction = globals.get("init").unwrap();
        let update: LuaFunction = globals.get("update").unwrap();

        init.call::<_, ()>(()).unwrap();

        loop {
            update.call::<_, ()>(()).unwrap();

            let mut should_close = false;

            VIB.with_borrow_mut(|v| {
                v.end_frame();
                should_close = v.should_close()
            });

            if should_close {break}
        }
    }

}

const SCRIPT: &str = r#"
function init()
    t = 0.0
end

function update()
    t = t + 1.0
    clear_screen()
    draw_text("hello from lua!", -32.0, math.sin(t / 10.0) * 10.0)
    print("test")
end
"#;

fn main() {
    let mut app = App::new();
    app.run(SCRIPT);
}

image
image
curiosity got the better of me and I gave the lua demo the ability to draw textures as well. when set to the same limit drawing 20k a frame, on my laptop, vibbit seems to have a nice boost! ~20% or so, maybe. It’s a pretty CPU-bound situation esp. on a laptop that doesn’t top 2.8ghz. nowhere near a real-world comparison given how little functionality is exposed on vibbit’s end - but i think it means the ideas going into it are solidly achieving the goals i’m working towards.