Created
April 13, 2018 08:27
Star
You must be signed in to star a gist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::path::PathBuf; | |
use std::collections::{BTreeMap, HashMap, HashSet}; | |
use std::thread; | |
use std::os::raw::c_void; | |
use std::rc::Rc; | |
use std::cell::{Cell, RefCell}; | |
use std::i16; | |
use std::time::Instant; | |
use failure::{err_msg, Error}; | |
use strum::IntoEnumIterator; | |
use gl; | |
use sdl2; | |
use spell_core::math::Vector2; | |
use spell_core::util::{duration_from_seconds, duration_to_seconds, TickMonitor}; | |
use spell_platform::{Application, ApplicationController, AudioCallback, AudioSpec, ControllerAxis, | |
ControllerButton, ControllerId, ControllerState, InputEvent, InputState, Key, | |
KeyModSet, MouseButton}; | |
use opengl_rendering::GLRenderer; | |
const TARGET_FRAME_RATE: f64 = 60.0; | |
const TICK_MONITOR_WINDOW: f64 = 5.0 / 60.0; | |
/// Runs the given application with a hard-coded target framerate of 60hz. Doesn't work well if the | |
/// monitor refresh rate is not a multiple of 60, but that is exceptionally rare. We have to always | |
/// target 60hz, because even if the SDL2 API claims that there is an OpenGL swap interval, in | |
/// certain situations on certain platforms `gl_swap_window` will return nearly instantly and can be | |
/// called arbitrarily fast. We set a maximum framerate of 60hz so that in that case, we do not | |
/// just spin and always use 100% of a core. | |
pub fn run_application<T: Application>() { | |
let sdl_context = sdl2::init().unwrap(); | |
let video_subsystem = sdl_context.video().unwrap(); | |
let controller_subsystem = sdl_context.game_controller().unwrap(); | |
let gl_attr = video_subsystem.gl_attr(); | |
gl_attr.set_context_profile(sdl2::video::GLProfile::Core); | |
gl_attr.set_context_version(3, 3); | |
gl_attr.set_double_buffer(true); | |
gl_attr.set_depth_size(0); | |
// Right now, hard code preferring late swap tearing, then vsync, then no vsync. | |
if !video_subsystem.gl_set_swap_interval(-1) { | |
video_subsystem.gl_set_swap_interval(1); | |
} | |
let window = video_subsystem | |
.window("spellbound", 1200, 700) | |
.opengl() | |
.position_centered() | |
.resizable() | |
.build() | |
.unwrap(); | |
let _gl_context = window.gl_create_context().unwrap(); | |
gl::load_with(|s| video_subsystem.gl_get_proc_address(s) as *const c_void); | |
let event_pump = Rc::new(RefCell::new(sdl_context.event_pump().unwrap())); | |
// Process controller events | |
controller_subsystem.set_event_state(true); | |
let renderer = GLRenderer::new().unwrap(); | |
let controller = Rc::new(SdlApplicationController { | |
audio_subsystem: sdl_context.audio().unwrap(), | |
audio_device: RefCell::new(None), | |
controller_subsystem, | |
controller_ids: RefCell::new(HashMap::new()), | |
controllers: RefCell::new(BTreeMap::new()), | |
window_height: Cell::new(0), | |
event_pump: event_pump.clone(), | |
text_input: video_subsystem.text_input(), | |
clipboard: video_subsystem.clipboard(), | |
quit: Cell::new(false), | |
}); | |
controller.scan_new_controllers(); | |
let base_path = | |
PathBuf::from(sdl2::filesystem::base_path().expect("could not find base application path")); | |
let mut application = T::init(&base_path, controller.clone(), renderer.clone()); | |
let mut tick_monitor = TickMonitor::new_at_rate(TARGET_FRAME_RATE, TICK_MONITOR_WINDOW); | |
let mut last_frame_start: Option<Instant> = None; | |
'running: loop { | |
let (window_width, window_height) = window.size(); | |
controller.window_height.set(window_height as i32); | |
renderer.set_screen_size(window_width, window_height); | |
let mut input_events = Vec::new(); | |
for event in event_pump.borrow_mut().poll_iter() { | |
match event { | |
sdl2::event::Event::Quit { .. } => break 'running, | |
sdl2::event::Event::KeyDown { | |
scancode: Some(sc), | |
repeat, | |
.. | |
} => { | |
// TODO: mods not properly set | |
if let Some(key) = get_spell_key(sc) { | |
input_events.push(InputEvent::KeyDown { | |
key: key, | |
mods: KeyModSet::empty(), | |
repeat: repeat, | |
}); | |
} | |
} | |
sdl2::event::Event::KeyUp { | |
scancode: Some(sc), .. | |
} => if let Some(key) = get_spell_key(sc) { | |
input_events.push(InputEvent::KeyUp { key: key }); | |
}, | |
sdl2::event::Event::MouseButtonDown { | |
mouse_btn, x, y, .. | |
} => { | |
let mouse_position = Vector2::new(x, window_height as i32 - y); | |
if let Some(mouse_button) = get_spell_mouse_button(mouse_btn) { | |
input_events.push(InputEvent::MouseButtonDown { | |
button: mouse_button, | |
position: mouse_position, | |
}); | |
} | |
} | |
sdl2::event::Event::MouseButtonUp { | |
mouse_btn, x, y, .. | |
} => { | |
let mouse_position = Vector2::new(x, window_height as i32 - y); | |
if let Some(mouse_button) = get_spell_mouse_button(mouse_btn) { | |
input_events.push(InputEvent::MouseButtonUp { | |
button: mouse_button, | |
position: mouse_position, | |
}); | |
} | |
} | |
sdl2::event::Event::MouseMotion { | |
x, y, xrel, yrel, .. | |
} => { | |
let mouse_position = Vector2::new(x, window_height as i32 - y); | |
input_events.push(InputEvent::MouseMove { | |
movement: Vector2::new(xrel, -yrel), | |
position: mouse_position, | |
}); | |
} | |
sdl2::event::Event::MouseWheel { x, y, .. } => { | |
input_events.push(InputEvent::MouseWheel { | |
scroll: Vector2::new(x, y), | |
}); | |
} | |
sdl2::event::Event::TextInput { text, .. } => { | |
input_events.push(InputEvent::TextInput { text }); | |
} | |
sdl2::event::Event::ControllerAxisMotion { | |
which, axis, value, .. | |
} => if let Some(&id) = controller.controller_ids.borrow().get(&which) { | |
let axis = get_spell_controller_axis(axis); | |
input_events.push(InputEvent::ControllerAxisChange { | |
controller_id: id, | |
axis, | |
value: fix_axis_direction(axis, value), | |
}); | |
}, | |
sdl2::event::Event::ControllerButtonDown { which, button, .. } => { | |
if let Some(&id) = controller.controller_ids.borrow().get(&which) { | |
input_events.push(InputEvent::ControllerButtonDown { | |
controller_id: id, | |
button: get_spell_controller_button(button), | |
}); | |
} | |
} | |
sdl2::event::Event::ControllerButtonUp { which, button, .. } => { | |
if let Some(&id) = controller.controller_ids.borrow().get(&which) { | |
input_events.push(InputEvent::ControllerButtonUp { | |
controller_id: id, | |
button: get_spell_controller_button(button), | |
}); | |
} | |
} | |
sdl2::event::Event::ControllerDeviceAdded { .. } => { | |
controller.scan_new_controllers(); | |
} | |
sdl2::event::Event::ControllerDeviceRemoved { .. } => { | |
controller.filter_detached_controllers(); | |
} | |
_ => {} | |
} | |
} | |
let dt = if let Some(last_frame_start) = last_frame_start { | |
duration_to_seconds(last_frame_start.elapsed()) | |
} else { | |
1.0 / TARGET_FRAME_RATE | |
}; | |
last_frame_start = Some(Instant::now()); | |
application.update(dt, input_events); | |
tick_monitor.tick(1.0); | |
window.gl_swap_window(); | |
let spare_time = tick_monitor.spare_time(TARGET_FRAME_RATE); | |
if spare_time > 0.0 { | |
thread::sleep(duration_from_seconds(spare_time)); | |
} | |
} | |
application.uninit(); | |
} | |
struct SdlAudioCallback(AudioCallback); | |
impl sdl2::audio::AudioCallback for SdlAudioCallback { | |
type Channel = f32; | |
fn callback(&mut self, buf: &mut [Self::Channel]) { | |
self.0(buf) | |
} | |
} | |
struct SdlApplicationController { | |
audio_subsystem: sdl2::AudioSubsystem, | |
audio_device: RefCell<Option<sdl2::audio::AudioDevice<SdlAudioCallback>>>, | |
controller_subsystem: sdl2::GameControllerSubsystem, | |
controller_ids: RefCell<HashMap<i32, ControllerId>>, | |
controllers: RefCell<BTreeMap<ControllerId, sdl2::controller::GameController>>, | |
window_height: Cell<i32>, | |
event_pump: Rc<RefCell<sdl2::EventPump>>, | |
text_input: sdl2::keyboard::TextInputUtil, | |
clipboard: sdl2::clipboard::ClipboardUtil, | |
quit: Cell<bool>, | |
} | |
impl SdlApplicationController { | |
fn scan_new_controllers(&self) { | |
let mut controller_ids = self.controller_ids.borrow_mut(); | |
let mut controllers = self.controllers.borrow_mut(); | |
for i in 0 | |
..self.controller_subsystem | |
.num_joysticks() | |
.expect("could not enumerate controllers") | |
{ | |
if self.controller_subsystem.is_game_controller(i) { | |
let controller = self.controller_subsystem | |
.open(i) | |
.expect("could not open controller"); | |
let controller_id = ControllerId( | |
controllers | |
.iter() | |
.last() | |
.map(|(id, _)| id.0 + 1) | |
.unwrap_or(0), | |
); | |
controller_ids.insert(controller.instance_id(), controller_id); | |
controllers.insert(controller_id, controller).is_none(); | |
} | |
} | |
} | |
fn filter_detached_controllers(&self) { | |
let mut controller_ids = self.controller_ids.borrow_mut(); | |
let mut controllers = self.controllers.borrow_mut(); | |
let mut detached_controllers = HashSet::new(); | |
for (&id, controller) in controllers.iter() { | |
if !controller.attached() { | |
detached_controllers.insert(id); | |
} | |
} | |
for id in &detached_controllers { | |
controllers.remove(id); | |
} | |
controller_ids.retain(|_, id| !detached_controllers.contains(id)); | |
} | |
} | |
impl ApplicationController for SdlApplicationController { | |
fn start_text_input(&self) { | |
self.text_input.start(); | |
} | |
fn stop_text_input(&self) { | |
self.text_input.stop(); | |
} | |
fn clipboard_text(&self) -> Result<String, Error> { | |
self.clipboard.clipboard_text().map_err(err_msg) | |
} | |
fn set_clipboard_text(&self, text: &str) -> Result<(), Error> { | |
Ok(self.clipboard.set_clipboard_text(text).map_err(err_msg) | |
} | |
fn input_state(&self) -> InputState { | |
let event_pump = self.event_pump.borrow(); | |
let keyboard_state = event_pump.keyboard_state(); | |
let mouse_state = event_pump.mouse_state(); | |
let mouse_position = | |
Vector2::new(mouse_state.x(), self.window_height.get() - mouse_state.y()); | |
let mut input_state = InputState { | |
keys_down: HashSet::new(), | |
mouse_buttons_down: HashSet::new(), | |
mouse_position, | |
controllers: BTreeMap::new(), | |
}; | |
input_state.keys_down = keyboard_state | |
.pressed_scancodes() | |
.filter_map(get_spell_key) | |
.collect(); | |
input_state.mouse_buttons_down = mouse_state | |
.mouse_buttons() | |
.filter_map(|(b, p)| if p { Some(b) } else { None }) | |
.filter_map(get_spell_mouse_button) | |
.collect(); | |
let controller_ids = self.controller_ids.borrow(); | |
let mut controllers = self.controllers.borrow_mut(); | |
for controller in controllers.values_mut() { | |
if controller.attached() { | |
let mut controller_state = ControllerState { | |
buttons_down: HashSet::new(), | |
axis_values: HashMap::new(), | |
}; | |
for axis in ControllerAxis::iter() { | |
let value = controller.axis(get_sdl_controller_axis(axis)); | |
if value != 0 { | |
controller_state | |
.axis_values | |
.insert(axis, fix_axis_direction(axis, value)); | |
} | |
} | |
for button in ControllerButton::iter() { | |
if controller.button(get_sdl_controller_button(button)) { | |
controller_state.buttons_down.insert(button); | |
} | |
} | |
let controller_id = *controller_ids.get(&controller.instance_id()).unwrap(); | |
input_state | |
.controllers | |
.insert(controller_id, controller_state); | |
} | |
} | |
input_state | |
} | |
fn start_audio(&self, get_callback: &mut FnMut(AudioSpec) -> AudioCallback) { | |
let mut device = self.audio_device.borrow_mut(); | |
*device = None; | |
let audio_device = self.audio_subsystem | |
.open_playback( | |
None, | |
&sdl2::audio::AudioSpecDesired { | |
freq: Some(44100), | |
channels: Some(2), | |
samples: Some(2048), | |
}, | |
|spec| { | |
SdlAudioCallback(get_callback(AudioSpec { | |
sample_rate: spec.freq as u32, | |
channels: spec.channels, | |
})) | |
}, | |
) | |
.expect("could not open audio device for playback"); | |
audio_device.resume(); | |
*device = Some(audio_device); | |
} | |
fn stop_audio(&self) { | |
*self.audio_device.borrow_mut() = None; | |
} | |
fn quit(&self) { | |
self.quit.set(true); | |
} | |
} | |
fn get_spell_key(key: sdl2::keyboard::Scancode) -> Option<Key> { | |
// UNINTERESTING | |
} | |
fn get_spell_mouse_button(mouse_button: sdl2::mouse::MouseButton) -> Option<MouseButton> { | |
// UNINTERESTING | |
} | |
fn get_sdl_controller_button(controller_button: ControllerButton) -> sdl2::controller::Button { | |
// UNINTERESTING | |
} | |
fn get_spell_controller_button(controller_button: sdl2::controller::Button) -> ControllerButton { | |
// UNINTERESTING | |
} | |
fn get_sdl_controller_axis(controller_axis: ControllerAxis) -> sdl2::controller::Axis { | |
// UNINTERESTING | |
} | |
fn get_spell_controller_axis(controller_axis: sdl2::controller::Axis) -> ControllerAxis { | |
// UNINTERESTING | |
} | |
fn fix_axis_direction(axis: ControllerAxis, axis_value: i16) -> i16 { | |
// UNINTERESTING | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment