diff --git a/README.md b/README.md index 4b04988..a8e0e55 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This is my collection of small example projects showcasing the Zig language, bui - [Breakout](#breakout) - [Snake](#snake) +- [Pong](#pong) - [OpenGL (SDL)](#opengl-sdl) - [C/C++/Zig](#cczig) @@ -24,6 +25,12 @@ Simple Breakout clone using SDL3 for video, audio, input, etc. How quickly can y ![Preview](snake/preview.gif) +## [Pong](pong) + +Simple Pong clone using SDL3 for video and input. + +![Preview](pong/preview.gif) + ## [OpenGL (SDL)](opengl-sdl) Creates a window using SDL3, then draws to it using OpenGL bindings generated by [zigglgen](https://github.com/castholm/zigglgen). diff --git a/pong/README.md b/pong/README.md new file mode 100644 index 0000000..fed64d3 --- /dev/null +++ b/pong/README.md @@ -0,0 +1,27 @@ + + +# Pong + +Simple Pong clone using SDL3 for video and input. + +Uses the [castholm/SDL](https://github.com/castholm/SDL) Zig package, which builds SDL3 from source using the Zig build system. + +![Preview](preview.gif) + +## Controls + +### keyboard + +- Paddle one: W and S for up and down, respectively. +- Paddle two: O and K for up and down, respectively. + +## Building + +Requires Zig `0.14.0` or `0.15.0-dev` (master). + +```sh +# Run the game +zig build run diff --git a/pong/build.zig b/pong/build.zig new file mode 100644 index 0000000..a3305cd --- /dev/null +++ b/pong/build.zig @@ -0,0 +1,35 @@ +// © 2024 Daniel Alves +// SPDX-License-Identifier: CC0-1.0 + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ + .name = "pong", + .root_module = exe_mod, + }); + + const sdl_dep = b.dependency("sdl", .{ + .target = target, + .optimize = optimize, + }); + const sdl_lib = sdl_dep.artifact("SDL3"); + + exe_mod.linkLibrary(sdl_lib); + + b.installArtifact(exe); + + const run_exe = b.addRunArtifact(exe); + run_exe.step.dependOn(b.getInstallStep()); + + const run = b.step("run", "Run the game"); + run.dependOn(&run_exe.step); +} diff --git a/pong/build.zig.zon b/pong/build.zig.zon new file mode 100644 index 0000000..a60086a --- /dev/null +++ b/pong/build.zig.zon @@ -0,0 +1,18 @@ +// © 2024 Daniel Alves +// SPDX-License-Identifier: CC0-1.0 + +.{ + .name = .breakout, + .version = "0.0.0", + .fingerprint = 0xdafcbd308058adbe, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + .sdl = .{ + .url = "git+https://github.com/castholm/SDL.git#dbb1b96360658f5845ff6fac380c4f13d7276dc2", + .hash = "sdl-0.2.0+3.2.8-7uIn9FxHfQE325TK7b0qpgt10G3x1xl-3ZMOfTzxUg3C", + }, + }, + .paths = .{ + "invalid", // Not intended for consumption as a package + }, +} diff --git a/pong/game.zig b/pong/game.zig new file mode 100644 index 0000000..c324ca9 --- /dev/null +++ b/pong/game.zig @@ -0,0 +1,214 @@ +const std = @import("std"); +const main = @import("main.zig"); +const c = @cImport({ + @cInclude("SDL3/SDL.h"); + @cInclude("SDL3/SDL_main.h"); +}); + +const object = @import("object.zig"); +const Object = object.Object; + +pub const WINDOW_WIDTH = 640; +pub const WINDOW_HEIGHT = 480; + +var elapsed_time_ptr: *f32 = &main.elapsed_time; + +const BALL_SIZE = 10; +const BALL_SPEED = 300; + +const PADDLE_WIDTH = 10; +const PADDLE_HEIGHT = 50; + +const Ball = Object(BALL_SIZE, BALL_SIZE); +const Paddle = Object(PADDLE_WIDTH, PADDLE_HEIGHT); + +var ball: Ball = undefined; +var paddle_one: Paddle = undefined; +var paddle_two: Paddle = undefined; + +var initial_ball_direction: i8 = 1; + +var score_one: u8 = 0; +var score_two: u8 = 0; + +pub const ControllerState = struct { + key_w: bool = false, + key_s: bool = false, + key_o: bool = false, + key_k: bool = false, +}; + +var controller_state: ControllerState = .{}; + +fn resetGameState() void { + std.time.sleep(2_000_000_000); + initialize(); +} + +fn countScore(score: *u8) void { + score.* += 1; + resetGameState(); +} + +fn handleBallCollisionWithWall() void { + const left_collision = ball.position.x < 0; + const right_collision = ball.position.x > WINDOW_WIDTH - ball.shape.w; + const top_collision = ball.position.y < 0; + const bottom_collision = ball.position.y > WINDOW_HEIGHT - ball.shape.h; + + if (left_collision) { + countScore(&score_two); + } + + if (right_collision) { + countScore(&score_one); + } + + if (top_collision) { + ball.velocity.y *= -1; + ball.setPosition(ball.position.x, 0); + } + + if (bottom_collision) { + ball.velocity.y *= -1; + ball.setPosition(ball.position.x, WINDOW_HEIGHT - ball.shape.h); + } +} + +fn handlePaddleCollisionWithWall(paddle: *Paddle) void { + if (paddle.position.y < 0) { + paddle.setPosition(paddle.position.x, 0); + } + + if (paddle.position.y > WINDOW_HEIGHT - paddle.shape.h) { + paddle.setPosition(paddle.position.x, WINDOW_HEIGHT - paddle.shape.h); + } +} + +fn checkBallCollisionWithPaddle(paddle: *Paddle) bool { + const min_x = ball.position.x - paddle.shape.w; + const max_x = ball.position.x + ball.shape.w; + + if (paddle.position.x > min_x and paddle.position.x < max_x) { + const min_y = ball.position.y - paddle.shape.h; + const max_y = ball.position.y + ball.shape.h; + + if (paddle.position.y > min_y and paddle.position.y < max_y) { + return true; + } + } + + return false; +} + +fn handleBallCollisionWithPaddle(paddle: *Paddle) void { + if (checkBallCollisionWithPaddle(paddle)) { + const min_collision_position = paddle_one.position.x + paddle_one.shape.w; + const max_collision_position = paddle_two.position.x - ball.shape.w; + + if (ball.velocity.x < 0 and ball.position.x <= min_collision_position) { + ball.position.x = min_collision_position; + } + + if (ball.velocity.x > 0 and ball.position.x >= max_collision_position) { + ball.position.x = max_collision_position; + } + + ball.velocity.x = -ball.velocity.x; + } +} + +fn handleGameOver() void { + if (score_one == 10 or score_two == 10) { + score_one = 0; + score_two = 0; + + resetGameState(); + } +} + +fn drawScore(renderer: ?*c.SDL_Renderer, score: u8, x: f32, y: f32) !void { + var buf: [10]u8 = undefined; + const text = try std.fmt.bufPrintZ(&buf, "SCORE {}", .{score}); + + _ = c.SDL_SetRenderScale(renderer, 2, 2); + _ = c.SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff); + _ = c.SDL_RenderDebugText(renderer, x, y, text.ptr); + _ = c.SDL_SetRenderScale(renderer, 1, 1); +} + +fn controlPaddleState(paddle: *Paddle, up: bool, down: bool) void { + var paddle_vel_y: f32 = 0; + if (up) paddle_vel_y -= 400; + if (down) paddle_vel_y += 400; + + paddle.velocity.y = paddle_vel_y; +} + +fn drawCenterDottedLine(renderer: ?*c.SDL_Renderer) void { + const x = WINDOW_WIDTH / 2; + var y: i32 = 3; + + while (y < WINDOW_WIDTH) : (y += 10) { + var dot = c.SDL_FRect{ .x = @floatFromInt(x), .y = @floatFromInt(y), .w = 4, .h = 4 }; + + _ = c.SDL_SetRenderDrawColor(renderer, 255, 255, 255, c.SDL_ALPHA_OPAQUE); + _ = c.SDL_RenderFillRect(renderer, &dot); + } +} + +pub fn initialize() void { + initial_ball_direction *= -1; + const initial_ball_direction_f32: f32 = @floatFromInt(initial_ball_direction); + + ball = Ball.init((WINDOW_WIDTH - BALL_SIZE) / 2, (WINDOW_HEIGHT - BALL_SIZE) / 2, BALL_SPEED * initial_ball_direction_f32, BALL_SPEED); + paddle_one = Paddle.init(10, (WINDOW_HEIGHT - PADDLE_HEIGHT) / 2, 0, 0); + paddle_two = Paddle.init(WINDOW_WIDTH - PADDLE_WIDTH - 10, (WINDOW_HEIGHT - PADDLE_HEIGHT) / 2, 0, 0); +} + +pub fn update() void { + ball.updatePositionByTime(elapsed_time_ptr.*); + handleBallCollisionWithWall(); + + paddle_one.updatePositionByTime(elapsed_time_ptr.*); + handlePaddleCollisionWithWall(&paddle_one); + + paddle_two.updatePositionByTime(elapsed_time_ptr.*); + handlePaddleCollisionWithWall(&paddle_two); + + handleBallCollisionWithPaddle(&paddle_one); + handleBallCollisionWithPaddle(&paddle_two); + + handleGameOver(); + + controlPaddleState(&paddle_one, controller_state.key_w, controller_state.key_s); + controlPaddleState(&paddle_two, controller_state.key_o, controller_state.key_k); +} + +pub fn draw(renderer: ?*c.SDL_Renderer) !void { + drawCenterDottedLine(renderer); + try drawScore(renderer, score_one, 8, 8); + try drawScore(renderer, score_two, WINDOW_WIDTH - 383, 8); + ball.draw(renderer); + paddle_one.draw(renderer); + paddle_two.draw(renderer); +} + +pub fn handleEvent(event: c.SDL_Event) !void { + switch (event.type) { + c.SDL_EVENT_QUIT => { + return error.Quit; + }, + c.SDL_EVENT_KEY_DOWN, c.SDL_EVENT_KEY_UP => { + const down = event.type == c.SDL_EVENT_KEY_DOWN; + switch (event.key.scancode) { + c.SDL_SCANCODE_W => controller_state.key_w = down, + c.SDL_SCANCODE_S => controller_state.key_s = down, + c.SDL_SCANCODE_O => controller_state.key_o = down, + c.SDL_SCANCODE_K => controller_state.key_k = down, + else => {}, + } + }, + else => {}, + } +} diff --git a/pong/main.zig b/pong/main.zig new file mode 100644 index 0000000..2fdc3e4 --- /dev/null +++ b/pong/main.zig @@ -0,0 +1,68 @@ +const c = @cImport({ + @cInclude("SDL3/SDL.h"); + @cInclude("SDL3/SDL_main.h"); +}); +const game = @import("game.zig"); + +const WINDOW_WIDTH = game.WINDOW_WIDTH; +const WINDOW_HEIGHT = game.WINDOW_HEIGHT; + +pub var last_time: u64 = 0; +pub var current_time: u64 = 0; +pub var elapsed_time: f32 = 0; + +pub fn createWindownAndRenderer() struct { *c.SDL_Window, *c.SDL_Renderer } { + c.SDL_SetMainReady(); + + _ = c.SDL_SetAppMetadata("Pong", "0.0.0", "sdl-examples.pong"); + _ = c.SDL_Init(c.SDL_INIT_VIDEO); + + const window: *c.SDL_Window, const renderer: *c.SDL_Renderer = create_window_and_renderer: { + var window: ?*c.SDL_Window = null; + var renderer: ?*c.SDL_Renderer = null; + _ = c.SDL_CreateWindowAndRenderer("Pong", WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer); + errdefer comptime unreachable; + + break :create_window_and_renderer .{ window.?, renderer.? }; + }; + + return .{ window, renderer }; +} + +fn render(renderer: ?*c.SDL_Renderer) !void { + _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, c.SDL_ALPHA_OPAQUE); + _ = c.SDL_RenderClear(renderer); + + try game.draw(renderer); + + _ = c.SDL_RenderPresent(renderer); +} + +pub fn main() !void { + const window: *c.SDL_Window, const renderer: *c.SDL_Renderer = createWindownAndRenderer(); + defer c.SDL_Quit(); + defer c.SDL_DestroyRenderer(renderer); + defer c.SDL_DestroyWindow(window); + + last_time = c.SDL_GetTicks(); + game.initialize(); + + main_loop: while (true) { + var event: c.SDL_Event = undefined; + + while (c.SDL_PollEvent(&event)) { + game.handleEvent(event) catch |err| { + if (err == error.Quit) break :main_loop; + }; + } + + { + current_time = c.SDL_GetTicks(); + elapsed_time = @as(f32, @floatFromInt(current_time - last_time)) / 1000; + game.update(); + last_time = c.SDL_GetTicks(); + } + + try render(renderer); + } +} diff --git a/pong/object.zig b/pong/object.zig new file mode 100644 index 0000000..0c48a1f --- /dev/null +++ b/pong/object.zig @@ -0,0 +1,56 @@ +const c = @cImport({ + @cInclude("SDL3/SDL.h"); + @cInclude("SDL3/SDL_main.h"); +}); + +const Position = struct { + x: f32, + y: f32, +}; + +const Velocity = struct { + x: f32, + y: f32, +}; + +pub fn Object(w: comptime_int, h: comptime_int) type { + return struct { + position: Position, + velocity: Velocity, + shape: c.SDL_FRect, + + pub fn init(pos_x: f32, pos_y: f32, vel_x: f32, vel_y: f32) @This() { + return @This(){ + .position = .{ .x = pos_x, .y = pos_y }, + .velocity = .{ .x = vel_x, .y = vel_y }, + .shape = c.SDL_FRect{ + .x = pos_x, + .y = pos_y, + .w = w, + .h = h, + }, + }; + } + + pub fn setPosition(self: *@This(), x: f32, y: f32) void { + self.position.x = x; + self.position.y = y; + + self.shape.x = x; + self.shape.y = y; + } + + + pub fn updatePositionByTime(self: *@This(), elapsed_time: f32) void { + const x = self.position.x + self.velocity.x * elapsed_time; + const y = self.position.y + self.velocity.y * elapsed_time; + + self.setPosition(x, y); + } + + pub fn draw(self: *@This(), renderer: ?*c.SDL_Renderer) void { + _ = c.SDL_SetRenderDrawColor(renderer, 255, 255, 255, c.SDL_ALPHA_OPAQUE); + _ = c.SDL_RenderFillRect(renderer, &self.shape); + } + }; +} diff --git a/pong/preview.gif b/pong/preview.gif new file mode 100644 index 0000000..1af4bac Binary files /dev/null and b/pong/preview.gif differ