Skip to content

Instantly share code, notes, and snippets.

@twobob
Created July 12, 2024 13:56
Show Gist options
  • Save twobob/76cfc68070e456528107265dbb476634 to your computer and use it in GitHub Desktop.
Save twobob/76cfc68070e456528107265dbb476634 to your computer and use it in GitHub Desktop.
pinball.zig a not at all tested wasmmodule
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