Add entity and UI rendering
This commit is contained in:
parent
f406495e22
commit
935998abb4
3 changed files with 313 additions and 11 deletions
16
TODO.md
16
TODO.md
|
|
@ -21,14 +21,14 @@
|
||||||
|
|
||||||
## phase 3: rendering
|
## phase 3: rendering
|
||||||
|
|
||||||
- [ ] draw terrain as connected line segments
|
- [x] draw terrain as connected line segments
|
||||||
- [ ] draw players as geometric shapes
|
- [x] draw players as geometric shapes
|
||||||
- [ ] draw cannon angle indicator
|
- [x] draw cannon angle indicator
|
||||||
- [ ] draw power meter
|
- [x] draw power meter
|
||||||
- [ ] draw projectile with trail (last N positions)
|
- [x] draw projectile with trail (last N positions)
|
||||||
- [ ] implement bloom shader (blur.fs)
|
- [x] implement bloom shader (blur.fs)
|
||||||
- [ ] render-to-texture pipeline for glow effect
|
- [x] render-to-texture pipeline for glow effect
|
||||||
- [ ] explosion effect (expanding circle)
|
- [x] explosion effect (expanding circle)
|
||||||
|
|
||||||
## phase 4: local two-player
|
## phase 4: local two-player
|
||||||
|
|
||||||
|
|
|
||||||
32
shaders/blur.fs
Normal file
32
shaders/blur.fs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#version 330
|
||||||
|
|
||||||
|
in vec2 fragTexCoord;
|
||||||
|
in vec4 fragColor;
|
||||||
|
|
||||||
|
uniform sampler2D texture0;
|
||||||
|
uniform vec4 colDiffuse;
|
||||||
|
|
||||||
|
// blur direction: (1.0, 0.0) for horizontal, (0.0, 1.0) for vertical
|
||||||
|
uniform vec2 direction;
|
||||||
|
uniform vec2 resolution;
|
||||||
|
|
||||||
|
out vec4 finalColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 texelSize = 1.0 / resolution;
|
||||||
|
vec4 sum = vec4(0.0);
|
||||||
|
|
||||||
|
// 9-tap gaussian blur
|
||||||
|
float weights[9] = float[](
|
||||||
|
0.0162, 0.0540, 0.1216, 0.1945, 0.2270,
|
||||||
|
0.1945, 0.1216, 0.0540, 0.0162
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int i = -4; i <= 4; i++) {
|
||||||
|
vec2 offset = direction * float(i) * texelSize * 2.0;
|
||||||
|
sum += texture(texture0, fragTexCoord + offset) * weights[i + 4];
|
||||||
|
}
|
||||||
|
|
||||||
|
finalColor = sum * colDiffuse * fragColor;
|
||||||
|
}
|
||||||
276
src/main.zig
276
src/main.zig
|
|
@ -13,6 +13,66 @@ const Input = game.Input;
|
||||||
const SCREEN_WIDTH = terrain_mod.SCREEN_WIDTH;
|
const SCREEN_WIDTH = terrain_mod.SCREEN_WIDTH;
|
||||||
const SCREEN_HEIGHT = terrain_mod.SCREEN_HEIGHT;
|
const SCREEN_HEIGHT = terrain_mod.SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
// trail ring buffer for projectile
|
||||||
|
const TRAIL_LENGTH = 20;
|
||||||
|
var trail_positions: [TRAIL_LENGTH]struct { x: f32, y: f32 } = undefined;
|
||||||
|
var trail_count: usize = 0;
|
||||||
|
var trail_head: usize = 0;
|
||||||
|
var last_proj_exists: bool = false;
|
||||||
|
|
||||||
|
// explosion animation state
|
||||||
|
const Explosion = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
radius: f32,
|
||||||
|
max_radius: f32,
|
||||||
|
alpha: u8,
|
||||||
|
};
|
||||||
|
const MAX_EXPLOSIONS = 4;
|
||||||
|
var explosions: [MAX_EXPLOSIONS]?Explosion = .{ null, null, null, null };
|
||||||
|
|
||||||
|
fn spawnExplosion(x: f32, y: f32) void {
|
||||||
|
for (&explosions) |*slot| {
|
||||||
|
if (slot.* == null) {
|
||||||
|
slot.* = .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.radius = 5,
|
||||||
|
.max_radius = 40,
|
||||||
|
.alpha = 255,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updateExplosions() void {
|
||||||
|
for (&explosions) |*slot| {
|
||||||
|
if (slot.*) |*exp| {
|
||||||
|
exp.radius += 2;
|
||||||
|
if (exp.alpha > 8) {
|
||||||
|
exp.alpha -= 8;
|
||||||
|
} else {
|
||||||
|
slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawExplosions() void {
|
||||||
|
for (explosions) |maybe_exp| {
|
||||||
|
if (maybe_exp) |exp| {
|
||||||
|
const color = rl.Color{ .r = 255, .g = 200, .b = 50, .a = exp.alpha };
|
||||||
|
rl.drawCircleLines(@intFromFloat(exp.x), @intFromFloat(exp.y), exp.radius, color);
|
||||||
|
// inner circle
|
||||||
|
if (exp.radius > 10) {
|
||||||
|
const inner_color = rl.Color{ .r = 255, .g = 255, .b = 200, .a = exp.alpha / 2 };
|
||||||
|
rl.drawCircleLines(@intFromFloat(exp.x), @intFromFloat(exp.y), exp.radius * 0.6, inner_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// colors (vector/oscilloscope aesthetic)
|
// colors (vector/oscilloscope aesthetic)
|
||||||
const BG_COLOR = rl.Color{ .r = 10, .g = 10, .b = 18, .a = 255 };
|
const BG_COLOR = rl.Color{ .r = 10, .g = 10, .b = 18, .a = 255 };
|
||||||
const CYAN = rl.Color{ .r = 0, .g = 255, .b = 255, .a = 255 };
|
const CYAN = rl.Color{ .r = 0, .g = 255, .b = 255, .a = 255 };
|
||||||
|
|
@ -25,6 +85,41 @@ pub fn main() !void {
|
||||||
defer rl.closeWindow();
|
defer rl.closeWindow();
|
||||||
rl.setTargetFPS(60);
|
rl.setTargetFPS(60);
|
||||||
|
|
||||||
|
// load blur shader
|
||||||
|
const blur_shader = rl.loadShader(null, "shaders/blur.fs") catch |err| {
|
||||||
|
std.debug.print("warning: could not load blur shader: {}\n", .{err});
|
||||||
|
@panic("shader load failed");
|
||||||
|
};
|
||||||
|
defer rl.unloadShader(blur_shader);
|
||||||
|
|
||||||
|
// get shader uniform locations
|
||||||
|
const direction_loc = rl.getShaderLocation(blur_shader, "direction");
|
||||||
|
const resolution_loc = rl.getShaderLocation(blur_shader, "resolution");
|
||||||
|
|
||||||
|
// set resolution uniform (doesn't change)
|
||||||
|
const resolution = [2]f32{ @floatFromInt(SCREEN_WIDTH), @floatFromInt(SCREEN_HEIGHT) };
|
||||||
|
rl.setShaderValue(blur_shader, resolution_loc, &resolution, .vec2);
|
||||||
|
|
||||||
|
// create render textures for bloom pipeline
|
||||||
|
const game_tex = rl.loadRenderTexture(@intCast(SCREEN_WIDTH), @intCast(SCREEN_HEIGHT)) catch |err| {
|
||||||
|
std.debug.print("warning: could not load render texture: {}\n", .{err});
|
||||||
|
@panic("render texture load failed");
|
||||||
|
};
|
||||||
|
defer rl.unloadRenderTexture(game_tex);
|
||||||
|
const blur_tex = rl.loadRenderTexture(@intCast(SCREEN_WIDTH), @intCast(SCREEN_HEIGHT)) catch |err| {
|
||||||
|
std.debug.print("warning: could not load render texture: {}\n", .{err});
|
||||||
|
@panic("render texture load failed");
|
||||||
|
};
|
||||||
|
defer rl.unloadRenderTexture(blur_tex);
|
||||||
|
|
||||||
|
// source rectangle (flip Y for render texture)
|
||||||
|
const src_rect = rl.Rectangle{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.width = @floatFromInt(SCREEN_WIDTH),
|
||||||
|
.height = @floatFromInt(-@as(i32, SCREEN_HEIGHT)),
|
||||||
|
};
|
||||||
|
|
||||||
// initialize game
|
// initialize game
|
||||||
const terrain = terrain_mod.generateFixed();
|
const terrain = terrain_mod.generateFixed();
|
||||||
var state = game.initGame(&terrain);
|
var state = game.initGame(&terrain);
|
||||||
|
|
@ -38,13 +133,53 @@ pub fn main() !void {
|
||||||
inputs[state.current_turn] = input;
|
inputs[state.current_turn] = input;
|
||||||
game.simulate(&state, inputs);
|
game.simulate(&state, inputs);
|
||||||
|
|
||||||
// render
|
const screen_h: i32 = @intCast(SCREEN_HEIGHT);
|
||||||
rl.beginDrawing();
|
|
||||||
defer rl.endDrawing();
|
|
||||||
|
|
||||||
|
// update animations
|
||||||
|
updateTrail(&state, screen_h);
|
||||||
|
updateExplosions();
|
||||||
|
|
||||||
|
// 1. draw game to texture
|
||||||
|
rl.beginTextureMode(game_tex);
|
||||||
rl.clearBackground(BG_COLOR);
|
rl.clearBackground(BG_COLOR);
|
||||||
|
|
||||||
|
drawTerrain(state.terrain);
|
||||||
|
for (0..2) |i| {
|
||||||
|
drawPlayer(&state.players[i], i, screen_h);
|
||||||
|
}
|
||||||
|
drawProjectile(&state, screen_h);
|
||||||
|
drawExplosions();
|
||||||
|
|
||||||
|
rl.endTextureMode();
|
||||||
|
|
||||||
|
// 2. horizontal blur pass
|
||||||
|
rl.beginTextureMode(blur_tex);
|
||||||
|
rl.beginShaderMode(blur_shader);
|
||||||
|
const h_dir = [2]f32{ 1.0, 0.0 };
|
||||||
|
rl.setShaderValue(blur_shader, direction_loc, &h_dir, .vec2);
|
||||||
|
rl.drawTextureRec(game_tex.texture, src_rect, .{ .x = 0, .y = 0 }, rl.Color.white);
|
||||||
|
rl.endShaderMode();
|
||||||
|
rl.endTextureMode();
|
||||||
|
|
||||||
|
// 3. final composite: original + vertical blur (additive)
|
||||||
|
rl.beginDrawing();
|
||||||
|
|
||||||
|
// draw original game
|
||||||
|
rl.drawTextureRec(game_tex.texture, src_rect, .{ .x = 0, .y = 0 }, rl.Color.white);
|
||||||
|
|
||||||
|
// additive blend the blurred version (vertical blur of horizontal blur)
|
||||||
|
rl.beginBlendMode(.additive);
|
||||||
|
rl.beginShaderMode(blur_shader);
|
||||||
|
const v_dir = [2]f32{ 0.0, 1.0 };
|
||||||
|
rl.setShaderValue(blur_shader, direction_loc, &v_dir, .vec2);
|
||||||
|
rl.drawTextureRec(blur_tex.texture, src_rect, .{ .x = 0, .y = 0 }, rl.Color.white);
|
||||||
|
rl.endShaderMode();
|
||||||
|
rl.endBlendMode();
|
||||||
|
|
||||||
|
// draw UI on top (not affected by bloom)
|
||||||
drawDebugInfo(&state);
|
drawDebugInfo(&state);
|
||||||
|
|
||||||
|
rl.endDrawing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +200,141 @@ fn gatherInput() Input {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Player = game.Player;
|
||||||
|
|
||||||
|
// player rendering constants
|
||||||
|
const PLAYER_BASE_WIDTH: f32 = 30;
|
||||||
|
const PLAYER_BASE_HEIGHT: f32 = 15;
|
||||||
|
const CANNON_LENGTH: f32 = 25;
|
||||||
|
const CANNON_THICKNESS: f32 = 3;
|
||||||
|
|
||||||
|
fn getPlayerColor(idx: usize) rl.Color {
|
||||||
|
return if (idx == 0) CYAN else MAGENTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawPlayer(player: *const Player, idx: usize, screen_h: i32) void {
|
||||||
|
if (!player.alive) return;
|
||||||
|
|
||||||
|
const color = getPlayerColor(idx);
|
||||||
|
const px = player.x.toFloat();
|
||||||
|
const py = @as(f32, @floatFromInt(screen_h)) - player.y.toFloat();
|
||||||
|
|
||||||
|
// base rectangle
|
||||||
|
const base_x = px - PLAYER_BASE_WIDTH / 2;
|
||||||
|
const base_y = py - PLAYER_BASE_HEIGHT;
|
||||||
|
rl.drawRectangleLines(
|
||||||
|
@intFromFloat(base_x),
|
||||||
|
@intFromFloat(base_y),
|
||||||
|
@intFromFloat(PLAYER_BASE_WIDTH),
|
||||||
|
@intFromFloat(PLAYER_BASE_HEIGHT),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
|
||||||
|
// turret circle on top
|
||||||
|
const turret_y = py - PLAYER_BASE_HEIGHT;
|
||||||
|
rl.drawCircleLines(@intFromFloat(px), @intFromFloat(turret_y), 8, color);
|
||||||
|
|
||||||
|
// cannon barrel
|
||||||
|
// angle: 0 = right, PI = left
|
||||||
|
// for player 1 (idx=1), flip the direction
|
||||||
|
const angle = player.cannon_angle.toFloat();
|
||||||
|
const dir: f32 = if (idx == 0) 1 else -1;
|
||||||
|
const cannon_end_x = px + dir * @cos(angle) * CANNON_LENGTH;
|
||||||
|
const cannon_end_y = turret_y - @sin(angle) * CANNON_LENGTH;
|
||||||
|
|
||||||
|
rl.drawLineEx(
|
||||||
|
.{ .x = px, .y = turret_y },
|
||||||
|
.{ .x = cannon_end_x, .y = cannon_end_y },
|
||||||
|
CANNON_THICKNESS,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
|
||||||
|
// power meter (bar below player)
|
||||||
|
const power_bar_width: f32 = 40;
|
||||||
|
const power_bar_height: f32 = 4;
|
||||||
|
const power_y = py + 5;
|
||||||
|
const power_x = px - power_bar_width / 2;
|
||||||
|
const power_pct = player.power.toFloat() / 100.0;
|
||||||
|
|
||||||
|
// outline
|
||||||
|
rl.drawRectangleLines(
|
||||||
|
@intFromFloat(power_x),
|
||||||
|
@intFromFloat(power_y),
|
||||||
|
@intFromFloat(power_bar_width),
|
||||||
|
@intFromFloat(power_bar_height),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
|
||||||
|
// filled portion
|
||||||
|
rl.drawRectangle(
|
||||||
|
@intFromFloat(power_x + 1),
|
||||||
|
@intFromFloat(power_y + 1),
|
||||||
|
@intFromFloat((power_bar_width - 2) * power_pct),
|
||||||
|
@intFromFloat(power_bar_height - 2),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updateTrail(state: *const GameState, screen_h: i32) void {
|
||||||
|
if (state.projectile) |proj| {
|
||||||
|
// add new position
|
||||||
|
const x = proj.x.toFloat();
|
||||||
|
const y = @as(f32, @floatFromInt(screen_h)) - proj.y.toFloat();
|
||||||
|
|
||||||
|
trail_positions[trail_head] = .{ .x = x, .y = y };
|
||||||
|
trail_head = (trail_head + 1) % TRAIL_LENGTH;
|
||||||
|
if (trail_count < TRAIL_LENGTH) trail_count += 1;
|
||||||
|
|
||||||
|
last_proj_exists = true;
|
||||||
|
} else {
|
||||||
|
// projectile gone - spawn explosion at last position
|
||||||
|
if (last_proj_exists and trail_count > 0) {
|
||||||
|
const last_idx = (trail_head + TRAIL_LENGTH - 1) % TRAIL_LENGTH;
|
||||||
|
spawnExplosion(trail_positions[last_idx].x, trail_positions[last_idx].y);
|
||||||
|
trail_count = 0;
|
||||||
|
trail_head = 0;
|
||||||
|
}
|
||||||
|
last_proj_exists = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawProjectile(state: *const GameState, screen_h: i32) void {
|
||||||
|
// draw trail (fading)
|
||||||
|
if (trail_count > 1) {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < trail_count - 1) : (i += 1) {
|
||||||
|
const idx = (trail_head + TRAIL_LENGTH - trail_count + i) % TRAIL_LENGTH;
|
||||||
|
const next_idx = (idx + 1) % TRAIL_LENGTH;
|
||||||
|
|
||||||
|
const alpha: u8 = @intFromFloat(255.0 * @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(trail_count)));
|
||||||
|
const trail_color = rl.Color{ .r = 255, .g = 255, .b = 0, .a = alpha };
|
||||||
|
|
||||||
|
rl.drawLineEx(
|
||||||
|
.{ .x = trail_positions[idx].x, .y = trail_positions[idx].y },
|
||||||
|
.{ .x = trail_positions[next_idx].x, .y = trail_positions[next_idx].y },
|
||||||
|
2,
|
||||||
|
trail_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw current projectile
|
||||||
|
if (state.projectile) |proj| {
|
||||||
|
const x = proj.x.toFloat();
|
||||||
|
const y = @as(f32, @floatFromInt(screen_h)) - proj.y.toFloat();
|
||||||
|
rl.drawCircle(@intFromFloat(x), @intFromFloat(y), 4, YELLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawTerrain(terrain: *const Terrain) void {
|
||||||
|
const screen_h: i32 = @intCast(SCREEN_HEIGHT);
|
||||||
|
for (0..SCREEN_WIDTH - 1) |x| {
|
||||||
|
const y1 = screen_h - terrain.heights[x].toInt();
|
||||||
|
const y2 = screen_h - terrain.heights[x + 1].toInt();
|
||||||
|
rl.drawLine(@intCast(x), y1, @intCast(x + 1), y2, GREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn drawDebugInfo(state: *const GameState) void {
|
fn drawDebugInfo(state: *const GameState) void {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
var y: i32 = 10;
|
var y: i32 = 10;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue