Created
July 12, 2024 13:56
-
-
Save twobob/76cfc68070e456528107265dbb476634 to your computer and use it in GitHub Desktop.
pinball.zig a not at all tested wasmmodule
This file contains hidden or 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
const std = @import("std"); | |
const physics = @import("physics"); | |
const Ball = physics.Ball; | |
const Surface = physics.Surface; | |
const Allocator = std.mem.Allocator; | |
const Pos2 = physics.Pos2; | |
const Vec2 = physics.Vec2; | |
const num_flippers = 2; | |
const num_bumpers = 3; | |
const num_targets = 5; | |
const FlipperState = enum(u8) { | |
idle, | |
flipping, | |
returning, | |
}; | |
const Flipper = struct { | |
pos: Pos2, | |
length: f32, | |
angle: f32, | |
max_angle: f32, | |
state: FlipperState, | |
}; | |
const Bumper = struct { | |
pos: Pos2, | |
radius: f32, | |
}; | |
const Target = struct { | |
pos: Pos2, | |
width: f32, | |
height: f32, | |
is_hit: bool, | |
}; | |
const State = struct { | |
flippers: [num_flippers]Flipper, | |
bumpers: [num_bumpers]Bumper, | |
targets: [num_targets]Target, | |
score: u32, | |
}; | |
var state: State = undefined; | |
var balls: []Ball = undefined; | |
var chamber_pixels: []u32 = undefined; | |
var save_data: [256]u8 = undefined; | |
pub export fn init(max_balls: usize, max_chamber_pixels: usize) void { | |
physics.assertBallLayout(); | |
balls = std.heap.wasm_allocator.alloc(Ball, max_balls) catch { | |
return; | |
}; | |
chamber_pixels = std.heap.wasm_allocator.alloc(u32, max_chamber_pixels) catch { | |
return; | |
}; | |
// Initialize state | |
state = State{ | |
.flippers = [_]Flipper{ | |
Flipper{ .pos = Pos2{ .x = 0.2, .y = 0.9 }, .length = 0.15, .angle = 0, .max_angle = std.math.pi / 4, .state = .idle }, | |
Flipper{ .pos = Pos2{ .x = 0.8, .y = 0.9 }, .length = 0.15, .angle = std.math.pi, .max_angle = 3 * std.math.pi / 4, .state = .idle }, | |
}, | |
.bumpers = [_]Bumper{ | |
Bumper{ .pos = Pos2{ .x = 0.3, .y = 0.3 }, .radius = 0.03 }, | |
Bumper{ .pos = Pos2{ .x = 0.5, .y = 0.5 }, .radius = 0.03 }, | |
Bumper{ .pos = Pos2{ .x = 0.7, .y = 0.3 }, .radius = 0.03 }, | |
}, | |
.targets = [_]Target{ | |
Target{ .pos = Pos2{ .x = 0.1, .y = 0.2 }, .width = 0.05, .height = 0.02, .is_hit = false }, | |
Target{ .pos = Pos2{ .x = 0.3, .y = 0.1 }, .width = 0.05, .height = 0.02, .is_hit = false }, | |
Target{ .pos = Pos2{ .x = 0.5, .y = 0.15 }, .width = 0.05, .height = 0.02, .is_hit = false }, | |
Target{ .pos = Pos2{ .x = 0.7, .y = 0.1 }, .width = 0.05, .height = 0.02, .is_hit = false }, | |
Target{ .pos = Pos2{ .x = 0.9, .y = 0.2 }, .width = 0.05, .height = 0.02, .is_hit = false }, | |
}, | |
.score = 0, | |
}; | |
} | |
pub export fn saveMemory() [*]u8 { | |
return &save_data; | |
} | |
pub export fn ballsMemory() [*]Ball { | |
return balls.ptr; | |
} | |
pub export fn canvasMemory() [*]u32 { | |
return chamber_pixels.ptr; | |
} | |
pub export fn saveSize() usize { | |
return save_data.len; | |
} | |
pub export fn save() void { | |
// Implement save logic here if we want to | |
// flipper angles, bumper positions, target states, score, et cetera | |
} | |
pub export fn load() void { | |
// Implement load logic here if we want to | |
// Restore flipper angles, blah blah | |
} | |
pub export fn step(num_balls: usize, delta: f32) void { | |
const flipper_speed = std.math.pi * 2; // Radians per second | |
const bumper_force = 5.0; | |
// Update flippers | |
for (state.flippers) |*flipper| { | |
switch (flipper.state) { | |
.flipping => { | |
flipper.angle += flipper_speed * delta; | |
if (flipper.angle >= flipper.max_angle) { | |
flipper.angle = flipper.max_angle; | |
flipper.state = .returning; | |
} | |
}, | |
.returning => { | |
flipper.angle -= flipper_speed * delta; | |
if (flipper.angle <= 0) { | |
flipper.angle = 0; | |
flipper.state = .idle; | |
} | |
}, | |
.idle => {}, | |
} | |
} | |
// Ball physics | |
for (balls[0..num_balls]) |*ball| { | |
// Flipper collisions | |
for (state.flippers) |flipper| { | |
const flipper_end = Pos2{ | |
.x = flipper.pos.x + @cos(flipper.angle) * flipper.length, | |
.y = flipper.pos.y + @sin(flipper.angle) * flipper.length, | |
}; | |
const flipper_surface = Surface{ .a = flipper.pos, .b = flipper_end }; | |
const flipper_normal = flipper_surface.normal(); | |
const ball_collision_point = ball.pos.add(flipper_normal.mul(-ball.r)); | |
const resolution = flipper_surface.collisionResolution(ball_collision_point, ball.velocity.mul(delta)); | |
if (resolution) |r| { | |
const flipper_velocity = Vec2{ | |
.x = -@sin(flipper.angle) * flipper_speed * flipper.length, | |
.y = @cos(flipper.angle) * flipper_speed * flipper.length, | |
}; | |
physics.applyCollision(ball, r, flipper_normal, flipper_velocity, delta, 1.5); | |
} | |
} | |
// Bumper collisions | |
for (state.bumpers) |bumper| { | |
const to_ball = ball.pos.sub(bumper.pos); | |
const distance = to_ball.length(); | |
if (distance < ball.r + bumper.radius) { | |
const normal = to_ball.normalized(); | |
ball.velocity = ball.velocity.add(normal.mul(bumper_force)); | |
state.score += 10; | |
} | |
} | |
// Target collisions | |
for (state.targets) |*target| { | |
if (!target.is_hit) { | |
const target_surface = Surface{ | |
.a = Pos2{ .x = target.pos.x, .y = target.pos.y }, | |
.b = Pos2{ .x = target.pos.x + target.width, .y = target.pos.y }, | |
}; | |
const target_normal = target_surface.normal(); | |
const ball_collision_point = ball.pos.add(target_normal.mul(-ball.r)); | |
const resolution = target_surface.collisionResolution(ball_collision_point, ball.velocity.mul(delta)); | |
if (resolution) |r| { | |
physics.applyCollision(ball, r, target_normal, Vec2.zero, delta, 1.0); | |
target.is_hit = true; | |
state.score += 50; | |
} | |
} | |
} | |
} | |
} | |
pub export fn render(canvas_width: usize, canvas_height: usize) void { | |
@memset(chamber_pixels, 0xffffffff); | |
// Render flippers | |
for (state.flippers) |flipper| { | |
const flipper_end = Pos2{ | |
.x = flipper.pos.x + @cos(flipper.angle) * flipper.length, | |
.y = flipper.pos.y + @sin(flipper.angle) * flipper.length, | |
}; | |
renderLine(flipper.pos, flipper_end, canvas_width, canvas_height); | |
} | |
// Render bumpers | |
for (state.bumpers) |bumper| { | |
renderCircle(bumper.pos, bumper.radius, canvas_width, canvas_height); | |
} | |
// Render targets | |
for (state.targets) |target| { | |
if (!target.is_hit) { | |
renderRectangle(target.pos, target.width, target.height, canvas_width, canvas_height); | |
} | |
} | |
} | |
fn renderLine(start: Pos2, end: Pos2, canvas_width: usize, canvas_height: usize) void { | |
const start_x_px = to_x_px(start.x, canvas_width); | |
const start_y_px = to_y_px(start.y, canvas_width, canvas_height); | |
const end_x_px = to_x_px(end.x, canvas_width); | |
const end_y_px = to_y_px(end.y, canvas_width, canvas_height); | |
const dx = @abs(@as(i32, @intCast(end_x_px)) - @as(i32, @intCast(start_x_px))); | |
const dy = -@abs(@as(i32, @intCast(end_y_px)) - @as(i32, @intCast(start_y_px))); | |
var err = dx + dy; | |
var x = start_x_px; | |
var y = start_y_px; | |
while (true) { | |
chamber_pixels[y * canvas_width + x] = 0xff000000; | |
if (x == end_x_px and y == end_y_px) break; | |
const e2 = 2 * err; | |
if (e2 >= dy) { | |
if (x == end_x_px) break; | |
err += dy; | |
x += if (start_x_px < end_x_px) 1 else -1; | |
} | |
if (e2 <= dx) { | |
if (y == end_y_px) break; | |
err += dx; | |
y += if (start_y_px < end_y_px) 1 else -1; | |
} | |
} | |
} | |
fn renderCircle(center: Pos2, radius: f32, canvas_width: usize, canvas_height: usize) void { | |
const center_x_px = to_x_px(center.x, canvas_width); | |
const center_y_px = to_y_px(center.y, canvas_width, canvas_height); | |
const radius_px = @intCast(i32, radius * @as(f32, canvas_width)); | |
var x = radius_px - 1; | |
var y = 0; | |
var dx = 1; | |
var dy = 1; | |
var err = dx - (radius_px << 1); | |
while (x >= y) { | |
drawCirclePoints(center_x_px, center_y_px, x, y, canvas_width); | |
drawCirclePoints(center_x_px, center_y_px, y, x, canvas_width); | |
if (err <= 0) { | |
y += 1; | |
err += dy; | |
dy += 2; | |
} | |
if (err > 0) { | |
x -= 1; | |
dx += 2; | |
err += dx - (radius_px << 1); | |
} | |
} | |
} | |
fn drawCirclePoints(cx: usize, cy: usize, x: i32, y: i32, canvas_width: usize) void { | |
setPixel(cx + x, cy + y, canvas_width); | |
setPixel(cx + y, cy + x, canvas_width); | |
setPixel(cx - y, cy + x, canvas_width); | |
setPixel(cx - x, cy + y, canvas_width); | |
setPixel(cx - x, cy - y, canvas_width); | |
setPixel(cx - y, cy - x, canvas_width); | |
setPixel(cx + y, cy - x, canvas_width); | |
setPixel(cx + x, cy - y, canvas_width); | |
} | |
fn renderRectangle(pos: Pos2, width: f32, height: f32, canvas_width: usize, canvas_height: usize) void { | |
const x_px = to_x_px(pos.x, canvas_width); | |
const y_px = to_y_px(pos.y, canvas_width, canvas_height); | |
const width_px = @intCast(usize, width * @as(f32, canvas_width)); | |
const height_px = @intCast(usize, height * @as(f32, canvas_height)); | |
for (y_px..(y_px + height_px)) |y| { | |
for (x_px..(x_px + width_px)) |x| { | |
setPixel(x, y, canvas_width); | |
} | |
} | |
} | |
fn setPixel(x: usize, y: usize, canvas_width: usize) void { | |
if (x < canvas_width and y < chamber_pixels.len / canvas_width) { | |
chamber_pixels[y * canvas_width + x] = 0xff000000; | |
} | |
} | |
fn to_x_px(x: f32, canvas_width: usize) usize { | |
return @intCast(usize, x * @as(f32, canvas_width)); | |
} | |
fn to_y_px(y: f32, canvas_width: usize, canvas_height: usize) usize { | |
return @intCast(usize, y * @as(f32, canvas_height)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment