From 6c2d724621378578e6a17e8bed2950160e132326 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Fri, 12 Jun 2020 19:34:43 +0200 Subject: [PATCH] Breakout clone to demonstrate Allegro & Zig. --- .gitignore | 5 + LICENSE | 5 + README.md | 67 +++++++++ assets/ball.png | Bin 0 -> 583 bytes assets/brick.png | Bin 0 -> 353 bytes assets/font.ttf | Bin 0 -> 10080 bytes assets/paddle.png | Bin 0 -> 799 bytes assets/particle.png | Bin 0 -> 195 bytes build.zig | 33 ++++ src/allegro.c | 43 ++++++ src/allegro.zig | 130 ++++++++++++++++ src/breakout.zig | 121 +++++++++++++++ src/game.zig | 355 ++++++++++++++++++++++++++++++++++++++++++++ src/geometry.zig | 80 ++++++++++ 14 files changed, 839 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/ball.png create mode 100644 assets/brick.png create mode 100644 assets/font.ttf create mode 100644 assets/paddle.png create mode 100644 assets/particle.png create mode 100644 build.zig create mode 100644 src/allegro.c create mode 100644 src/allegro.zig create mode 100644 src/breakout.zig create mode 100644 src/game.zig create mode 100644 src/geometry.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b663e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Visual Studio Code +.vscode + +# Zig +zig-cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5792599 --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +Copyright 2020 Sander Schobers + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b1d68a --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Breakout + +A Breakout clone writting using [Allegro](https://liballeg.org/) and the [Zig programming language](https://ziglang.org/). + +## How to play + +Move the paddle with the `left` and `right` keys (or `A` and `D` keys). Start a new game with `R` and enable god mode with `G`. `Escape` exits the game. + +## Building + +Run `zig build play` + +### Dependencies +- [Allegro 5 development libraries](https://liballeg.org/download.html). +- [Zig compiler](https://ziglang.org/download/). + +## License + +This project is ICS licensed. + +## Licenses of third parties + +### Allegro + +Copyright © 2008-2010 the Allegro 5 Development Team + +This software is provided ‘as-is’, without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +This notice may not be removed or altered from any source distribution. + +### Zig + +The MIT License (Expat) + +Copyright (c) 2015 Andrew Kelley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +### Kenney Puzzle Pack + +You're free to use these game assets in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but is greatly appreciated! + +### Thaleah font + +CC-BY 3.0 licensed. diff --git a/assets/ball.png b/assets/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..d6db2be34bef4b95a4ab79bcea4081f86bbea69f GIT binary patch literal 583 zcmV-N0=WH&P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0o+MMK~zXfrIkC1 z0znW!y}=&ABkYm(2p(aNu;3>!5D~<{z)8#}FcV=C69pacgP_6){=(jV%~Uhv&Zl=* zD0=!+ud1uc>PjW&*K9VcjYi``kDeC)MZ9>gny3F`z1?oVwOXyN21R;QMG~LsqEl#~ zUa!AvPeOr>$7ALDzM4*_YCfME;hVMLaA=L9gKj1qs{-{rPlaKqj>lu6V5r~k8w2QO zdagGE4&Y50@O3}oeUS26$LbR(2V9_06sha=YUFe}soid8J%?rBxG#xvx&GyHQP1bo z3UHiRE|(@GuuMqwq{ae?a`E=@c%;A>5j_}j;%gP^lSvQ+PT>3fo&qx$FpSu1AF!!c zSMd3KR=3;Dz!{lmfFnX-tSlA_wcT!=yv_>QX0tI2;ibTAkih+Z?`(jISglqDngUBG zD0bOoT(8&W28YAphY{A;O9Ih>A(dg1HNn{1`ISV`@iy0XB4ude`@%$AjK*4XGE{-7?&TnV#6({KHt^$b2`C#zkO%9QRwY${Lk-fe!VMWUeo_|VR@#p}kQ`CJ#&6x;W!ICQgc{@cW$ zBXfM?3#R6f-M$9#K8$x5|FGO(_Ik&h)8xZ=OyM~LpOcgP1C<>zjQ?ahJffFA;W)C{ zm`$>|=iqO{8{ccc+!K#Y*df|1_2Ba9$tp6(7O4HO<=o9=a>!fAg>lxyV$OOdodfx* wYnWy&tBn3@$yL^K=9Uy^+f9g3-Ezopr0OGuah5!Hn literal 0 HcmV?d00001 diff --git a/assets/font.ttf b/assets/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..91fc7dc3916500aaa192c2fd9da4467af1e5870a GIT binary patch literal 10080 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 literal 0 HcmV?d00001 diff --git a/assets/particle.png b/assets/particle.png new file mode 100644 index 0000000000000000000000000000000000000000..98f759893d3f6d4d8808d7f5f80d25d6b9eb46bc GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>VM%xNb!1@J z*w6hZkrl}2EbxddW?R_tv?-vUvJc6@j7vM&7a@j-`5DVeRM2J@C|6Jn4qb^6S76VC9y_ +#include +#include + +void clear_to_color(float r, float g, float b) +{ + ALLEGRO_COLOR color; + color.r = r; + color.g = g; + color.b = b; + color.a = 1; + al_clear_to_color(color); +} + +void draw_filled_rectangle(float x1, float y1, float x2, float y2, float r, float g, float b, float a) +{ + ALLEGRO_COLOR color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + al_draw_filled_rectangle(x1, y1, x2, y2, color); +} + +void draw_text(ALLEGRO_FONT *font, float r, float g, float b, float a, float x, float y, int flags, const char *text) +{ + ALLEGRO_COLOR color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + al_draw_text(font, color, x, y, flags, text); +} + +void draw_tinted_bitmap(ALLEGRO_BITMAP *bitmap, float r, float g, float b, float a, float dx, float dy, int flags) +{ + ALLEGRO_COLOR tint; + tint.r = r; + tint.g = g; + tint.b = b; + tint.a = a; + al_draw_tinted_bitmap(bitmap, tint, dx, dy, flags); +} diff --git a/src/allegro.zig b/src/allegro.zig new file mode 100644 index 0000000..da7105f --- /dev/null +++ b/src/allegro.zig @@ -0,0 +1,130 @@ +pub usingnamespace @cImport({ + @cInclude("allegro5/allegro5.h"); + @cInclude("allegro5/allegro_font.h"); + @cInclude("allegro5/allegro_image.h"); + @cInclude("allegro5/allegro_primitives.h"); + @cInclude("allegro5/allegro_ttf.h"); +}); + +pub const Bitmap = struct { + bitmap: *ALLEGRO_BITMAP, + + pub fn init(path: var) !Bitmap { + const bitmap = al_load_bitmap(path) orelse return error.LoadingBitmap; + return Bitmap{ .bitmap = bitmap }; + } + + pub fn destroy(self: Bitmap) void { + al_destroy_bitmap(self.bitmap); + } + + pub fn draw(self: Bitmap, x: f32, y: f32) void { + al_draw_bitmap(self.bitmap, x, y, 0); + } + + pub fn drawTinted(self: Bitmap, x: f32, y: f32, tint: Color) void { + draw_tinted_bitmap(self.bitmap, tint.r, tint.g, tint.b, tint.a, x, y, 0); + } +}; + +pub const ColorU8 = struct { + r: u8, + g: u8, + b: u8, + a: u8, + + pub fn rgb(r: u8, g: u8, b: u8) ColorU8 { + return ColorU8{ + .r = r, + .g = g, + .b = b, + .a = 0xff, + }; + } + + pub fn rgba(r: u8, g: u8, b: u8, a: u8) ColorU8 { + return ColorU8{ + .r = r, + .g = g, + .b = b, + .a = a, + }; + } + + pub fn toColor(self: ColorU8) Color { + return Color{ + .r = @intToFloat(f32, self.r) / 255, + .g = @intToFloat(f32, self.g) / 255, + .b = @intToFloat(f32, self.b) / 255, + .a = @intToFloat(f32, self.a) / 255, + }; + } +}; + +pub const Color = struct { + r: f32, + g: f32, + b: f32, + a: f32, + + pub fn rgb(r: f32, g: f32, b: f32) Color { + return Color{ + .r = r, + .g = g, + .b = b, + .a = 0xff, + }; + } + + pub fn rgba(r: f32, g: f32, b: f32, a: f32) Color { + return Color{ + .r = r, + .g = g, + .b = b, + .a = a, + }; + } + + pub fn alpha(self: Color, a: f32) Color { + const pre = a / self.a; + return Color{ + .r = self.r * pre, + .g = self.g * pre, + .b = self.b * pre, + .a = a, + }; + } +}; + +pub const Font = struct { + font: *ALLEGRO_FONT, + + pub fn init(path: var, size: i32) !Font { + const font = al_load_font(path, size, 0) orelse return error.LoadingFont; + return Font{ .font = font }; + } + + pub fn destroy(self: Font) void { + al_destroy_font(self.font); + } + + pub fn drawText(self: Font, c: Color, x: f32, y: f32, flags: u32, text: []const u8) void { + draw_text(self.font, c.r, c.g, c.b, c.a, x, y, flags, text.ptr); + } +}; + +pub const Primitives = struct { + pub fn drawFilledRectangle(x1: f32, y1: f32, x2: f32, y2: f32, c: Color) void { + draw_filled_rectangle(x1, y1, x2, y2, c.r, c.g, c.b, c.a); + } +}; + +pub fn clearToColor(c: Color) void { + clear_to_color(c.r, c.g, c.b); +} + +// Forward declarations of C functions (defined in allegro.c). These are used to work around a limitation of Zig (can't pass structs smaller than 16 bytes to a C ABI, see: https://github.com/ziglang/zig/issues/1481). +pub extern fn clear_to_color(r: f32, g: f32, b: f32) void; +pub extern fn draw_filled_rectangle(x1: f32, y1: f32, x2: f32, y2: f32, r: f32, g: f32, b: f32, a: f32) void; +pub extern fn draw_text(font: *ALLEGRO_FONT, r: f32, g: f32, b: f32, a: f32, x: f32, y: f32, flags: u32, text: [*]const u8) void; +pub extern fn draw_tinted_bitmap(bitmap: *ALLEGRO_BITMAP, r: f32, g: f32, b: f32, a: f32, dx: f32, dy: f32, flags: u32) void; diff --git a/src/breakout.zig b/src/breakout.zig new file mode 100644 index 0000000..9297bea --- /dev/null +++ b/src/breakout.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const al = @import("allegro.zig"); +usingnamespace @import("game.zig"); + +pub fn main() anyerror!void { + if (!al.al_install_system(al.ALLEGRO_VERSION_INT, null)) { + return error.AllegroInstall; + } + defer al.al_uninstall_system(); + + al.al_set_new_window_title("Breakout - Allegro 5 and Zig demo"); + const header: f32 = 64; + al.al_set_new_display_option(al.ALLEGRO_VSYNC, 2, al.ALLEGRO_SUGGEST); + const disp = al.al_create_display(@floatToInt(c_int, Game.area.width()), @floatToInt(c_int, Game.area.height() + header)); + defer al.al_destroy_display(disp); + + if (!al.al_install_keyboard()) { + return error.AllegroInstallKeyboard; + } + if (!al.al_init_font_addon()) { + return error.AllegroInitFontAddon; + } + if (!al.al_init_image_addon()) { + return error.AllegroInitImageAddon; + } + if (!al.al_init_primitives_addon()) { + return error.AllegroInitPrimitivesAddon; + } + if (!al.al_init_ttf_addon()) { + return error.AllegroInitTrueTypeFontAddon; + } + + var queue = al.al_create_event_queue(); + defer al.al_destroy_event_queue(queue); + + al.al_register_event_source(queue, al.al_get_display_event_source(disp)); + al.al_register_event_source(queue, al.al_get_keyboard_event_source()); + + const font = try al.Font.init("assets/font.ttf", 48); + defer font.destroy(); + + const paddle = try al.Bitmap.init("assets/paddle.png"); + defer paddle.destroy(); + const brick = try al.Bitmap.init("assets/brick.png"); + defer brick.destroy(); + const ball = try al.Bitmap.init("assets/ball.png"); + defer ball.destroy(); + const particle = try al.Bitmap.init("assets/particle.png"); + defer particle.destroy(); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator: *std.mem.Allocator = &arena.allocator; + var game = Game.init(allocator); + + var quit = false; + var previous_frame = std.time.milliTimestamp(); + const back_color = al.ColorU8.rgb(0xef, 0xef, 0xef).toColor(); + const text_color = al.ColorU8.rgb(0x1d, 0x1d, 0x1d).toColor(); + const offset = Game.area.min.add(Point.init(0, -header)); + const top_bar_back_color = al.ColorU8.rgb(0xda, 0xda, 0xda).toColor(); + var score_buffer: [14]u8 = undefined; + while (!quit) { + al.clearToColor(back_color); + al.Primitives.drawFilledRectangle(0, 0, Game.area.width(), header, top_bar_back_color); + font.drawText(text_color, 16, 12, 0, "Welcome to Allegro!"); + const score = std.fmt.bufPrint(score_buffer[0..], "Score: {:0>6}", .{game.score}) catch "-1"; + font.drawText(text_color, Game.area.width() - 16, 12, al.ALLEGRO_ALIGN_RIGHT, score); + for (game.bricks) |b, i| { + if (b.destroyed) continue; + const position = b.bounds.min; + const row = i / Game.bricks_per_row; + const color = Game.row_colors[row]; + brick.drawTinted(position.x - offset.x, position.y - offset.y, color); + } + const paddle_position = game.paddle.position.subtract(offset).subtract(Paddle.size.multiply(0.5)); + paddle.draw(paddle_position.x, paddle_position.y); + const ball_position = game.ball.position.subtract(offset).subtract2D(Ball.radius, Ball.radius); + ball.draw(ball_position.x, ball_position.y); + for (game.particles.items) |particles| { + const color = if (particles.t > 0.2) particles.color.alpha((0.4 - particles.t) * 5) else particles.color; + for (particles.particles) |p| { + particle.drawTinted(p.position.x - offset.x - 5, p.position.y - offset.y - 5, color); + } + } + al.al_flip_display(); + + var event: al.ALLEGRO_EVENT = undefined; + while (al.al_get_next_event(queue, &event)) { + switch (event.type) { + al.ALLEGRO_EVENT_DISPLAY_CLOSE => quit = true, + al.ALLEGRO_EVENT_KEY_DOWN => { + switch (event.keyboard.keycode) { + al.ALLEGRO_KEY_A, al.ALLEGRO_KEY_LEFT => game.keyDown(Key.MovePaddleLeft), + al.ALLEGRO_KEY_D, al.ALLEGRO_KEY_RIGHT => game.keyDown(Key.MovePaddleRight), + al.ALLEGRO_KEY_G => game.god_mode = !game.god_mode, + al.ALLEGRO_KEY_R => { + game.destroy(); + game = Game.init(allocator); + }, + al.ALLEGRO_KEY_ESCAPE => quit = true, + else => {}, + } + }, + al.ALLEGRO_EVENT_KEY_UP => { + switch (event.keyboard.keycode) { + al.ALLEGRO_KEY_A, al.ALLEGRO_KEY_LEFT => game.keyUp(Key.MovePaddleLeft), + al.ALLEGRO_KEY_D, al.ALLEGRO_KEY_RIGHT => game.keyUp(Key.MovePaddleRight), + else => {}, + } + }, + else => {}, + } + } + const current_frame = std.time.milliTimestamp(); + const duration = @intToFloat(f32, current_frame - previous_frame) * 0.001; + game.update(duration); + previous_frame = current_frame; + } +} diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..c936454 --- /dev/null +++ b/src/game.zig @@ -0,0 +1,355 @@ +pub usingnamespace @import("geometry.zig"); +const allegro = @import("allegro.zig"); +const Color = allegro.Color; +const ColorU8 = allegro.ColorU8; +const std = @import("std"); +const math = std.math; + +pub var prng = std.rand.DefaultPrng.init(0); + +pub const Ball = struct { + pub const radius: f32 = 11; + pub const speed: f32 = 360; + + angle: f32 = -math.pi * 0.25, + position: Point = Point.init(0, Game.bricks_height + Paddle.offset - radius), + + pub fn bounceCircle(self: *Ball, center: Point, circle_radius: f32) Point { + const bounce_distance = circle_radius + Ball.radius; + const ball_distance = self.position.distanceTo(center); + const overshoot = bounce_distance - ball_distance; + + // translate back to point where ball is supposed to bounce of the circle + self.move(-overshoot); + const bounce = self.position; + + // calculate new angle + const angle = math.atan2(f32, center.y - self.position.y, center.x - self.position.x); // angle of vector from ball to center + self.angle = -math.pi + 2 * angle - self.angle; + + // bounce + self.move(overshoot); + + return bounce; + } + + pub fn bounceCorner(self: *Ball, corner: Point) Point { + const ball_distance = self.position.distanceTo(corner); + const overshoot = radius - ball_distance; + + self.angle = -math.pi + self.angle; + self.move(overshoot); + const bounce = self.position; + self.move(overshoot); + return bounce; + } + + fn bounceHorizontalObstable(self: *Ball, overshoot: f32) Point { + const bounce = self.position.subtract2D(0, overshoot); + self.position = bounce.subtract2D(0, overshoot); + self.angle = -self.angle; + return bounce; + } + + pub fn bounceLeftObstacle(self: *Ball, obstacle: f32) Point { + const overshoot = (self.position.x - radius) - obstacle; + return self.bounceVerticalObstable(overshoot); + } + + pub fn bounceRightObstacle(self: *Ball, obstacle: f32) Point { + const overshoot = (self.position.x + radius) - obstacle; + return self.bounceVerticalObstable(overshoot); + } + + pub fn bounceBottomObstacle(self: *Ball, obstacle: f32) Point { + const overshoot = (self.position.y + radius) - obstacle; + return self.bounceHorizontalObstable(overshoot); + } + + pub fn bounceTopObstacle(self: *Ball, obstacle: f32) Point { + const overshoot = (self.position.y - radius) - obstacle; + return self.bounceHorizontalObstable(overshoot); + } + + fn bounceVerticalObstable(self: *Ball, overshoot: f32) Point { + const bounce = self.position.subtract2D(overshoot, 0); + self.position = bounce.subtract2D(overshoot, 0); + self.angle = math.pi - self.angle; + return bounce; + } + + pub fn move(self: *Ball, distance: f32) void { + self.position = self.position.add(Point.radial(self.angle).multiply(distance)); + } + + pub fn passedMax(value: f32, max: f32) bool { + return value + radius > max; + } + + pub fn passedMin(value: f32, min: f32) bool { + return value - radius < min; + } +}; + +pub const Brick = struct { + pub const gap: f32 = 8; + pub const size = Point.init(64, 32); + + destroyed: bool = false, + bounds: Rectangle, +}; + +pub const Game = struct { + pub const area = Rectangle{ + .min = Point.init(0 - 0.5 * bricks_width, 0), + .max = Point.init(0 + 0.5 * bricks_width, bricks_height + Paddle.offset + Paddle.size.y + Brick.gap), + }; + pub const bricks_height = @intToFloat(f32, rows) * Brick.size.y + (@intToFloat(f32, rows) + 1) * Brick.gap; + pub const bricks_per_row: usize = 12; + pub const bricks_width = @intToFloat(f32, bricks_per_row) * Brick.size.x + (@intToFloat(f32, bricks_per_row) + 1) * Brick.gap; + pub const row_colors = [rows]Color{ + ColorU8.rgb(171, 112, 234).toColor(), + ColorU8.rgb(239, 47, 115).toColor(), + ColorU8.rgb(246, 230, 101).toColor(), + ColorU8.rgb(140, 242, 133).toColor(), + ColorU8.rgb(50, 188, 255).toColor(), + }; + pub const rows: usize = 5; + + bricks: [rows * bricks_per_row]Brick = undefined, + paddle: Paddle = Paddle{}, + ball: Ball = Ball{}, + particles: std.ArrayList(Particles), + + score: u32 = 0, + god_mode: bool = false, + game_over: bool = false, + move_paddle_left: bool = false, + move_paddle_right: bool = false, + + pub fn init(allocator: *std.mem.Allocator) Game { + var game = Game{ + .particles = std.ArrayList(Particles).init(allocator), + }; + // game.allocator = allocator; + const gap = Point.init(Brick.gap, Brick.gap); + const brick = Brick.size.add(gap); + const top_left = Game.area.min.add(gap); + for (game.bricks) |_, i| { + const column = i % bricks_per_row; + const row = i / bricks_per_row; + const left = @intToFloat(f32, column) * brick.x + top_left.x; + const top = @intToFloat(f32, row) * brick.y + top_left.y; + game.bricks[i].bounds = Rectangle.init(left, top, left + Brick.size.x, top + Brick.size.y); + } + return game; + } + + pub fn destroy(self: *Game) void { + self.particles.deinit(); + } + + fn destroyBrick(self: *Game, row: usize, i: usize, bounce: Point) void { + const ii = row * bricks_per_row + i; + self.bricks[ii].destroyed = true; + self.score += @intCast(u32, (rows - row) * 5); + const particles = Particles.init(Game.row_colors[row], bounce); + self.particles.append(particles) catch unreachable; + } + + pub fn keyDown(self: *Game, key: Key) void { + switch (key) { + Key.MovePaddleLeft => self.move_paddle_left = true, + Key.MovePaddleRight => self.move_paddle_right = true, + else => unreachable, + } + } + + pub fn keyUp(self: *Game, key: Key) void { + switch (key) { + Key.MovePaddleLeft => self.move_paddle_left = false, + Key.MovePaddleRight => self.move_paddle_right = false, + else => unreachable, + } + } + + pub fn update(self: *Game, t: f32) void { + const max_increment = 0.01; + var increment = t; + while (increment > max_increment) { + self.update(max_increment); + increment -= max_increment; + } + if (self.game_over) { + return; + } + const ball_position = &self.ball.position; + self.ball.move(Ball.speed * increment); + + // bounce the ball + { + // left of game area + if (Ball.passedMin(ball_position.x, Game.area.min.x)) { + _ = self.ball.bounceLeftObstacle(Game.area.min.x); + } + // right of game area + if (Ball.passedMax(ball_position.x, Game.area.max.x)) { + _ = self.ball.bounceRightObstacle(Game.area.max.x); + } + // top of game area + if (Ball.passedMin(ball_position.y, Game.area.min.y)) { + _ = self.ball.bounceTopObstacle(Game.area.min.y); + } + // top of paddle area + if (Ball.passedMax(ball_position.y, Paddle.area.min.y)) { + const paddle_x = self.paddle.position.x; + const paddle_radius = 0.5 * Paddle.size.y; + const paddle_left = paddle_x - 0.5 * Paddle.size.x + paddle_radius; + const paddle_right = paddle_x + 0.5 * Paddle.size.x - paddle_radius; + // ball hits paddle in center part. + if (ball_position.x >= paddle_left and ball_position.x < paddle_right) { + _ = self.ball.bounceBottomObstacle(Paddle.area.min.y); + } else { + // ball hits paddle on rounded corners + const paddle_center = self.paddle.position.y; + const paddle_left_center = Point.init(paddle_left, paddle_center); + const paddle_right_center = Point.init(paddle_right, paddle_center); + const bounce_distance = Ball.radius + paddle_radius; + const bounce_distance2 = bounce_distance * bounce_distance; + if (ball_position.x < paddle_left and paddle_left_center.distanceTo2(ball_position.*) < bounce_distance2) { + _ = self.ball.bounceCircle(paddle_left_center, paddle_radius); + } + if (ball_position.x > paddle_right and paddle_right_center.distanceTo2(ball_position.*) < bounce_distance2) { + _ = self.ball.bounceCircle(paddle_right_center, paddle_radius); + } + } + } + // bottom of game area + if (Ball.passedMax(ball_position.y, Game.area.max.y)) { + if (self.god_mode) { + _ = self.ball.bounceBottomObstacle(Game.area.max.y); + } else { + self.game_over = true; + } + } + + // bricks (per row) + var row: usize = 0; + while (row < 5) { + defer row += 1; + const bricks = self.bricks[row * bricks_per_row .. row * bricks_per_row + bricks_per_row]; + // check if the outside of the ball is within radius-distance of the row top/bottom + if (!Ball.passedMin(ball_position.y, bricks[0].bounds.max.y) or !Ball.passedMax(ball_position.y, bricks[0].bounds.min.y)) { + continue; + } + for (bricks) |brick, i| { + // check if the outside of the ball is within radius-distance of the brick left/right side + if (brick.destroyed or !Ball.passedMin(ball_position.x, brick.bounds.max.x) or !Ball.passedMax(ball_position.x, brick.bounds.min.x)) { + continue; + } + const is_left_of = ball_position.x < brick.bounds.min.x; + const is_right_of = ball_position.x > brick.bounds.max.x; + if (!is_left_of and !is_right_of) { + // the center of the ball is within the brick left/right side (bounce of top/bottom). + const bounce = if (ball_position.y > brick.bounds.max.y) + self.ball.bounceTopObstacle(brick.bounds.max.y) + else + self.ball.bounceBottomObstacle(brick.bounds.min.y); + self.destroyBrick(row, i, bounce); + } else if (ball_position.y < brick.bounds.max.y and ball_position.y >= brick.bounds.min.y) { + // the center of the ball is to the left or right of the brick, within radius distance (bounce of left/right). + if (is_left_of) { + const bounce = self.ball.bounceRightObstacle(brick.bounds.min.x); + self.destroyBrick(row, i, bounce); + } else { // if (ball_position > brick.bounds.max.x) + const bounce = self.ball.bounceLeftObstacle(brick.bounds.max.x); + self.destroyBrick(row, i, bounce); + } + } else { + // bounce of corner + const corner = Point.init(if (is_left_of) brick.bounds.min.x else brick.bounds.max.x, if (ball_position.y < brick.bounds.min.y) brick.bounds.min.y else brick.bounds.max.y); + const bounce_distance2 = Ball.radius * Ball.radius; + if (self.ball.position.distanceTo2(corner) < bounce_distance2) { + const bounce = self.ball.bounceCorner(corner); + self.destroyBrick(row, i, bounce); + } + } + } + } + } + + // move the paddle + if (self.move_paddle_left and !self.move_paddle_right) { + self.paddle.position.x -= Paddle.speed * increment; + if (self.paddle.position.x < Paddle.area.min.x) { + self.paddle.position.x = Paddle.area.min.x; + } + } + if (self.move_paddle_right and !self.move_paddle_left) { + self.paddle.position.x += Paddle.speed * increment; + if (self.paddle.position.x > Paddle.area.max.x) { + self.paddle.position.x = Paddle.area.max.x; + } + } + + // update the particles + for (self.particles.items) |_, i| { + self.particles.items[i].update(increment); + } + while (self.particles.items.len > 0 and self.particles.items[0].t > 0.4) { + _ = self.particles.orderedRemove(0); + } + } +}; + +pub const Key = enum { + MovePaddleLeft, + MovePaddleRight, +}; + +pub const Paddle = struct { + pub const size = Point.init(104, 24); + pub const offset: f32 = 400; + pub const speed: f32 = 200; + pub const area = Rectangle.init(Game.area.min.x + 0.5 * size.x, Game.bricks_height + offset, Game.area.max.x - 0.5 * size.x, Game.bricks_height + offset + size.y); + + position: Point = Point.init(0, area.min.y + 0.5 * size.y), +}; + +pub const Particle = struct { + speed: f32, + angle: Point, + position: Point, +}; + +pub const Particles = struct { + pub const n: usize = 8; + + color: Color, + origin: Point, + particles: [n]Particle, + t: f32 = 0, + + pub fn init(color: Color, origin: Point) Particles { + var particles: [n]Particle = undefined; + for (particles) |_, i| { + particles[i] = Particle{ + .speed = prng.random.float(f32) * 80 + 80, + .angle = Point.radial(prng.random.float(f32) * 2 * math.pi), + .position = origin, + }; + } + return Particles{ + .color = color, + .origin = origin, + .particles = particles, + }; + } + + pub fn update(self: *Particles, dt: f32) void { + self.t += dt; + for (self.particles) |particle, i| { + self.particles[i].position = self.origin.add(particle.angle.multiply(self.t * particle.speed)); + } + } +}; diff --git a/src/geometry.zig b/src/geometry.zig new file mode 100644 index 0000000..fa11e7e --- /dev/null +++ b/src/geometry.zig @@ -0,0 +1,80 @@ +const math = @import("std").math; + +pub const Point = struct { + x: f32 = 0, + y: f32 = 0, + + pub fn init(x: f32, y: f32) Point { + return Point{ .x = x, .y = y }; + } + + pub fn add(self: Point, p: Point) Point { + return Point.init(self.x + p.x, self.y + p.y); + } + + pub fn add2D(self: Point, x: f32, y: f32) Point { + return Point.init(self.x + x, self.y + y); + } + + pub fn distanceTo(self: Point, other: Point) f32 { + return math.sqrt(self.distanceTo2(other)); + } + + pub fn distanceTo2(self: Point, other: Point) f32 { + const dx = self.x - other.x; + const dy = self.y - other.y; + return dx * dx + dy * dy; + } + + pub fn multiply(self: Point, factor: f32) Point { + return Point.init(self.x * factor, self.y * factor); + } + + pub fn radial(angle: f32) Point { + return Point.init(math.cos(angle), math.sin(angle)); + } + + pub fn subtract(self: Point, p: Point) Point { + return Point.init(self.x - p.x, self.y - p.y); + } + + pub fn subtract2D(self: Point, x: f32, y: f32) Point { + return Point.init(self.x - x, self.y - y); + } +}; + +pub const Rectangle = struct { + min: Point = Point{}, + max: Point = Point{}, + + pub fn init(x1: f32, y1: f32, x2: f32, y2: f32) Rectangle { + const swap_x = x1 > x2; + const swap_y = y1 > y2; + return Rectangle{ + .min = Point.init(if (swap_x) x2 else x1, if (swap_y) y2 else y1), + .max = Point.init(if (swap_x) x1 else x2, if (swap_y) y1 else y2), + }; + } + + pub fn add(self: Rectangle, p: Point) Rectangle { + return Rectangle{ + .min = self.min.add(p), + .max = self.max.add(p), + }; + } + + pub fn height(self: Rectangle) f32 { + return self.max.y - self.min.y; + } + + pub fn multiply(self: Rectangle, factor: f32) Rectangle { + return Rectangle{ + .min = Point.init(self.min.x * factor, self.min.y * factor), + .max = Point.init(self.max.x * factor, self.max.y * factor), + }; + } + + pub fn width(self: Rectangle) f32 { + return self.max.x - self.min.x; + } +};