#engine #game-engine #gamedev #game

thin-engine

A thin engine between glium, winit, glium-types and winit-input-map with some extra helper functions

7 releases

new 0.3.1 Nov 20, 2024
0.3.0 Oct 15, 2024
0.1.4 Jul 1, 2024
0.1.3 Jun 23, 2024
0.1.0 May 11, 2024

#365 in Game dev

MIT OR Apache-2.0 OR CC-BY-4.0

135KB
580 lines

Thin Engine

A thin game engine (hence the name) which ties some of my libraries (winit-input-map and glium-types) into an easy to use, low boilerplate and high control game engine. Best suited for small projects.

Features: - Gamepad Support - Variable Input Support - Drawing through glium - Prebuilt shaders - Data Types for everything in glsl - Quaternions - Prebuilt meshes - optional text renderer

use std::f32::consts::PI;
use thin_engine::{prelude::*, meshes::teapot};
use draw_parameters::BackfaceCullingMode;
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
enum Action {
    Jump, Exit,
    Left, Right, Forward, Back,
    LookUp, LookDown, LookLeft, LookRight
}
use Action::*;

let (event_loop, window, display) = thin_engine::set_up().unwrap();
window.set_title("Walk Test");
window.set_cursor_grab(CursorGrabMode::Confined).ok();
window.set_cursor_grab(CursorGrabMode::Locked).ok();
window.set_cursor_visible(false);

let input = {
    use thin_engine::input_map_setup::*;
    input_map!(
        (Jump,    Space,  GamepadButton::South),
        (Exit,    Escape, GamepadButton::Start),
        (Left,    ArrowLeft,  KeyA,  Axis(LeftStickX,  Neg)),
        (Right,   ArrowRight, KeyD,  Axis(LeftStickX,  Pos)),
        (Forward, ArrowUp,    KeyW,  Axis(LeftStickY,  Neg)),
        (Back,    ArrowDown,  KeyS,  Axis(LeftStickY,  Pos)),
        (LookRight, MouseMoveX(Pos), Axis(RightStickX, Pos)),
        (LookLeft,  MouseMoveX(Neg), Axis(RightStickX, Neg)),
        (LookUp,    MouseMoveY(Pos), Axis(RightStickY, Neg)),
        (LookDown,  MouseMoveY(Neg), Axis(RightStickY, Pos))
    )
};

let (indices, verts, norms) = mesh!(
    &display, &teapot::INDICES,
    &teapot::VERTICES, &teapot::NORMALS
);
let draw_parameters = DrawParameters {
    backface_culling: BackfaceCullingMode::CullClockwise,
    ..params::alias_3d()
};
let program = Progronam::from_source(
    &display, shaders::VERTEX,
    "#version 140
    out vec4 colour;
    in vec3 v_normal;
    uniform vec3 light;
    const vec3 albedo = vec3(0.1, 1.0, 0.3);
    void main(){
        float light_level = dot(light, v_normal);
        colour = vec4(albedo * light_level, 1.0);
    }", None,
).unwrap();

let mut pos = vec3(0.0, 0.0, -30.0);
let mut rot = vec2(0.0, 0.0);
let mut gravity = 0.0;

let mut frame_start = Instant::now();
let settings = Settings::from_fps(60);
thin_engine::run(event_loop, input, settings, |input, _, target| {
    let delta_time = frame_start.elapsed().as_secs_f32();
    frame_start = Instant::now();

    let size = window.inner_size().into();
    display.resize(size);
    let mut frame = display.draw();
    let view = Mat4::view_matrix_3d(size, 1.0, 1024.0, 0.1);

    //handle gravity and jump
    gravity += delta_time * 9.5;
    if input.pressed(Jump) {
        gravity = -10.0;
    }

    //set camera rotation
    let look_move = input.dir(LookRight, LookLeft, LookUp, LookDown);
    rot += look_move.scale(delta_time * 7.0);
    rot.y = rot.y.clamp(-PI / 2.0, PI / 2.0);
    let rx = Quaternion::from_y_rot(rot.x);
    let ry = Quaternion::from_x_rot(rot.y);
    let rot = rx * ry;

    //move player based on view and gravity
    let dir = input.dir_max_len_1(Right, Left, Forward, Back);
    let dir =  vec3(dir.x, 0.0, dir.y);
    pos += dir.transform(&rx.into()).scale(5.0*delta_time);
    pos.y = (pos.y - gravity * delta_time).max(0.0);

    if input.pressed(Exit) { target.exit() }

    frame.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0);
    //draw teapot
    frame.draw(
        (&verts, &norms), &indices,
        &program, &uniform! {
            view: view,
            model: Mat4::from_scale(Vec3::splat(0.1)),
            camera: Mat4::from_inverse_transform(pos, Vec3::ONE, rot),
            light: vec3(1.0, -0.9, -1.0).normalise()
        },
        &draw_parameters,
    ).unwrap();

    frame.finish().unwrap();
}).unwrap();

Recomended Drawing Abstraction

It is encouraged to make structs for abstracting drawing that is used often. A good way to structure the data is like so:

pub struct Drawer<'a> {
    pub display: &'a thin_engine::Display,
    pub vertices: &'a VertexBuffer<Vertex>,
    pub indices: &'a IndexBuffer<u32>,
    pub shader: &'a Program,
}
impl Drawerer {
    pub fn draw(&self, &mut impl glium::Surface) -> Result<(), glium::DrawError> {
        // your drawing code
    }
}

and initialising the data like this:

let drawerer = Drawer {
    pub display: &display,
    pub vertices: &vertices,
    pub indices: &indices,
    pub shader: &shader
}

this allows for reuse of the display and even shaders and meshes while still removing boilerplate

Optional Text Renderer

If you want to use the text renderer enable the text feature by adding this to your Cargo.toml

[dependencies]
thin-engine = { version = "*", features = ["text"] }

The text renderer can be setup like this

use thin_engine::{prelude::*, text_renderer::*};

let (indices, vertices, uvs) = Font::mesh(&display);
let text_shader = Font::shader(&display).unwrap();
let draw_params = DrawParameters {
    blend: glium::Blend::alpha_blending(),
    ..Default::default()
};

let mut font = Font::from_scale_and_file(40.0, "file/location").unwrap();
let text_renderer = TextRenderer {
    shader: &text_shaderm indices: &indices, vertices: &vertices,
    uv: &uvs, draw_params: &draw_params, display: &display
};

and then can be drawn in the loop like so

// draw the sentence Hello World in green text
text_renderer.draw(
    "Hello World", vec3(0.0, 1.0, 0.0), &mut frame,
    model_mat, view_mat, camera_mat, &mut font
).unwrap();

for wrapping and tab spacing of text you can use

font.format_text(text, Some(wrap), tab_indent, &display);

Dependencies

~8–23MB
~376K SLoC