A self imposed game jam trip report
I started off building big, learning slowly
Learning out they're bad in 24 hours
is much cheaper than 3 months
Might be the best game jam language in existence
Can you make it "Game Jam" friendly though?
Can we create a layer of machine inefficient,
developer productivity based shortcuts?
Something you start with,
and phase out when you need speed ...
Glium & Glutin overview
Game jam style engine design
A smidge of game design
Building a tiny game
I read https://doc.rust-lang.org/book/ beforehand
I only counted development.
There were a few hours planning with what to build.
24:00
Safe OpenGL without the state machine.
target.draw(
&vertex_buffer,
&index_buffer,
&program,
&uniforms,
&Default::default()
).unwrap();
target.draw(
&vertex_buffer,
&index_buffer,
&program,
&uniforms,
&Default::default()
).unwrap();
What we're drawing on (window, or off screen)
target.draw(
&vertex_buffer,
&index_buffer,
&program,
&uniforms,
&Default::default()
).unwrap();
Vertices that we want to draw (usually triangles)
target.draw(
&vertex_buffer,
&index_buffer,
&program,
&uniforms,
&Default::default()
).unwrap();
A pair of GLSL programs/"shaders"
Vertices + Uniforms in -> Coloured pixels out
void main() {
gl_Position = matrix * vec4(position, 1.0);
v_color = color * u_color;
v_tex_coord = tex_coord;
v_normal = normal;
}
target.draw(
&vertex_buffer,
&index_buffer,
&program,
&uniforms,
&Default::default()
).unwrap();
Shader program state
(textures, colors, camera transforms)
let uniforms = uniform! {
matrix: mvp_raw,
u_texture_array: texture,
u_color: color,
u_alpha_minimum: 0.05_f32,
u_sun_direction: adjusted_sun_direction_raw,
};
Pretend the (!) isn't there and that it's just a struct.
That ! is saving you from 24 years of OpenGL's legacy
When you see a ! you're exchanging a few hours of runtime trial and error for a runtime error on program start
Uniforms
glGetUniformLocation(id,u.name)
glActiveTexture(GL_TEXTURE0 + n)
glBindTexture(tu.target, 0)
glUniformMatrix4(id, matrix)
Shaders
glBindAttribLocation(id, n, a.name)
glVertexAttribPointer(n, a.components, a.componentType.glCode,
a.normalized, strideBytes, at)
glEnableVertexAttribArray(n)
glDisableVertexAttribArray(n)
Becomes
thread '<main>' panicked at 'The program attribute `position` is missing in the vertex bindings', /Users/michael/.cargo/registry/src/github.com-1ecc6299db9ec823/ glium-0.15.0/src/vertex_array_object.rs:287 note: Run with `RUST_BACKTRACE=1` for a backtrace.
Demand a core profile
let display = WindowBuilder::new()
.with_gl_profile(GlProfile::Core)
.with_gl(GlRequest::Specific(Api::OpenGl,(4,0)))
.build_glium().unwrap();
Make your shaders 3.3+
#version 330
Glium examples are really easy to run (steal them)
cargo run --example tutorial-14
Print input events to get familiar
for event in display.poll_events() {
match event {
glutin::Event::Closed => return support::Action::Stop,
e => println!("got {:?}", e)
}
}
Cross platform:
23:00
An hour spent learning Rust's module system
You know what I said about stealing everything ...
Throw it out and add it back line by line
lib.rs
#![crate_name="gm2"]
#![allow(dead_code)]
#[macro_use]
extern crate glium;
pub mod core {
pub mod camera;
pub mod render;
pub mod shader;
pub mod game;
}
pub mod game;
pub mod input;
module directives form a tree
Named pointers, not namespaces
Halfway between namespaces & include directives
22:00
An hour spent battling glium Types
pub fn build_window() ->
glium::backend::glutin_backend::GlutinFacade
All the examples are one function, so there's no examples of:
pub fn render(display: &glium::Display, rs:&RenderState)
I'm pretty sure I shouldn't be passing around a GlutinFacade.
An hour later I find ...
pub use backend::glutin_backend::GlutinFacade as Display;
Because I'm new at Rust :-/
21:00
2D sprites positioned in 3D (more than Z order)
Quads only, in one vertex format
At most a few batches per frame
Dev performance, not machine performance
"Flash" guys in the middle with per vertex colors
#[derive(Copy, Clone)]
pub struct PTCNVertex {
pub position: [f32; 3],
pub tex_coord: [f32; 3],
pub color: [f32; 4],
pub normal: [f32; 3],
}
implement_vertex!(PTCNVertex, position, tex_coord, color, normal);
color allows us to flash guys white if they get hit
or red if they're hurt
normal allows us to add lighting if we have time
glium::VertexBuffer::new(display,
&[
PTCNVertex { position: [-0.5, -0.5, 0.0], tex_coord: [1.0, 0.0, 1.0],
color: [0.0, 1.0, 0.0, 1.0], normal: [0.0, 1.0, 0.0] },
PTCNVertex { position: [0.0, 0.5, 0.0], tex_coord: [1.0, 0.0, 1.0],
color: [0.0, 0.0, 1.0, 1.0], normal: [0.0, 1.0, 0.0] },
PTCNVertex { position: [0.5, -0.5, 0.0], tex_coord: [1.0, 0.0, 1.0],
color: [1.0, 0.0, 0.0, 1.0], normal: [0.0, 1.0, 0.0] },
]
).unwrap()
(Poorly) recoloured sprites for different teams
Great for sprites
Run out of space?
Add another layer
Different sides/factions?
Add layers with recoloured versions
The last image library you'll need
let image = image::open(&Path::new("img/tiles.png")).unwrap().to_rgba();
let image_dimensions = image.dimensions();
let image_raw = texture::RawImage2d::from_raw_rgba_reversed(
image.into_raw(),
image_dimensions
);
let texture_array = texture::Texture2dArray::new(
display,
vec![image_raw]
).unwrap();
OpenGL doesn't have quads
We can just duplicate bottom-left and top-right vertices
pub struct QuadTesselator<T> {
vertices: Vec<T>,
}
impl<T : Copy> QuadTesselator<T> {
pub fn add_quad(&mut self, ts:[T; 4]) {
for ele in ts.iter() {
self.vertices.push(*ele);
}
self.vertices.push(ts[0]);
self.vertices.push(ts[2]);
}
}
*Developer efficient, embrace #Derive(Copy)
20:00
"Smart" quad tesselator understands desired pixel scale and texture sizes to give consistent pixel density
for x in 0..16 {
for z in 0..16 {
tesselator.draw_floor_tile(&ground_tile, 0,
x as f64, 0.0, z as f64, 0.0, false);
}
}
tesselator.draw_wall_base_anchored_at(&man, 0,
Vec3::new(1.5, 0.0, 1.5), 0.0, false);
tesselator.draw_floor_centre_anchored_at(&man_shadow, 0,
Vec3::new(1.5, 0.0, 1.5), 0.01, false);
if let &Some(its) = intersection {
let x = round_down(its.x);
let z = round_down(its.z);
tesselator.draw_floor_tile(&indicator, 0,
x as f64, 0.0, z as f64, 0.02, false);
}
18:00
All the vecs, mats, dots, crosses and inverts you need for a game
pub fn projection(zoom:f64,
width:u32,
height:u32,
pixels_per_unit: f64) -> Mat4 {
let effective_width = (width as f64) / (zoom * pixels_per_unit);
let effective_height = (height as f64) / (zoom * pixels_per_unit)
/ (2.0_f64).sqrt();
let half_width = effective_width / 2.0;
let half_height = effective_height / 2.0;
cgmath::ortho(-half_width, half_width,
-half_height, half_height,
-100.0, 100.0
)
}
let draw_parameters = glium::DrawParameters {
depth: glium::Depth {
test: glium::draw_parameters::DepthTest::IfLess,
write: true,
.. Default::default()
},
blend: glium::Blend::alpha_blending(),
.. Default::default()
};
16:00
Assumed my camera creation was wrong, and spent an hour debugging it
Example shader had matrix multiplication reversed
gl_Position = vec4(position, 1.0) * matrix;
gl_Position = matrix * vec4(position, 1.0);
15:00
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyState {
pub down: HashSet<glutin::VirtualKeyCode>,
pub pushed: HashSet<glutin::VirtualKeyCode>, // pushed this frame
pub released: HashSet<glutin::VirtualKeyCode>, // released this frame
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InputState {
pub mouse:MouseState,
pub keys:KeyState,
}
pub fn produce(input:&InputState, events: &Vec<glutin::Event>)
-> InputState
We need to remember a few things between frames
From windowed mouse coordinates to world objects
pub fn ray_for_mouse_position(&self, x:i32, y:i32)
-> Option<camera::Line>
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Line {
pub from: Vec3,
pub to: Vec3,
}
impl Line {
pub fn intersects(&self, plane:Plane) -> Option<Vec3>
In our main loop
let new_input_state = input::produce(&input_state, &evs);
let (mouse_x, mouse_y) = input_state.mouse.at;
let line = render_state.ray_for_mouse_position(mouse_x, mouse_y);
intersection = line.and_then(|l| l.intersects(ground_plane) );
12:00
Tile laying
&
Mountain climbing
Good? Evil? You're the big stone head
That's just lemmings
That's just weird tower defense
> 1 You Lose ... < 1 You Lose
Who's the chosen one?
Clearly the guy that made it
Climbing is dangerous
11:45
9:00
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
Simple & Inefficient
Why borrow when you can copy? (don't answer that)
This is not idiomatic Rust, these are the fever dreams of a Scala developer who can't even remember what life is like without a garbage collector
pub type WorldGrid = Vec<Vec<TileId>>;
pub struct World {
pub tick: Tick,
pub size: Vec2i,
pub tiles: WorldGrid,
pub climbers_by_tile : multimap::MultiMap<Vec2i, u32>,
pub climbers_by_id: HashMap<u32, Climber>,
}
pub fn advance_world<R : Rng>(world:&World, tiles: &Tiles, rng: &mut R)
-> World
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Tile {
pub name: String,
pub id: TileId,
pub nodes: Vec<InnerBlockLocation>, // connected
}
for x in 0..size.x {
for y in 0..size.y {
let tile = &tiles.safe[rng.gen_range(0, tile_count)];
let tile_id = tile.id;
placed_tiles[x][y] = place(tile_id);
}
}
placed_tiles[0][0] = place(spawner.id);
placed_tiles[1][0] = place(flat.id);
placed_tiles[rng.gen_range(0, size.x)][top_row] = place(stone_head.id);
for x in 0..(world_size.x as usize) {
for y in 0..(world_size.y as usize) {
let tile_id = game_state.world.tiles[x][y].id as usize;
let texture_region = &rs.tile_renderers[tile_id];
tesselator.draw_wall_tile(&texture_region, 0, x, y, 0.0, 0.0, false);
tesselator.draw_wall_tile(&texture_region, 1, x, y, 0.0, 0.1, false);
tesselator.draw_wall_tile(&texture_region, 2, x, y, 0.0, 0.2, false);
}
}
7:00
// place tile
if let (Some(is), true) = (intersection,
new_input_state.mouse.left_pushed()) {
// can't place on top of climbers
if game_state.world.can_place_at(&tiles, is) {
if let Some((tile_id, _)) = game_state.tile_queue.pop_front() {
game_state.world.tiles[is.x as usize][is.y as usize] =
place(tile_id);
tile_placement.play(); // sound
}
}
}
5:00
https://github.com/jhasse/ears
let mut tile_placement = Sound::new("snd/place_tile.ogg").unwrap();
tile_placement.play();
4:00
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Climber {
pub id: UniqGameId,
pub prev: TimedLocation,
pub next: TimedLocation,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct TimedLocation {
pub loc: Vec2i,
pub inner_loc: Vec2i,
pub at: Tick,
}
impl TimedLocation {
pub fn exact_location(&self, z:f64) -> Vec3
impl Climber {
pub fn exact_location_at(&self, tick:Tick, z:f64) -> Vec3 {
let time = clamp(tick, self.prev.at, self.next.at);
let action_duration = self.next.at - self.prev.at;
let progress = if action_duration == 0 {
1.0
} else {
(time - self.prev.at) as f64 / action_duration as f64
};
lerp(self.prev.exact_location(z),
self.next.exact_location(z),
progress);
2:00
pub fn travellable_locations(&self, from:Vec2i, tiles:&Tiles)
-> Vec<(Vec2i, Vec2i)> {
let from_tile = tiles.with_id(self.tile_at(from).id);
self.adjacent_locations(from).into_iter().filter_map( |tl| {
let to_tile = tiles.with_id(self.tile_at(tl).id);
can_travel(from, from_tile, tl, to_tile).map(|il| (tl, il))
}).collect()
}
00:00
Eric Skiff
pub fn smooth_3d(from:Vec3, to:Vec3, velocity:Vec3, smooth_time:f64,
time_delta:f64) -> (Vec3, Vec3) {
let omega = 2.0 / smooth_time;
let x = omega * time_delta;
let exp = 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x);
let change = from - to;
let tmp = (velocity + change * omega) * time_delta;
let new_velocity = (velocity - omega * tmp) * exp;
(to + (change + tmp) * exp, new_velocity)
}
Attach them to everything
let mut render_pos = Vec3::new(pos.x, pos.y, -8.0 + pos.z);
render_pos -= rs.camera_target.position *
pos.z * 0.25; // moved based on camera
tesselator.draw_wall_centre_anchored_at(&cloud_renderer,
0, render_pos, 0.0, false)
let seed = ((climber.id as f64 * 1732.0) % 17.0) / 17.0;
let up = ((climber_state.walk_progress + seed) % 0.05) < 0.025;
if up {
pos.y += 0.02;
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClimberRenderState {
pub spring: SpringState3,
pub walk_progress: f64,
}
let mut walk_sound = Sound::new("snd/walk.ogg").unwrap();
walk_sound.set_looping(true);
walk_sound.set_volume(0.0);
walk_sound.play();
loop {
let mut total_vel = 0.0_f64;
for (_,css) in &render_state.entity_springs {
total_vel += css.spring.velocity.magnitude();
}
let volume = total_vel.log(2.0) / 4.0;
walk_sound.set_volume(clamp(volume as f32, 0.0, 0.45));
let depth_offset = seed * 0.20 + 0.11;
Thanks for listening
Engine source is up at
https://github.com/MichaelShaw/rust-game-24h
Game is up at
https://github.com/MichaelShaw/grom