diff --git a/README.md b/README.md new file mode 100644 index 0000000..21590be --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +## Physics and Timing + +I want to have decoupled rendering with the ability to render partial physics simulations, so it needs to be deterministic. + +Based on this snippet from [this article](https://gafferongames.com/post/fix_your_timestep/) + +```c +double t = 0.0; +double dt = 0.01; + +double currentTime = hires_time_in_seconds(); +double accumulator = 0.0; + +State previous; +State current; + +while ( !quit ) +{ + double newTime = time(); + double frameTime = newTime - currentTime; + if ( frameTime > 0.25 ) + frameTime = 0.25; + currentTime = newTime; + + accumulator += frameTime; + + while ( accumulator >= dt ) + { + previousState = currentState; + integrate( currentState, t, dt ); + t += dt; + accumulator -= dt; + } + + const double alpha = accumulator / dt; + + State state = currentState * alpha + + previousState * ( 1.0 - alpha ); + + render( state ); +} +``` + +or from this [medium article](https://medium.com/@tglaiel/how-to-make-your-game-run-at-60fps-24c61210fe75) + +```c +while(running){ + computeDeltaTimeSomehow(); + accumulator += deltaTime; + while(accumulator >= 1.0/60.0){ + previous_state = current_state; + current_state = update(); + accumulator -= 1.0/60.0; + } + render_interpolated_somehow(previous_state, current_state, accumulator/(1.0/60.0)); + display(); +} +``` diff --git a/assets/fonts/DepartureMonoNerdFont-Regular.otf b/assets/fonts/DepartureMonoNerdFont-Regular.otf deleted file mode 100644 index 4097cb3..0000000 Binary files a/assets/fonts/DepartureMonoNerdFont-Regular.otf and /dev/null differ diff --git a/assets/fonts/DepartureMonoNerdFontMono-Regular.otf b/assets/fonts/DepartureMonoNerdFontMono-Regular.otf deleted file mode 100644 index e950e8e..0000000 Binary files a/assets/fonts/DepartureMonoNerdFontMono-Regular.otf and /dev/null differ diff --git a/assets/fonts/DepartureMonoNerdFontPropo-Regular.otf b/assets/fonts/DepartureMonoNerdFontPropo-Regular.otf deleted file mode 100644 index 656a96c..0000000 Binary files a/assets/fonts/DepartureMonoNerdFontPropo-Regular.otf and /dev/null differ diff --git a/assets/stretch.bmp b/assets/stretch.bmp deleted file mode 100644 index 42657b6..0000000 Binary files a/assets/stretch.bmp and /dev/null differ diff --git a/src/game_state.zig b/src/game_state.zig index 0582f4a..6edc9ca 100644 --- a/src/game_state.zig +++ b/src/game_state.zig @@ -1,6 +1,9 @@ const sdl = @import("./sdl.zig").c; const std = @import("std"); const sf = @import("./slime_factory.zig"); +const atils = @import("./asset_utils.zig"); +const GameText = @import("./text.zig").GameText; +const RGBAColor = @import("./utils/rgb_color.zig").RGBAColor; pub const GameState = struct { r: u8 = 0xff, @@ -21,7 +24,66 @@ pub const GameState = struct { self.slime_factory.deinit(); } - pub fn update_tick(self: *GameState) void { + // Physics main loop that processes variable time steps + pub fn update_tick(self: *GameState) !void { + const bg_texture = try atils.loadTexture(self.renderer, "assets/background.png"); + defer sdl.SDL_DestroyTexture(bg_texture); + + _ = sdl.SDL_SetRenderDrawColor(self.renderer, 0xff, 0xff, 0xff, 0xff); + _ = sdl.SDL_RenderClear(self.renderer); + _ = sdl.SDL_RenderCopy(self.renderer, bg_texture, null, null); + + var text = try GameText.loadFromRenderedText("Press S to start / stop", RGBAColor.whiteSmoke().tosdl()); + try text.render( + self, + .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 2 }, + ); + text = try GameText.loadFromRenderedText("Press P to pause / unpause", RGBAColor.whiteSmoke().tosdl()); + try text.render( + self, + .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 24 }, + ); + + // var buf: [16]u8 = undefined; + // const timer_ms = timer.getTicks(); + // // const timer_base = std.math.log10(timer_ms) + 3; + // const time_str: [*:0]const u8 = try std.fmt.bufPrintZ(&buf, "{d}ms", .{timer_ms}); + // var time_text = try GameText.loadFromRenderedText(time_str, RGBAColor.whiteSmoke().tosdl()); + // try time_text.render( + // &game_state, + // .{ .x = @intCast(game_state.SCREEN_WIDTH - time_text.w), .y = 64 }, + // ); + + // const fps_ms: f32 = @floatFromInt(fps_timer.getTicks()); + // const avg_fps: f32 = @round((frames / fps_ms) * 100_000) / 100.0; + // var fps_text = try GameText.loadFromRenderedText(try std.fmt.bufPrintZ(&buf, "{d} fps", .{avg_fps}), RGBAColor.whiteSmoke().tosdl()); + // try fps_text.render( + // &game_state, + // .{ .x = 5, .y = 5 }, + // ); + + // Render red rect + // const fill_rect: sdl.struct_SDL_Rect = sdl.SDL_Rect{ .x = SCREEN_WIDTH / 4, .y = SCREEN_HEIGHT / 4, .w = SCREEN_WIDTH / 2 + img_pos.x, .h = SCREEN_HEIGHT / 2 + img_pos.y }; + // _ = sdl.SDL_SetRenderDrawColor(self.renderer, 0xff, 0x00, 0x00, 0xff); + // _ = sdl.SDL_RenderFillRect(self.renderer, &fill_rect); + // _ = sdl.SDL_RenderSetViewport(self.renderer, &.{ .x = SCREEN_WIDTH / 2, .y = SCREEN_HEIGHT / 2, .w = SCREEN_WIDTH, .h = SCREEN_HEIGHT / 2 }); + // _ = sdl.SDL_RenderCopy(self.renderer, texture, null, null); + // frames += 1; + // if (frames > 60) { + // frames = 1; + // fps_timer.reset(); + // } + // var dest_rect = sdl.SDL_Rect{ .x = src_rect.x + img_pos.x, .y = src_rect.y + img_pos.y, .w = SCREEN_WIDTH, .h = SCREEN_HEIGHT }; + // _ = sdl.SDL_BlitScaled(zig_image, &src_rect, screen_surface, &dest_rect); + + // _ = sdl.SDL_UpdateWindowSurface(window); + // sdl.SDL_Delay(100); + self.slime_factory.render(self); } + + // Renders the current frame to screen + pub fn render(self: *GameState) void { + _ = sdl.SDL_RenderPresent(self.renderer); + } }; diff --git a/src/main.zig b/src/main.zig index 06b7f7d..1190d79 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,10 +1,11 @@ const std = @import("std"); const sdl = @import("sdl.zig").c; -const atils = @import("./asset_utils.zig"); const gs = @import("./game_state.zig"); const assert = std.debug.assert; const SCREEN_WIDTH = 640; const SCREEN_HEIGHT = 480; +const SCREEN_FPS: f64 = 60.0; +const SCREEN_TICKS_PER_FRAME: f64 = 1000 / SCREEN_FPS; const log = sdl.SDL_Log; const GameText = @import("./text.zig").GameText; const RGBAColor = @import("./utils/rgb_color.zig").RGBAColor; @@ -27,64 +28,29 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); - const bg_texture = try atils.loadTexture(renderer, "assets/background.png"); - defer sdl.SDL_DestroyTexture(bg_texture); - try GameText.initFont(); defer GameText.deinitFont(); - // var slime_factory = try sf.SlimeFactory.init(allocator, renderer); - // defer slime_factory.deinit(); - // try slime_factory.add(.{}); - var game_state = try gs.GameState.init(allocator, renderer); defer game_state.deinit(); var quit = false; - var shifted = false; - var start_time: u32 = 0; - var timer = Timer{}; var fps_timer = Timer{}; fps_timer.start(); - var frames: f32 = 0; + var frame_time = Timer{}; + var prev_frame_ms: f64 = 16.0; + var accumulator: f64 = 0.0; + while (!quit) { + frame_time.reset(); var event: sdl.SDL_Event = undefined; while (sdl.SDL_PollEvent(&event) != 0) { switch (event.type) { sdl.SDL_QUIT => { quit = true; }, - sdl.SDL_KEYUP => { - switch (event.key.keysym.sym) { - sdl.SDLK_LSHIFT => { - shifted = false; - }, - else => {}, - } - }, sdl.SDL_KEYDOWN => { switch (event.key.keysym.sym) { - sdl.SDLK_RETURN => { - start_time = sdl.SDL_GetTicks(); - }, - sdl.SDLK_s => { - if (timer.status == .started) timer.stop() else timer.start(); - }, - sdl.SDLK_p => { - if (timer.status == .paused) timer.unpause() else timer.pause(); - }, - // sdl.SDLK_UP => { - // img_pos.y = img_pos.y - 5; - // }, - // sdl.SDLK_DOWN => { - // img_pos.y = img_pos.y + 5; - // }, - // sdl.SDLK_LEFT => { - // img_pos.x = img_pos.x - 5; - // }, - // sdl.SDLK_RIGHT => { - // img_pos.x = img_pos.x + 5; - // }, else => {}, } log("Got key event: %i\n", event.key.keysym.sym); @@ -92,52 +58,43 @@ pub fn main() !void { else => {}, } } - _ = sdl.SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff); - _ = sdl.SDL_RenderClear(renderer); - _ = sdl.SDL_RenderCopy(renderer, bg_texture, null, null); - game_state.update_tick(); - var text = try GameText.loadFromRenderedText("Press S to start / stop", RGBAColor.whiteSmoke().tosdl()); - try text.render( - &game_state, - .{ .x = @intCast(game_state.SCREEN_WIDTH - text.w), .y = 2 }, - ); - text = try GameText.loadFromRenderedText("Press P to pause / unpause", RGBAColor.whiteSmoke().tosdl()); - try text.render( - &game_state, - .{ .x = @intCast(game_state.SCREEN_WIDTH - text.w), .y = 24 }, - ); - var buf: [16]u8 = undefined; - const timer_ms = timer.getTicks(); - // const timer_base = std.math.log10(timer_ms) + 3; - const time_str: [*:0]const u8 = try std.fmt.bufPrintZ(&buf, "{d}ms", .{timer_ms}); - var time_text = try GameText.loadFromRenderedText(time_str, RGBAColor.whiteSmoke().tosdl()); - try time_text.render( - &game_state, - .{ .x = @intCast(game_state.SCREEN_WIDTH - time_text.w), .y = 64 }, - ); + // double newTime = time(); + // double frameTime = newTime - currentTime; + // if ( frameTime > 0.25 ) + // frameTime = 0.25; + // currentTime = newTime; - const fps_time: f32 = @floatFromInt(fps_timer.getTicks() / 1000); - const avg_fps: f32 = @round((frames / fps_time) * 100) / 100.0; - var fps_text = try GameText.loadFromRenderedText(try std.fmt.bufPrintZ(&buf, "{d} fps", .{avg_fps}), RGBAColor.whiteSmoke().tosdl()); - try fps_text.render( - &game_state, - .{ .x = 5, .y = 5 }, - ); + // accumulator += frameTime; - // Render red rect - // const fill_rect: sdl.struct_SDL_Rect = sdl.SDL_Rect{ .x = SCREEN_WIDTH / 4, .y = SCREEN_HEIGHT / 4, .w = SCREEN_WIDTH / 2 + img_pos.x, .h = SCREEN_HEIGHT / 2 + img_pos.y }; - // _ = sdl.SDL_SetRenderDrawColor(renderer, 0xff, 0x00, 0x00, 0xff); - // _ = sdl.SDL_RenderFillRect(renderer, &fill_rect); - // _ = sdl.SDL_RenderSetViewport(renderer, &.{ .x = SCREEN_WIDTH / 2, .y = SCREEN_HEIGHT / 2, .w = SCREEN_WIDTH, .h = SCREEN_HEIGHT / 2 }); - // _ = sdl.SDL_RenderCopy(renderer, texture, null, null); - _ = sdl.SDL_RenderPresent(renderer); - frames += 1; - // var dest_rect = sdl.SDL_Rect{ .x = src_rect.x + img_pos.x, .y = src_rect.y + img_pos.y, .w = SCREEN_WIDTH, .h = SCREEN_HEIGHT }; - // _ = sdl.SDL_BlitScaled(zig_image, &src_rect, screen_surface, &dest_rect); + // while ( accumulator >= dt ) + // { + // previousState = currentState; + // integrate( currentState, t, dt ); + // t += dt; + // accumulator -= dt; + // } - // _ = sdl.SDL_UpdateWindowSurface(window); - // sdl.SDL_Delay(100); + // const double alpha = accumulator / dt; + + // State state = currentState * alpha + + // previousState * ( 1.0 - alpha ); + + // render( state ); + accumulator = accumulator + prev_frame_ms; + while (accumulator >= SCREEN_TICKS_PER_FRAME) { + try game_state.update_tick(); + accumulator = accumulator - SCREEN_TICKS_PER_FRAME; + std.debug.print("Tick update done. Acc: {d}\n", .{accumulator}); + } + + game_state.render(); + + prev_frame_ms = @floatFromInt(frame_time.getTicks()); + // const frame_time_ticks = frame_time.getTicks(); + // if (frame_time_ticks < SCREEN_TICKS_PER_FRAME) { + // sdl.SDL_Delay(SCREEN_TICKS_PER_FRAME - frame_time_ticks); + // } } return; } diff --git a/src/utils/timer.zig b/src/utils/timer.zig index 02d43db..8abe812 100644 --- a/src/utils/timer.zig +++ b/src/utils/timer.zig @@ -21,6 +21,7 @@ pub const Timer = struct { self.pause_ticks_ms = 0; } + // Resets the timer to 0 and starts it pub fn reset(self: *Timer) void { self.status = .started; self.start_ticks_ms = sdl.SDL_GetTicks();