use std::{ collections::HashMap, time::Instant, }; use anyhow::Result; use maplit::hashmap; use opengl_rust::{ prelude::*, gl_state::*, physics::PhysicsBody, renderable_model::{ attributes, RenderableModel, }, shader_closure::ShaderLookup, texture::Texture, }; mod graphics; mod virtual_gamepad; use graphics::Graphics; use virtual_gamepad::VirtualGamepad; pub struct GameState { logic: LogicState, static_level: StaticLevel, } impl GameState { fn step (&mut self, phys_params: &opengl_rust::physics::Params, p_gp: VirtualGamepad) -> LogicStepOutput { self.logic.step (&self.static_level, phys_params, p_gp) } } struct StaticLevel { tris: Vec , } #[derive (Clone, Default)] struct LogicState { player: PhysicsBody, player_jump_vec: Option , } #[derive (Clone, Copy, Default)] struct LogicStepOutput { reset_level: bool, } impl LogicState { fn reset_level (&mut self, level: &LoadedLevel) { self.player = Default::default (); self.player.pos = level.player_spawn; } fn step (&mut self, static_level: &StaticLevel, phys_params: &opengl_rust::physics::Params, p_gp: VirtualGamepad) -> LogicStepOutput { let player_acc = 1.0; let player_acc_air = 0.125; let player_max_speed = 4.0; let player_max_speed_air = 8.0; let player_jump_speed = 12.0; let kill_z = -3.0; let mut output = LogicStepOutput::default (); let mut wanted_dir = Vec3::default (); if p_gp.d_left.any_press () { wanted_dir.x -= 1.0; } if p_gp.d_right.any_press () { wanted_dir.x += 1.0; } if p_gp.d_up.any_press () { wanted_dir.y += 1.0; } if p_gp.d_down.any_press () { wanted_dir.y -= 1.0; } let wanted_dir = if wanted_dir.length_squared () >= 1.0 { wanted_dir.normalize () } else { wanted_dir }; let wanted_dir = wanted_dir; let old_vel = self.player.vel; let old_vel_2 = old_vel * Vec3::new (1.0, 1.0, 0.0); let new_vel_2 = match self.player_jump_vec.as_ref () { Some (v) => { // Ground let acc = player_acc * v.z * v.z; let wanted_vel = wanted_dir * player_max_speed; let diff = wanted_vel - old_vel_2; if diff.length_squared () < acc * acc { // We're near the wanted velocity, so snap to it wanted_vel } else { // We're not near the wanted velocity, so accelerate old_vel_2 + diff.normalize_or_zero () * acc } }, // Air None => { let proposed_vel = old_vel_2 + wanted_dir * player_acc_air; if old_vel_2.length_squared () < player_max_speed_air * player_max_speed_air { // Air control is normal below player_max_speed proposed_vel } else { // If the player's input would push them beyond player_max_speed, // apply drag to preserve overall energy proposed_vel * old_vel_2.length () / proposed_vel.length () } }, }; self.player.vel = new_vel_2; self.player.vel.z = old_vel.z; // dbg! (self.player.vel); if p_gp.jump.pressed { if let Some (normal) = self.player_jump_vec.clone () { self.player.vel.z = 0.0; self.player.vel += normal * player_jump_speed; } } let phys_result = opengl_rust::physics::step (&phys_params, &static_level.tris, &[], 0.5, &self.player); self.player = phys_result.body; if self.player.pos.z < kill_z { output.reset_level = true; } // tracing::debug! ("player pos: {}", self.player.pos); self.player_jump_vec = None; for normal in &phys_result.normals_hit { self.player_jump_vec = Some (match self.player_jump_vec { None => *normal, Some (old) => { if normal.z > old.z { *normal } else { old } }, }); } output } } #[tokio::main] async fn main () -> Result <()> { tracing_subscriber::fmt::fmt () .with_env_filter (tracing_subscriber::EnvFilter::from_default_env()) .with_span_events (tracing_subscriber::fmt::format::FmtSpan::CLOSE) .init (); let sdl_context = sdl2::init ().map_err (|e| anyhow! ("Can't init SDL: {}", e))?; let video_subsystem = sdl_context.video ().map_err (|e| anyhow! ("Can't get SDL video subsystem: {}", e))?; let window = video_subsystem.window ("3D platformer", 320 * 2, 240 * 2) .position_centered () .opengl () .build () ?; gl::load_with (|s| { video_subsystem.gl_get_proc_address (s) as *const _ }); if ! gl::ClearColor::is_loaded () { bail! ("gl::ClearColor didn't load, something is wrong with OpenGL"); } let gl_ctx = window.gl_create_context ().map_err (|e| anyhow! ("Can't create OpenGL context: {}", e))?; window.gl_make_current (&gl_ctx).map_err (|e| anyhow! ("Can't make OpenGL context current: {}", e))?; let mut event_pump = sdl_context.event_pump ().unwrap (); let mut time_step = TimeStep::new (60, 1000); let level = LoadedLevel::from_path ("gltf/level-00.glb")?; let scene = level.level.scenes ().next ().unwrap (); let mut camera = None; let mut phys_tris: Vec = Vec::new (); for node in scene.nodes () { if let Some (c) = node.camera () { camera = Some (Camera { transform: node.transform ().clone (), }); } let mesh = match node.mesh () { None => continue, Some (x) => x, }; let m = gltf_node_get_mat4 (&node); for prim in mesh.primitives () { use gltf::Semantic; let positions = match prim.get (&Semantic::Positions) { None => continue, Some (x) => x, }; let pos_view = match positions.view () { None => continue, Some (x) => x, }; let indices = match prim.indices () { None => continue, Some (x) => x, }; let indices_view = match indices.view () { None => continue, Some (x) => x, }; let idx_start = indices.offset () + indices_view.offset (); let idx_slice = &level.buffer [idx_start..idx_start + indices.count () * 2]; let vert_start = positions.offset () + pos_view.offset (); let vert_slice = &level.buffer [vert_start..vert_start + positions.count () * 4 * 3]; for chunk in idx_slice.chunks_exact (2 * 3) { let read_idx = |i: usize| u16::from_le_bytes ([chunk [i * 2 + 0], chunk [i * 2 + 1]]); let idxs = [ read_idx (0), read_idx (1), read_idx (2), ]; let read_pos_coord = |i| f32::from_le_bytes ([ vert_slice [i * 4 + 0], vert_slice [i * 4 + 1], vert_slice [i * 4 + 2], vert_slice [i * 4 + 3], ]); let read_pos = move |i| { let i = usize::try_from (i).unwrap (); m.transform_point3 (Vec3::new ( read_pos_coord (i * 3 + 0), read_pos_coord (i * 3 + 1), read_pos_coord (i * 3 + 2), )) }; // glTF triangle winding is backwards from what I expected // no biggie let verts = [ read_pos (idxs [0]), read_pos (idxs [2]), read_pos (idxs [1]), ]; phys_tris.push (opengl_rust::physics::Triangle { verts, }); } } } let camera = match camera { None => bail! ("Couldn't find camera node in glTF file"), Some (x) => x, }; let mut graphics = Graphics::new (); let mut gl_state = Default::default (); let mut game_state = GameState { logic: Default::default (), static_level: StaticLevel { tris: phys_tris, }, }; let phys_params = opengl_rust::physics::Params { dt: 1.0 / 60.0, gravity: (0.0, 0.0, -0.5).into (), margin: 0.00125, }; game_state.logic.reset_level (&level); let mut next_upf_print = 60; let mut last_upf_instant = Instant::now (); 'running: loop { let _frames_to_do = time_step.step (); let mut player_gamepad = virtual_gamepad::VirtualGamepad::default (); for event in event_pump.poll_iter () { match event { Event::Quit {..} | Event::KeyDown { keycode: Some (Keycode::Escape), .. } => { break 'running }, Event::KeyDown { keycode: Some (Keycode::R), .. } => { game_state.logic.reset_level (&level); }, Event::KeyDown { scancode: Some (Scancode::Space), repeat: false, .. } => { player_gamepad.jump.pressed = true; }, Event::KeyDown { scancode: Some (Scancode::Left), repeat: false, .. } => { player_gamepad.d_left.pressed = true; }, Event::KeyDown { scancode: Some (Scancode::Right), repeat: false, .. } => { player_gamepad.d_right.pressed = true; }, Event::KeyDown { scancode: Some (Scancode::Up), repeat: false, .. } => { player_gamepad.d_up.pressed = true; }, Event::KeyDown { scancode: Some (Scancode::Down), repeat: false, .. } => { player_gamepad.d_down.pressed = true; }, _ => (), } } { let kb_state = event_pump.keyboard_state (); if kb_state.is_scancode_pressed (Scancode::Space) { player_gamepad.jump.held = true; } if kb_state.is_scancode_pressed (Scancode::Left) { player_gamepad.d_left.held = true; } if kb_state.is_scancode_pressed (Scancode::Right) { player_gamepad.d_right.held = true; } if kb_state.is_scancode_pressed (Scancode::Up) { player_gamepad.d_up.held = true; } if kb_state.is_scancode_pressed (Scancode::Down) { player_gamepad.d_down.held = true; } } let p_gp = player_gamepad; let logic_step_output = game_state.step (&phys_params, p_gp); if logic_step_output.reset_level { game_state.logic.reset_level (&level); } // dbg! (game_state.logic.player.pos); window.gl_make_current (&gl_ctx).unwrap (); graphics.draw (&game_state, &mut gl_state, &level, &camera); graphics.frames += 1; if graphics.frames == next_upf_print { let now = Instant::now (); let upf = (now - last_upf_instant).as_micros () / 60; dbg! (upf); next_upf_print += 60; last_upf_instant = now; } window.gl_swap_window (); tokio::time::sleep (Duration::from_millis (10)).await; } Ok (()) } struct LoadedLevel { player_spawn: Vec3, buffer: Vec , level: gltf::Document, } impl LoadedLevel { fn from_path (path: &str) -> Result { use gltf::{ Semantic, scene::Transform, }; let (level, buffers, _) = gltf::import (path)?; let buffer = match buffers.get (0) { None => bail! ("gltf didn't load any buffers"), Some (x) => x.0.to_vec (), }; let scene = match level.scenes ().next () { None => bail! ("No scenes in glTF file"), Some (x) => x, }; let mut player_spawn = None; for node in scene.nodes () { if node.name () == Some ("Player Spawn") { let (translation, _, _) = node.transform ().decomposed (); player_spawn = Some (Vec3::from (translation)); } else if let Some (camera) = node.camera () { } else if let Some (mesh) = node.mesh () { for (i, prim) in mesh.primitives ().enumerate () { let positions = match prim.get (&Semantic::Positions) { None => continue, Some (x) => x, }; let normals = match prim.get (&Semantic::Normals) { None => continue, Some (x) => x, }; let indices = match prim.indices () { None => continue, Some (x) => x, }; let pos_view = match positions.view () { None => continue, Some (x) => x, }; let norm_view = match normals.view () { None => continue, Some (x) => x, }; let indices_view = match indices.view () { None => continue, Some (x) => x, }; } } } let player_spawn = match player_spawn { None => bail! ("glTF file must have `Player Spawn` node"), Some (x) => x, }; Ok (Self { buffer, level, player_spawn, }) } } struct ShaderLocations { attr_pos: u32, attr_normal: u32, attr_color: u32, uni_mvp: i32, } impl ShaderLocations { pub fn new (shader_program: &opengl_rust::shader::ShaderProgram) -> anyhow::Result { let attr = |name: &str| shader_program.get_attribute_location (&CString::new (name.as_bytes ())?).try_into ().context ("Attribute location negative"); let uni = |name: &str| shader_program.get_uniform_location (&CString::new (name.as_bytes ())?).try_into ().context ("Uniform location bad"); Ok (Self { attr_pos: attr ("attr_pos")?, attr_normal: attr ("attr_normal")?, attr_color: attr ("attr_color")?, uni_mvp: uni ("uni_mvp")?, }) } } struct TriangleStream { pub verts: gpu_buffers::VertexBuffer, pub indices: gpu_buffers::IndexBuffer, } struct Camera { transform: gltf::scene::Transform, } fn gltf_node_get_mat4 (node: &gltf::Node) -> Mat4 { Mat4::from_cols_array_2d (&node.transform ().matrix ()) }