use crate::input::Input;
use crate::scene::Scene;
use crate::renderer::{Renderer, RendererNewInfo};
#[cfg(feature = "imgui_integration")] use crate::imgui_handler::ImGuiHandler;
#[cfg(feature = "default_logger")] use crate::doglog;
use sdl2::event::Event;
use sdl2::event::WindowEvent;
use sdl2::keyboard::Scancode;
#[cfg(feature = "graphical_panic")]use sdl2::messagebox;
#[cfg(feature = "graphical_panic")] use std::panic::PanicInfo;
#[cfg(feature = "graphical_panic")] use backtrace::Backtrace;
#[cfg(feature = "imgui_integration")] use imgui;
use std::time::{Instant, Duration};
const MILLISECONDS_PER_NANOSECOND : f64 = 0.000001;
#[cfg(feature = "graphical_panic")]
fn panic_hook(info : &PanicInfo)
{
let message : &str = match info.payload().downcast_ref::<String>()
{
Some(s) => s,
None => match info.payload().downcast_ref::<&str>()
{
Some(s) => s,
None => "No reason was given.",
}
};
let mut message = match info.location()
{
Some(location) => format!("A panic error occurred on {}:{}: \n{}",
location.file(),
location.line(),
message),
None => format!("A panic error occurred: \n{}", message),
};
let bt = Backtrace::new();
message = format!("{}\n\n{:?}", message, bt);
gui_cli_message(&message, "Panic Error");
std::process::exit(1);
}
#[allow(unused_variables)]
pub fn gui_cli_message(message : &str, title : &str)
{
println!("{}", message);
#[cfg(feature = "graphical_panic")]
{
match messagebox::show_simple_message_box(messagebox::MESSAGEBOX_ERROR, title, &message[..], None)
{
Ok(_) => (),
Err(_) => println!("(Messagebox could not be displayed)"),
};
}
}
fn read_options() -> getopts::Matches
{
let args : Vec<String> = std::env::args().collect();
let options = getopts::Options::new();
let match_result = options.parse(&args[1..]);
match match_result
{
Ok(_) => (),
Err(fail) =>
{
let message = format!("{:?}\n{}",
fail,
options.usage("Command parsing failed. Run without options to use defaults."));
gui_cli_message(&message, "Commandline Help");
std::process::exit(1);
}
}
match_result.unwrap()
}
pub struct GameloopStopwatch
{
jiffies : Instant,
jiffies_this_frame : Duration,
jiffies_this_second : Duration,
tics_this_frame : u32,
tics_this_second : u32,
frames_this_second : u32,
in_frame : bool
}
impl GameloopStopwatch
{
pub fn new() -> GameloopStopwatch
{
GameloopStopwatch
{
jiffies : Instant::now(),
jiffies_this_frame : Duration::new(0, 0),
jiffies_this_second : Duration::new(0, 0),
tics_this_frame : 0,
tics_this_second : 0,
frames_this_second : 0,
in_frame : false
}
}
pub fn begin_frame_timing(&mut self)
{
if self.in_frame
{
return;
}
self.in_frame = true;
self.jiffies = Instant::now();
self.jiffies_this_frame = Duration::new(0, 0);
self.tics_this_frame = 0;
}
pub fn end_frame_timing(&mut self)
{
if !self.in_frame
{
return;
}
self.in_frame = false;
self.jiffies_this_frame = Instant::now() - self.jiffies;
self.tics_this_second += self.tics_this_frame;
self.jiffies_this_second += self.jiffies_this_frame;
self.frames_this_second += 1;
}
pub fn tic(&mut self)
{
self.tics_this_frame += 1;
}
pub fn new_second(&mut self)
{
self.jiffies_this_second = Duration::new(0, 0);
self.tics_this_second = 0;
self.frames_this_second = 0;
}
pub fn jiffies_this_frame(&self) -> Duration
{
if self.in_frame
{
return Instant::now() - self.jiffies;
}
self.jiffies_this_frame
}
pub fn jiffies_this_second(&self) -> Duration
{
self.jiffies_this_second
}
pub fn tics_this_frame(&self) -> u32
{
self.tics_this_frame
}
pub fn tics_this_second(&self) -> u32
{
self.tics_this_second
}
pub fn frames_this_second(&self) -> u32
{
self.frames_this_second
}
pub fn average_ms(&self) -> f64
{
self.jiffies_this_second.as_nanos() as f64 * MILLISECONDS_PER_NANOSECOND / self.frames_this_second as f64
}
}
pub struct GameloopControl
{
target_framerate : f64,
frame_interpolation : bool,
input : Input,
next_scene : Box<Scene>,
next_scene_waiting : bool,
#[cfg(feature = "imgui_integration")]
imgui_handler : ImGuiHandler
}
impl GameloopControl
{
pub fn new() -> Result<GameloopControl, String>
{
#[cfg(feature = "imgui_integration")]
let imgui_handler = unwrap_result_or_return_error!(ImGuiHandler::new(), "Could not create imgui");
Ok(GameloopControl
{
target_framerate : 60.0,
frame_interpolation : false,
input : Input::new(),
next_scene : Box::new(Scene::new()),
next_scene_waiting : false,
#[cfg(feature = "imgui_integration")]
imgui_handler
})
}
pub fn target_framerate(&self) -> f64
{
self.target_framerate
}
pub fn set_target_framerate(&mut self, new_framerate : f64)
{
if new_framerate < 1.0 || new_framerate >= 1000.0
{
panic!("Target framerate must be between 1 and 1000.");
}
self.target_framerate = new_framerate;
}
pub fn frame_interpolation(&self) -> bool
{
self.frame_interpolation
}
pub fn set_frame_interpolation(&mut self, enabled : bool)
{
self.frame_interpolation = enabled;
}
pub fn input(&self) -> &Input
{
&self.input
}
pub fn input_mut(&mut self) -> &mut Input
{
&mut self.input
}
#[cfg(feature = "imgui_integration")]
pub fn imgui(&self) -> &imgui::ImGui
{
self.imgui_handler.imgui()
}
#[cfg(feature = "imgui_integration")]
pub fn imgui_mut(&mut self) -> &mut imgui::ImGui
{
self.imgui_handler.imgui_mut()
}
pub fn goto_constructed_scene(&mut self, next_scene : Box<Scene>)
{
self.next_scene = next_scene;
self.next_scene_waiting = true;
}
}
struct GameloopTimeState
{
jiffies : Instant,
last_jiffies : Instant,
last_fps_jiffies : Instant,
jiffy_queue : Duration,
target_frametime : Duration,
lag_this_second : u32,
thought_yet : bool,
think_timer : GameloopStopwatch,
draw_timer : GameloopStopwatch,
wait_timer : GameloopStopwatch,
present_timer : GameloopStopwatch
}
impl GameloopTimeState
{
fn new() -> GameloopTimeState
{
let jiffies = Instant::now();
let last_jiffies = jiffies;
let last_fps_jiffies = Instant::now();
let jiffy_queue = Duration::new(0, 0);
let target_frametime = Duration::new(0, 0);
GameloopTimeState
{
jiffies,
last_jiffies,
last_fps_jiffies,
jiffy_queue,
target_frametime,
lag_this_second : 0,
thought_yet : false,
think_timer : GameloopStopwatch::new(),
draw_timer : GameloopStopwatch::new(),
wait_timer : GameloopStopwatch::new(),
present_timer : GameloopStopwatch::new()
}
}
fn new_second(&mut self)
{
self.think_timer.new_second();
self.draw_timer.new_second();
self.wait_timer.new_second();
self.present_timer.new_second();
self.last_fps_jiffies += Duration::from_secs(1);
self.lag_this_second = 0;
}
fn update_jiffies(&mut self)
{
self.jiffies = Instant::now();
self.jiffy_queue += self.jiffies - self.last_jiffies;
self.last_jiffies = self.jiffies;
}
fn sleep(&mut self, wait_duration : Duration)
{
while self.jiffy_queue <= wait_duration
{
self.update_jiffies();
if self.jiffy_queue <= wait_duration - Duration::from_millis(1)
{
std::thread::sleep(Duration::from_millis(1));
}
}
}
}
pub struct Gameloop
{
#[allow(dead_code)]
sdl_context : sdl2::Sdl,
event_pump : sdl2::EventPump,
renderer : Renderer,
control : GameloopControl,
current_scene : Box<Scene>,
time : GameloopTimeState
}
impl Gameloop
{
pub fn new(package_name : &str, friendly_name : &str) -> Result<Gameloop, String>
{
#[cfg(feature = "default_logger")]
{
let mut logger = doglog::DogLog::new();
logger.add_package_filter(String::from(package_name));
if let Err(_) = doglog::DogLog::init(logger, log::LevelFilter::Debug)
{
eprintln!("Could not initialize default logger.");
}
}
info!("🐶 KEESHOND Game Engine 🐶");
#[cfg(feature = "graphical_panic")]
std::panic::set_hook(Box::new(panic_hook));
let _matches = read_options();
let version = sdl2::version::version();
info!("Starting SDL version {}", version);
let sdl_context = unwrap_result_or_return_error!(sdl2::init(), "Could not initialize SDL");
let event_pump = unwrap_result_or_return_error!(sdl_context.event_pump(), "Could not initialize SDL event pump");
let video_subsystem = unwrap_result_or_return_error!(sdl_context.video(), "Could not initialize SDL video subsystem");
#[allow(unused_mut)]
let mut control = unwrap_result_or_return_error!(GameloopControl::new(), "Could not initialize gameloop control");
let renderer_new_info = RendererNewInfo
{
video_subsystem,
window_title : friendly_name,
#[cfg(feature = "imgui_integration")]
imgui : control.imgui_mut()
};
let renderer = unwrap_result_or_return_error!(Renderer::new(renderer_new_info), "Could not initialize renderer");
Ok(Gameloop
{
sdl_context,
event_pump, renderer,
control,
current_scene : Box::new(Scene::new()),
time : GameloopTimeState::new()
})
}
pub fn control(&self) -> &GameloopControl
{
&self.control
}
pub fn control_mut(&mut self) -> &mut GameloopControl
{
&mut self.control
}
pub fn set_power_management_enabled(&mut self, enabled : bool)
{
self.renderer.set_power_management_enabled(enabled);
}
fn update_input(&mut self)
{
let event_pump = &self.event_pump;
let mut callback = | key : &str |
{
let scancode = unwrap_option_or_return!(Scancode::from_name(key), false);
event_pump.keyboard_state().is_scancode_pressed(scancode)
};
self.control.input_mut().check_and_update_state(&mut callback);
}
fn think(&mut self)
{
self.time.think_timer.begin_frame_timing();
let target_ms = 1000.0 / self.control.target_framerate();
self.time.target_frametime = Duration::from_nanos((target_ms * 1_000_000.0) as u64);
while self.time.jiffy_queue > self.time.target_frametime
{
if (self.time.think_timer.jiffies_this_frame()) > self.time.target_frametime
{
while self.time.jiffy_queue > self.time.target_frametime
{
self.time.jiffy_queue -= self.time.target_frametime;
self.time.lag_this_second += 1;
}
break;
}
self.update_input();
self.current_scene.think(&mut self.control);
self.time.think_timer.tic();
self.time.jiffy_queue -= self.time.target_frametime;
self.time.thought_yet = true;
}
self.time.think_timer.end_frame_timing();
}
fn draw(&mut self)
{
self.time.draw_timer.begin_frame_timing();
if self.time.thought_yet
{
let mut interpolation_amount = 0.0;
if self.control.frame_interpolation
{
interpolation_amount = (self.time.jiffy_queue.as_nanos() as f64
/ self.time.target_frametime.as_nanos() as f64) - 1.0;
}
self.renderer.draw(&mut self.current_scene, interpolation_amount as f32);
}
self.renderer.flush();
#[cfg(feature = "imgui_integration")]
{
self.update_imgui();
self.renderer.flush();
}
self.time.draw_timer.end_frame_timing();
}
#[cfg(feature = "imgui_integration")]
fn update_imgui(&mut self)
{
let frametime = self.time.target_frametime * self.time.think_timer.tics_this_frame();
self.control.imgui_handler.update_cursor(&mut self.sdl_context.mouse());
let frame_delta = frametime.as_nanos() as f64 * MILLISECONDS_PER_NANOSECOND / 1000.0;
let (width, height) = self.renderer.window_size();
let ui = self.control.imgui_mut().frame(imgui::FrameSize::new(width as f64, height as f64, 1.0), frame_delta as f32);
self.current_scene.imgui_think(&ui);
self.renderer.control().draw_imgui(ui);
}
fn wait(&mut self)
{
self.time.wait_timer.begin_frame_timing();
if !self.control.frame_interpolation
{
self.time.sleep(self.time.target_frametime);
}
else
{
self.time.update_jiffies();
}
self.time.wait_timer.end_frame_timing();
}
fn present(&mut self)
{
self.time.present_timer.begin_frame_timing();
self.renderer.present();
self.time.present_timer.end_frame_timing();
}
fn update_fps(&mut self)
{
if self.time.jiffies < self.time.last_fps_jiffies + Duration::from_secs(1)
{
return;
}
debug!("{} ents | FPS: {} / {} | avg ms: think {:.2} draw {:.2} present {:.2} wait {:.2}",
self.current_scene.entity_count(),
self.time.draw_timer.frames_this_second(),
self.time.think_timer.tics_this_second(),
self.time.think_timer.average_ms(),
self.time.draw_timer.average_ms(),
self.time.present_timer.average_ms(),
self.time.wait_timer.average_ms());
if self.time.lag_this_second > 0
{
warn!("Too much CPU work, game is slowing down!");
}
self.time.new_second();
}
pub fn run(&mut self)
{
self.time = GameloopTimeState::new();
'runloop: loop
{
for event in self.event_pump.poll_iter()
{
match event
{
Event::Quit {..} =>
{
break 'runloop;
},
Event::Window { win_event, .. } => match win_event
{
WindowEvent::SizeChanged {..} =>
{
self.renderer.update_view();
}
_ => ()
}
_ => ()
}
#[cfg(feature = "imgui_integration")]
self.control.imgui_handler.handle_event(event);
}
self.think();
self.draw();
self.wait();
self.present();
self.update_fps();
if self.control.next_scene_waiting
{
std::mem::swap(&mut self.current_scene, &mut self.control.next_scene);
self.time = GameloopTimeState::new();
self.control.next_scene_waiting = false;
}
}
}
}