WIP decoupled rendering

This commit is contained in:
Nate Anderson 2025-01-07 16:34:47 -07:00
parent ba53a7dd66
commit a43b46449f
8 changed files with 162 additions and 84 deletions

58
README.md Normal file
View File

@ -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();
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

View File

@ -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);
}
};

View File

@ -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;
}

View File

@ -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();