use crate::input::Input;
use crate::scene::{Scene, BaseScene, SceneType, ThinkerSystem};
use crate::audio::{Audio, AlAudio, NullAudio, AudioError, Sound};
use crate::renderer::{Renderer, RendererError, RendererNewInfo, Sheet, Shader, ViewportMode};
#[cfg(feature = "imgui_base")] use crate::imgui_handler::ImGuiHandler;
#[cfg(feature = "default_logger")] use crate::doglog;
use sdl2::event::Event;
use sdl2::event::WindowEvent;
use sdl2::keyboard::Scancode;
use sdl2::keyboard::Mod as KeyboardMod;
#[cfg(feature = "graphical_panic")] use sdl2::messagebox;
#[cfg(feature = "graphical_panic")] use std::panic::PanicInfo;
#[cfg(feature = "graphical_panic")] use backtrace::Backtrace;
use keeshond_datapack::DataStore;
use keeshond_datapack::source::SourceManager;
#[cfg(feature = "imgui_base")] use imgui;
use std::time::{Instant, Duration};
use std::rc::Rc;
use std::cell::RefCell;
const MILLISECONDS_PER_NANOSECOND : f64 = 0.000001;
#[derive(Debug)]
pub enum Sdl2Subsystem
{
Main,
Event,
Video
}
#[derive(Debug, Fail)]
pub enum InitError
{
#[fail(display = "SDL {:?} subsystem: {}", _0, _1)]
Sdl2InitError(Sdl2Subsystem, String),
#[fail(display = "{}", _0)]
RendererInitError(RendererError),
#[fail(display = "{}", _0)]
AudioInitError(AudioError),
#[fail(display = "{}", _0)]
ImguiInitError(String)
}
struct PlaceholderScene
{
}
impl SceneType for PlaceholderScene
{
type SpawnableIdType = u32;
fn new() -> Self where Self : Sized { PlaceholderScene {} }
fn thinkers(&mut self, _game : &mut GameControl) -> Vec<Box<dyn ThinkerSystem<Self>>>
{
warn!("No scene loaded in gameloop!");
vec![]
}
}
#[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 cli_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();
cli_message = format!("{}\n\n{:?}", cli_message, bt);
println!("{}", &cli_message);
let gui_message = format!("A panic error occurred.\n\n{}", message);
match messagebox::show_simple_message_box(messagebox::MessageBoxFlag::ERROR, "Panic Error", &gui_message[..], None)
{
Ok(_) => (),
Err(_) => println!("(Messagebox could not be displayed)"),
};
std::process::exit(1);
}
fn print_log_section(title : &str)
{
info!("");
info!("{:-^40}", format!(" {} ", title));
info!("");
}
#[allow(unused_variables)]
pub fn gui_cli_message(message : &str, title : &str)
{
println!("{}", message);
match messagebox::show_simple_message_box(messagebox::MessageBoxFlag::INFORMATION, 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 mut options = getopts::Options::new();
options.optflag("", "help", "Show this help");
options.optflag("f", "fullscreen", "Run in fullscreen mode");
options.optflag("w", "window", "Run in windowed mode");
options.optopt("", "interpolate", "Unlock display framerate from logic and enable frame interpolation for alternate refresh rates or Variable Refresh Rate displays. Upper framerate limit is determined by FPS_LIMIT (default 120, use 0 to disable). Do *not* use with a fixed refresh rate equal to game's logic rate!", "FPS_LIMIT");
options.optflag("", "no-interpolate", "Force disable frame interpolation and lock display rate to logic rate. Use with a fixed refresh rate equal to game's refresh rate.");
options.optflag("", "kiosk", "Enable kiosk mode, which forces fullscreen and disables closing the window. Note that the user may still be able to close the program depending on your operating environment's global shortcuts.");
let match_result = options.parse(&args[1..]);
match match_result
{
Ok(matches) =>
{
if matches.opt_present("help")
{
let message = options.usage("Commandline Parameters Help");
gui_cli_message(&message, "Commandline Help");
std::process::exit(1);
}
matches
}
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);
}
}
}
fn apply_config(gameinfo : &mut GameInfo)
{
let matches = read_options();
if matches.opt_present("window")
{
gameinfo.fullscreen = false;
}
else if matches.opt_present("fullscreen")
{
gameinfo.fullscreen = true;
}
if matches.opt_present("no-interpolate")
{
gameinfo.frame_interpolation = false;
}
else if matches.opt_present("interpolate")
{
gameinfo.frame_interpolation = true;
}
let cap : f64 = matches.opt_get_default("interpolate", 120.0).unwrap_or(120.0);
if cap == 0.0
{
gameinfo.frame_interpolation_cap = None;
}
else
{
gameinfo.frame_interpolation_cap = Some(cap);
}
if matches.opt_present("kiosk")
{
gameinfo.kiosk_mode = true;
}
}
#[derive(Clone)]
pub struct GameInfo
{
pub package_name : &'static str,
pub friendly_name : &'static str,
pub base_width : u32,
pub base_height : u32,
pub default_zoom : u32,
pub texture_filtering : bool,
pub viewport_mode : ViewportMode,
pub fullscreen : bool,
pub target_framerate : f64,
pub frame_interpolation : bool,
pub frame_interpolation_cap : Option<f64>,
pub kiosk_mode : bool
}
impl Default for GameInfo
{
fn default() -> GameInfo
{
GameInfo
{
package_name : "",
friendly_name : "Untitled",
base_width : 1280,
base_height : 720,
default_zoom : 1,
texture_filtering : true,
viewport_mode : ViewportMode::Independent,
fullscreen : false,
kiosk_mode : false,
target_framerate : 60.0,
frame_interpolation : false,
frame_interpolation_cap : Some(120.0)
}
}
}
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 GameResources
{
sheet_store : DataStore<Sheet>,
shader_store : DataStore<Shader>,
sound_store : DataStore<Sound>
}
impl GameResources
{
pub fn sheets(&self) -> &DataStore<Sheet>
{
&self.sheet_store
}
pub fn sheets_mut(&mut self) -> &mut DataStore<Sheet>
{
&mut self.sheet_store
}
pub fn shaders(&self) -> &DataStore<Shader>
{
&self.shader_store
}
pub fn shaders_mut(&mut self) -> &mut DataStore<Shader>
{
&mut self.shader_store
}
pub fn sounds(&self) -> &DataStore<Sound>
{
&self.sound_store
}
pub fn sounds_mut(&mut self) -> &mut DataStore<Sound>
{
&mut self.sound_store
}
}
pub struct GameControl
{
target_framerate : f64,
frame_interpolation : bool,
interpolation_cap : Option<f64>,
input : Input,
renderer : Renderer,
audio : Box<dyn Audio>,
next_scene : Box<dyn BaseScene>,
next_scene_waiting : bool,
source_manager : Rc<RefCell<SourceManager>>,
resources : GameResources,
done : bool,
#[cfg(feature = "imgui_base")]
imgui_handler : ImGuiHandler
}
impl GameControl
{
pub fn new(gameinfo : &GameInfo, sdl_context : Option<&sdl2::Sdl>) -> Result<GameControl, InitError>
{
let source_manager = Rc::new(RefCell::new(SourceManager::new()));
let sheet_store = DataStore::new(source_manager.clone());
let shader_store = DataStore::new(source_manager.clone());
let sound_store = DataStore::new(source_manager.clone());
let default_zoom = gameinfo.default_zoom.max(1).min(8);
let mut resources = GameResources { sheet_store, shader_store, sound_store };
let mut renderer_new_info = None;
let audio : Box<dyn Audio>;
if let Some(sdl) = sdl_context
{
let video_subsystem = try_or_else!(sdl.video(),
|error| Err(InitError::Sdl2InitError(Sdl2Subsystem::Video, error)));
audio = Box::new(try_or_else!(AlAudio::new(&mut resources),
|error| Err(InitError::AudioInitError(error))));
renderer_new_info = Some(RendererNewInfo
{
width : gameinfo.base_width,
height : gameinfo.base_height,
default_zoom,
texture_filtering : gameinfo.texture_filtering,
kiosk_mode : gameinfo.kiosk_mode,
fullscreen : gameinfo.fullscreen,
viewport_mode : gameinfo.viewport_mode,
video_subsystem,
resources : &mut resources
});
}
else
{
audio = Box::new(NullAudio::new());
}
#[cfg(feature = "imgui_base")]
let mut imgui_handler = try_or_else!(ImGuiHandler::new(),
|error| Err(InitError::ImguiInitError(error)));
#[allow(unused_mut)]
let mut renderer = try_or_else!(Renderer::new(renderer_new_info),
|error| Err(InitError::RendererInitError(error)));
#[cfg(feature = "imgui_base")]
try_or_else!(renderer.control().init_imgui_renderer(imgui_handler.imgui_mut()),
|error| Err(InitError::RendererInitError(error)));
Ok(GameControl
{
target_framerate : gameinfo.target_framerate,
frame_interpolation : gameinfo.frame_interpolation,
interpolation_cap : gameinfo.frame_interpolation_cap,
input : Input::new(),
renderer,
audio,
next_scene : Box::new(Scene::<PlaceholderScene>::new()),
next_scene_waiting : false,
source_manager,
resources,
done : false,
#[cfg(feature = "imgui_base")]
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 frame_interpolation_cap(&self) -> Option<f64>
{
self.interpolation_cap
}
pub fn set_frame_interpolation_cap(&mut self, framerate : Option<f64>)
{
if let Some(cap) = framerate
{
if cap < 1.0 || cap >= 1000.0
{
panic!("Target framerate cap must be between 1 and 1000.");
}
}
self.interpolation_cap = framerate;
}
pub fn input(&self) -> &Input
{
&self.input
}
pub fn input_mut(&mut self) -> &mut Input
{
&mut self.input
}
pub fn renderer(&self) -> &Renderer
{
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer
{
&mut self.renderer
}
pub fn audio(&self) -> &Box<dyn Audio>
{
&self.audio
}
pub fn audio_mut(&mut self) -> &mut Box<dyn Audio>
{
&mut self.audio
}
fn sync_audio_store(&mut self)
{
self.audio.sync_sound_store(&mut self.resources.sound_store)
.expect("Audio backend store sync failed");
}
pub fn source_manager(&self) -> Rc<RefCell<SourceManager>>
{
self.source_manager.clone()
}
pub fn res(&self) -> &GameResources
{
&self.resources
}
pub fn res_mut(&mut self) -> &mut GameResources
{
&mut self.resources
}
#[cfg(feature = "imgui_base")]
pub fn imgui(&self) -> &imgui::Context
{
self.imgui_handler.imgui()
}
#[cfg(feature = "imgui_base")]
pub fn imgui_mut(&mut self) -> &mut imgui::Context
{
self.imgui_handler.imgui_mut()
}
pub fn goto_constructed_scene(&mut self, next_scene : Box<dyn BaseScene>)
{
self.next_scene = next_scene;
self.next_scene_waiting = true;
}
pub fn quit(&mut self)
{
self.done = true;
}
}
struct GameloopTimeState
{
jiffies : Instant,
last_jiffies : Instant,
last_fps_jiffies : Instant,
jiffy_queue : Duration,
target_frametime : Duration,
cap_frametime : Duration,
last_display : Instant,
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);
let cap_frametime = Duration::new(0, 0);
let last_display = Instant::now();
GameloopTimeState
{
jiffies,
last_jiffies,
last_fps_jiffies,
jiffy_queue,
target_frametime,
cap_frametime,
last_display,
lag_this_second : 0,
thought_yet : false,
think_timer : GameloopStopwatch::new(),
draw_timer : GameloopStopwatch::new(),
wait_timer : GameloopStopwatch::new(),
present_timer : GameloopStopwatch::new()
}
}
fn flush(&mut self)
{
while self.jiffy_queue > self.target_frametime
{
self.jiffy_queue -= self.target_frametime;
}
self.new_second();
self.jiffies = Instant::now();
self.last_jiffies = self.jiffies;
self.last_fps_jiffies = Instant::now();
self.last_display = self.jiffies;
self.thought_yet = false;
}
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));
}
}
}
fn interpolate_sleep(&mut self, wait_duration : Duration)
{
while Instant::now() - self.last_display <= wait_duration
{
}
self.last_display = Instant::now();
}
}
pub struct Gameloop
{
#[allow(dead_code)]
sdl_context : sdl2::Sdl,
event_pump : sdl2::EventPump,
control : GameControl,
current_scene : Box<dyn BaseScene>,
time : GameloopTimeState,
kiosk_mode : bool
}
impl Gameloop
{
pub fn new(gameinfo : GameInfo) -> Gameloop
{
Gameloop::try_new(gameinfo).unwrap_or_else(|error| panic!("Startup failed: {}", error))
}
pub fn try_new(gameinfo : GameInfo) -> Result<Gameloop, InitError>
{
let mut gameinfo = gameinfo.clone();
if gameinfo.package_name.is_empty()
{
panic!("Package name cannot be empty.");
}
#[cfg(feature = "default_logger")]
{
let mut logger = doglog::DogLog::new();
logger.add_package_filter(String::from(gameinfo.package_name));
if let Err(_) = doglog::DogLog::init(logger, log::LevelFilter::Debug)
{
eprintln!("Could not initialize default logger.");
}
}
#[cfg(feature = "graphical_panic")]
std::panic::set_hook(Box::new(panic_hook));
apply_config(&mut gameinfo);
print_log_section("🐶 KEESHOND Game Engine 🐶");
let version = sdl2::version::version();
info!("Starting SDL version {}", version);
let sdl_context = try_or_else!(sdl2::init(),
|error| Err(InitError::Sdl2InitError(Sdl2Subsystem::Main, error)));
let event_pump = try_or_else!(sdl_context.event_pump(),
|error| Err(InitError::Sdl2InitError(Sdl2Subsystem::Event, error)));
#[allow(unused_mut)]
let mut control = try_or_else!(GameControl::new(&gameinfo, Some(&sdl_context)), |error| Err(error));
control.renderer_mut().set_base_size(gameinfo.base_width as f32, gameinfo.base_height as f32);
control.renderer_mut().set_window_title(&gameinfo.friendly_name);
info!("Gameloop constructed");
Ok(Gameloop
{
sdl_context,
event_pump,
control,
current_scene : Box::new(Scene::<PlaceholderScene>::new()),
time : GameloopTimeState::new(),
kiosk_mode : gameinfo.kiosk_mode
})
}
pub fn control(&self) -> &GameControl
{
&self.control
}
pub fn control_mut(&mut self) -> &mut GameControl
{
&mut self.control
}
fn update_input(&mut self)
{
let event_pump = &self.event_pump;
let mut callback = | key : &str |
{
let scancode = try_opt_or_else!(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();
let cap_ms = 1000.0 / self.control.frame_interpolation_cap().unwrap_or(1000.0);
self.time.target_frametime = Duration::from_nanos((target_ms * 1_000_000.0) as u64);
self.time.cap_frametime = Duration::from_nanos((cap_ms * 1_000_000.0) as u64);
self.control.sync_audio_store();
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.control.renderer.control().sync_sheet_store(&mut self.control.resources.sheet_store)
.expect("GPU sheet store sync failed");
self.control.renderer.control().sync_shader_store(&mut self.control.resources.shader_store)
.expect("GPU shader store sync failed");
self.control.renderer.draw(&mut self.current_scene, interpolation_amount as f32);
}
#[cfg(feature = "imgui_base")]
{
self.update_imgui();
}
self.control.renderer.control().flush_drawing();
self.time.draw_timer.end_frame_timing();
}
#[cfg(feature = "imgui_base")]
fn update_imgui(&mut self)
{
let frametime = self.time.target_frametime * self.time.think_timer.tics_this_frame();
let frame_delta = frametime.as_nanos() as f64 * MILLISECONDS_PER_NANOSECOND / 1000.0;
let (width, height) = self.control.renderer.window_size();
self.control.imgui_handler.update_size_and_delta(frame_delta as f32, width as f32, height as f32);
let mut ui = self.control.imgui_handler.imgui_mut().frame();
self.current_scene.imgui_think(&mut ui);
let imgui_cursor = ui.mouse_cursor();
self.control.renderer.control().draw_imgui(ui);
self.control.imgui_handler.update_cursor(&mut self.sdl_context.mouse(), imgui_cursor);
}
fn wait(&mut self)
{
self.time.wait_timer.begin_frame_timing();
if !self.control.frame_interpolation
{
self.time.sleep(self.time.target_frametime);
}
else if self.control.frame_interpolation_cap().is_some()
{
self.time.interpolate_sleep(self.time.cap_frametime);
self.time.update_jiffies();
}
else
{
self.time.update_jiffies();
}
self.time.wait_timer.end_frame_timing();
}
fn present(&mut self)
{
self.time.present_timer.begin_frame_timing();
self.control.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!("{} ent {} com | {} / {} FPS | avg ms think {:.2} draw {:.2} present {:.2} wait {:.2}",
self.current_scene.entity_count(),
self.current_scene.component_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)
{
print_log_section("START GAMELOOP");
self.time = GameloopTimeState::new();
'runloop: while !self.control.done
{
for event in self.event_pump.poll_iter()
{
match event
{
Event::Quit {..} =>
{
if !self.kiosk_mode
{
break 'runloop;
}
},
Event::Window { win_event, .. } => match win_event
{
WindowEvent::SizeChanged {..} =>
{
self.control.renderer.recalculate_viewport();
}
_ => ()
},
Event::KeyDown { scancode, keymod, repeat, .. } =>
{
if !repeat && (keymod.contains(KeyboardMod::LALTMOD)
|| keymod.contains(KeyboardMod::RALTMOD))
&& scancode == Some(sdl2::keyboard::Scancode::Return)
&& !self.kiosk_mode
{
self.control.renderer.toggle_fullscreen();
}
},
_ => ()
}
#[cfg(feature = "imgui_base")]
self.control.imgui_handler.handle_event(event);
}
self.think();
self.draw();
self.wait();
self.present();
self.update_fps();
if self.control.next_scene_waiting
{
self.current_scene.end(&mut self.control);
print_log_section("NEW SCENE");
std::mem::swap(&mut self.current_scene, &mut self.control.next_scene);
self.control.next_scene = Box::new(Scene::<PlaceholderScene>::new());
self.current_scene.start(&mut self.control);
self.time.flush();
self.control.next_scene_waiting = false;
}
}
print_log_section("END GAMELOOP");
self.current_scene.end(&mut self.control);
}
}