1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-03-27 04:19:23 +00:00

Implement frame interpolation for smooth 50tps experience

This commit is contained in:
Alula 2020-10-30 23:47:29 +01:00
parent 1259de8c44
commit 2542376362
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
9 changed files with 145 additions and 28 deletions

View file

@ -275,3 +275,13 @@ impl<T: Num + PartialOrd + Copy + AsPrimitive<f32>> Into<crate::ggez::graphics::
pub fn fix9_scale(val: isize, scale: f32) -> f32 {
(val as f64 * scale as f64 / 512.0).floor() as f32 / scale
}
#[inline(always)]
fn lerp_f64(v1: f64, v2: f64, t: f64) -> f64 {
v1 * (1.0 - t.fract()) + v2 * t.fract()
}
#[inline(always)]
pub fn interpolate_fix9_scale(old_val: isize, val: isize, frame_delta: f64, scale: f32) -> f32 {
((lerp_f64(old_val as f64, val as f64, frame_delta) * scale as f64 / 512.0).floor() / (scale as f64)) as f32
}

View file

@ -1,14 +1,24 @@
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::stage::Stage;
use crate::common::interpolate_fix9_scale;
pub struct Frame {
pub x: isize,
pub y: isize,
pub prev_x: isize,
pub prev_y: isize,
pub wait: isize,
}
impl Frame {
pub fn xy_interpolated(&self, frame_time: f64, scale: f32) -> (f32, f32) {
let x = interpolate_fix9_scale(self.prev_x, self.x, frame_time, scale);
let y = interpolate_fix9_scale(self.prev_y, self.y, frame_time, scale);
(x, y)
}
pub fn immediate_update(&mut self, state: &mut SharedGameState, player: &Player, stage: &Stage) {
if (stage.map.width - 1) * 16 < state.canvas_size.0 as usize {
self.x = -(((state.canvas_size.0 as isize - ((stage.map.width - 1) * 16) as isize) * 0x200) / 2);
@ -39,6 +49,9 @@ impl Frame {
self.y = max_y;
}
}
self.prev_x = self.x;
self.prev_y = self.y;
}
pub fn update(&mut self, state: &mut SharedGameState, player: &Player, stage: &Stage) {

View file

@ -72,6 +72,7 @@ struct Game {
state: SharedGameState,
ui: UI,
def_matrix: ColumnMatrix4<f32>,
last_time: Instant,
start_time: Instant,
next_tick: u128,
loops: u64,
@ -84,6 +85,7 @@ impl Game {
ui: UI::new(ctx)?,
def_matrix: DrawParam::new().to_matrix(),
state: SharedGameState::new(ctx)?,
last_time: Instant::now(),
start_time: Instant::now(),
next_tick: 0,
loops: 0,
@ -118,6 +120,15 @@ impl Game {
}
fn draw(&mut self, ctx: &mut Context) -> GameResult {
if self.state.timing_mode != TimingMode::FrameSynchronized {
let now = Instant::now();
let delta = (now.duration_since(self.last_time).as_nanos() as f64 / 1000000.0).floor()
/ (self.state.timing_mode.get_delta_millis() / self.state.settings.speed);
self.state.frame_time += delta;
self.last_time = now;
}
self.loops = 0;
graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into());
graphics::set_transform(ctx, DrawParam::new()
.scale(Vector2::new(self.state.scale, self.state.scale))
@ -125,6 +136,11 @@ impl Game {
graphics::apply_transformations(ctx)?;
if let Some(scene) = self.scene.as_mut() {
if self.state.frame_time.floor() > self.state.prev_frame_time.floor() {
self.state.prev_frame_time = self.state.frame_time;
scene.draw_tick(&mut self.state, ctx)?;
}
scene.draw(&mut self.state, ctx)?;
if self.state.settings.touch_controls {
self.state.touch_controls.draw(&self.state.constants, &mut self.state.texture_set, ctx)?;
@ -136,7 +152,6 @@ impl Game {
}
graphics::present(ctx)?;
self.loops = 0;
Ok(())
}
@ -333,6 +348,7 @@ pub fn init() -> GameResult {
if let Some(game) = &mut game {
game.loops = 0;
game.last_time = Instant::now();
}
}
Event::Suspended => {
@ -342,6 +358,7 @@ pub fn init() -> GameResult {
}
if let Some(game) = &mut game {
game.loops = 0;
game.last_time = Instant::now();
}
}
Event::WindowEvent { event, .. } => {
@ -419,6 +436,9 @@ pub fn init() -> GameResult {
game.state.next_scene = None;
game.scene.as_mut().unwrap().init(&mut game.state, ctx).unwrap();
game.loops = 0;
game.state.frame_time = 0.0;
game.state.prev_frame_time = 0.0;
}
}
}

View file

@ -6,10 +6,11 @@ use std::io::Cursor;
use bitvec::vec::BitVec;
use byteorder::{LE, ReadBytesExt};
use itertools::Itertools;
use num_traits::abs;
use crate::bitfield;
use crate::caret::CaretType;
use crate::common::{Condition, fix9_scale, Rect};
use crate::common::{Condition, fix9_scale, interpolate_fix9_scale, Rect};
use crate::common::Direction;
use crate::common::Flag;
use crate::entity::GameEntity;
@ -75,6 +76,8 @@ pub struct NPC {
pub vel_y: isize,
pub target_x: isize,
pub target_y: isize,
pub prev_x: isize,
pub prev_y: isize,
pub exp: u16,
pub size: u8,
pub shock: u16,
@ -118,6 +121,8 @@ impl NPC {
vel_y: 0,
target_x: 0,
target_y: 0,
prev_x: 0,
prev_y: 0,
exp: 0,
size: 0,
shock: 0,
@ -230,6 +235,14 @@ impl GameEntity<(&mut Player, &HashMap<u16, RefCell<NPC>>, &mut Stage)> for NPC
self.shock -= 1;
}
if abs(self.prev_x - self.x) > 0x1000 {
self.prev_x = self.x;
}
if abs(self.prev_y - self.y) > 0x1000 {
self.prev_y = self.y;
}
Ok(())
}
@ -239,7 +252,6 @@ impl GameEntity<(&mut Player, &HashMap<u16, RefCell<NPC>>, &mut Stage)> for NPC
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, state.npc_table.get_texture_name(self.npc_type))?;
let scale = state.scale;
let off_x = if self.direction == Direction::Left { self.display_bounds.left } else { self.display_bounds.right } as isize;
let shock = if self.shock > 0 {
@ -247,8 +259,12 @@ impl GameEntity<(&mut Player, &HashMap<u16, RefCell<NPC>>, &mut Stage)> for NPC
} else { 0.0 };
batch.add_rect(
fix9_scale(self.x - off_x - frame.x, scale) + shock,
fix9_scale(self.y - self.display_bounds.top as isize - frame.y, scale),
interpolate_fix9_scale(self.prev_x - off_x - frame.prev_x,
self.x - off_x - frame.x,
state.frame_time, state.scale) + shock,
interpolate_fix9_scale(self.prev_y - self.display_bounds.top as isize - frame.prev_y,
self.y - self.display_bounds.top as isize - frame.y,
state.frame_time, state.scale),
&self.anim_rect,
);
batch.draw(ctx)?;
@ -372,6 +388,8 @@ impl NPCMap {
vel_y: 0,
target_x: 0,
target_y: 0,
prev_x: 0,
prev_y: 0,
action_num: 0,
anim_num: 0,
flag_num: data.flag_num,
@ -418,6 +436,8 @@ impl NPCMap {
vel_y: 0,
target_x: 0,
target_y: 0,
prev_x: 0,
prev_y: 0,
action_num: 0,
anim_num: 0,
flag_num: 0,

View file

@ -5,7 +5,7 @@ use num_traits::clamp;
use num_traits::FromPrimitive;
use crate::caret::CaretType;
use crate::common::{Condition, Direction, Equipment, fix9_scale, Flag, Rect};
use crate::common::{Condition, Direction, Equipment, fix9_scale, Flag, interpolate_fix9_scale, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::ggez::{Context, GameResult};
@ -27,6 +27,8 @@ pub struct Player {
pub vel_y: isize,
pub target_x: isize,
pub target_y: isize,
pub prev_x: isize,
pub prev_y: isize,
pub life: u16,
pub max_life: u16,
pub cond: Condition,
@ -72,6 +74,8 @@ impl Player {
vel_y: 0,
target_x: 0,
target_y: 0,
prev_x: 0,
prev_y: 0,
life: constants.my_char.life,
max_life: constants.my_char.max_life,
cond: Condition(0x80),
@ -635,8 +639,12 @@ impl GameEntity<()> for Player {
{
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "MyChar")?;
batch.add_rect(
fix9_scale(self.x - self.display_bounds.left as isize - frame.x, state.scale),
fix9_scale(self.y - self.display_bounds.top as isize - frame.y, state.scale),
interpolate_fix9_scale(self.prev_x - self.display_bounds.left as isize - frame.prev_x,
self.x - self.display_bounds.left as isize - frame.x,
state.frame_time, state.scale),
interpolate_fix9_scale(self.prev_y - self.display_bounds.left as isize - frame.prev_y,
self.y - self.display_bounds.left as isize - frame.y,
state.frame_time, state.scale),
&self.anim_rect,
);
batch.draw(ctx)?;
@ -647,15 +655,23 @@ impl GameEntity<()> for Player {
match self.direction {
Direction::Left => {
batch.add_rect(
fix9_scale(self.x - self.display_bounds.left as isize - frame.x, state.scale) - 8.0,
fix9_scale(self.y - self.display_bounds.top as isize - frame.y, state.scale) + self.weapon_offset_y as f32,
interpolate_fix9_scale(self.prev_x - self.display_bounds.left as isize - frame.prev_x,
self.x - self.display_bounds.left as isize - frame.x,
state.frame_time, state.scale) - 8.0,
interpolate_fix9_scale(self.prev_y - self.display_bounds.left as isize - frame.prev_y,
self.y - self.display_bounds.left as isize - frame.y,
state.frame_time, state.scale) + self.weapon_offset_y as f32,
&self.weapon_rect,
);
}
Direction::Right => {
batch.add_rect(
fix9_scale(self.x - self.display_bounds.left as isize - frame.x, state.scale),
fix9_scale(self.y - self.display_bounds.top as isize - frame.y, state.scale) + self.weapon_offset_y as f32,
interpolate_fix9_scale(self.prev_x - self.display_bounds.left as isize - frame.prev_x,
self.x - self.display_bounds.left as isize - frame.x,
state.frame_time, state.scale),
interpolate_fix9_scale(self.prev_y - self.display_bounds.left as isize - frame.prev_y,
self.y - self.display_bounds.left as isize - frame.y,
state.frame_time, state.scale) + self.weapon_offset_y as f32,
&self.weapon_rect,
);
}

View file

@ -77,6 +77,8 @@ impl GameScene {
frame: Frame {
x: 0,
y: 0,
prev_x: 0,
prev_y: 0,
wait: 16,
},
stage_id: id,
@ -222,6 +224,7 @@ impl GameScene {
fn draw_background(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_background_name)?;
let scale = state.scale;
let (frame_x, frame_y) = self.frame.xy_interpolated(state.frame_time, state.scale);
match self.stage.data.background_type {
BackgroundType::Stationary => {
@ -237,13 +240,13 @@ impl GameScene {
BackgroundType::MoveDistant | BackgroundType::MoveNear => {
let (off_x, off_y) = if self.stage.data.background_type == BackgroundType::MoveNear {
(
self.frame.x as usize % (batch.width() * 0x200),
self.frame.y as usize % (batch.height() * 0x200)
frame_x % (batch.width() as f32),
frame_y % (batch.height() as f32)
)
} else {
(
self.frame.x as usize / 2 % (batch.width() * 0x200),
self.frame.y as usize / 2 % (batch.height() * 0x200)
((frame_x / 2.0 * scale).floor() / scale) % (batch.width() as f32),
((frame_y / 2.0 * scale).floor() / scale) % (batch.height() as f32)
)
};
@ -252,8 +255,8 @@ impl GameScene {
for y in 0..count_y {
for x in 0..count_x {
batch.add((x * batch.width()) as f32 - fix9_scale(off_x as isize, scale),
(y * batch.height()) as f32 - fix9_scale(off_y as isize, scale));
batch.add((x * batch.width()) as f32 - off_x,
(y * batch.height()) as f32 - off_y);
}
}
}
@ -598,9 +601,9 @@ impl GameScene {
for npc_cell in self.npc_map.npcs.values() {
let npc = npc_cell.borrow();
if npc.x < (self.frame.x - npc.display_bounds.width() as isize * 0x200)
if npc.x < (self.frame.x - 128 - npc.display_bounds.width() as isize * 0x200)
|| npc.x > (self.frame.x + (state.canvas_size.0 as isize + npc.display_bounds.width() as isize) * 0x200)
&& npc.y < (self.frame.y - npc.display_bounds.height() as isize * 0x200)
&& npc.y < (self.frame.y - 128 - npc.display_bounds.height() as isize * 0x200)
|| npc.y > (self.frame.y + (state.canvas_size.1 as isize + npc.display_bounds.height() as isize) * 0x200) {
continue;
}
@ -716,6 +719,7 @@ impl GameScene {
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex)?;
let mut rect = Rect::<usize>::new(0, 0, 16, 16);
let (frame_x, frame_y) = self.frame.xy_interpolated(state.frame_time, state.scale);
let tile_start_x = clamp(self.frame.x / 0x200 / 16, 0, self.stage.map.width as isize) as usize;
let tile_start_y = clamp(self.frame.y / 0x200 / 16, 0, self.stage.map.height as isize) as usize;
@ -763,8 +767,8 @@ impl GameScene {
_ => {}
}
batch.add_rect((x as f32 * 16.0 - 8.0) - fix9_scale(self.frame.x, state.scale),
(y as f32 * 16.0 - 8.0) - fix9_scale(self.frame.y, state.scale), &rect);
batch.add_rect((x as f32 * 16.0 - 8.0) - frame_x,
(y as f32 * 16.0 - 8.0) - frame_y, &rect);
}
}
@ -1124,6 +1128,24 @@ impl Scene for GameScene {
Ok(())
}
fn draw_tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
self.frame.prev_x = self.frame.x;
self.frame.prev_y = self.frame.y;
self.player.prev_x = self.player.x;
self.player.prev_y = self.player.y;
for npc_cell in self.npc_map.npcs.values() {
let mut npc = npc_cell.borrow_mut();
if npc.cond.alive() {
npc.prev_x = npc.x;
npc.prev_y = npc.y;
}
}
Ok(())
}
fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
state.update_key_trigger();
@ -1182,9 +1204,9 @@ impl Scene for GameScene {
if let Some(npc_cell) = self.npc_map.npcs.get(npc_id) {
let npc = npc_cell.borrow();
if npc.x < (self.frame.x - npc.display_bounds.width() as isize * 0x200)
if npc.x < (self.frame.x - 128 - npc.display_bounds.width() as isize * 0x200)
|| npc.x > (self.frame.x + (state.canvas_size.0 as isize + npc.display_bounds.width() as isize) * 0x200)
&& npc.y < (self.frame.y - npc.display_bounds.height() as isize * 0x200)
&& npc.y < (self.frame.y - 128 - npc.display_bounds.height() as isize * 0x200)
|| npc.y > (self.frame.y + (state.canvas_size.1 as isize + npc.display_bounds.height() as isize) * 0x200) {
continue;
}

View file

@ -12,6 +12,8 @@ pub trait Scene {
fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) }
fn draw_tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) }
fn draw(&self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) }
fn debug_overlay_draw(&mut self, _game_ui: &mut Components, _state: &mut SharedGameState, _ctx: &mut Context, _frame: &mut imgui::Ui) -> GameResult { Ok(()) }

View file

@ -92,13 +92,13 @@ impl Scene for TitleScene {
self.main_menu.push_entry(MenuEntry::Active("Quit".to_string()));
self.main_menu.height = self.main_menu.entries.len() * 14 + 6;
self.option_menu.push_entry(MenuEntry::Toggle("50 FPS timing".to_string(), state.timing_mode == TimingMode::_50Hz));
self.option_menu.push_entry(MenuEntry::Toggle("Original timing (50TPS)".to_string(), state.timing_mode == TimingMode::_50Hz));
self.option_menu.push_entry(MenuEntry::Toggle("Linear scaling".to_string(), ctx.filter_mode == FilterMode::Linear));
self.option_menu.push_entry(MenuEntry::Toggle("Lighting effects".to_string(), state.settings.lighting_efects));
if state.constants.is_cs_plus {
self.option_menu.push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures));
self.option_menu.push_entry(MenuEntry::Toggle("Freeware textures".to_string(), state.settings.original_textures));
} else {
self.option_menu.push_entry(MenuEntry::Disabled("Original textures".to_string()));
self.option_menu.push_entry(MenuEntry::Disabled("Freeware textures".to_string()));
}
self.option_menu.push_entry(MenuEntry::Active("Join our Discord".to_string()));
self.option_menu.push_entry(MenuEntry::Disabled(DISCORD_LINK.to_owned()));

View file

@ -38,6 +38,14 @@ impl TimingMode {
TimingMode::FrameSynchronized => { 0 }
}
}
pub fn get_delta_millis(self) -> f64 {
match self {
TimingMode::_50Hz => { 1000.0 / 50.0 }
TimingMode::_60Hz => { 1000.0 / 60.0 }
TimingMode::FrameSynchronized => { 0.0 }
}
}
}
pub struct Settings {
@ -74,6 +82,8 @@ pub struct SharedGameState {
pub settings: Settings,
pub constants: EngineConstants,
pub new_npcs: Vec<NPC>,
pub frame_time: f64,
pub prev_frame_time: f64,
pub scale: f32,
pub lightmap_canvas: Canvas,
pub canvas_size: (f32, f32),
@ -112,7 +122,7 @@ impl SharedGameState {
.or_else(|_| BMFontRenderer::load("/", "builtin/builtin_font.fnt", ctx))?;
Ok(SharedGameState {
timing_mode: TimingMode::_60Hz,
timing_mode: TimingMode::_50Hz,
control_flags: ControlFlags(0),
game_flags: bitvec::bitvec![0; 8000],
fade_state: FadeState::Hidden,
@ -141,6 +151,8 @@ impl SharedGameState {
},
constants,
new_npcs: Vec::with_capacity(8),
frame_time: 0.0,
prev_frame_time: 0.0,
scale,
lightmap_canvas: Canvas::with_window_size(ctx)?,
screen_size,
@ -259,6 +271,8 @@ impl SharedGameState {
pub fn set_speed(&mut self, value: f64) {
self.settings.speed = value;
self.frame_time = 0.0;
self.prev_frame_time = 0.0;
if let Err(err) = self.sound_manager.set_speed(value as f32) {
log::error!("Error while sending a message to sound manager: {}", err);