Breakout clone to demonstrate Allegro & Zig.

This commit is contained in:
Sander Schobers 2020-06-12 19:34:43 +02:00
commit 6c2d724621
14 changed files with 839 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Visual Studio Code
.vscode
# Zig
zig-cache/

5
LICENSE Normal file
View File

@ -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.

67
README.md Normal file
View File

@ -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.

BIN
assets/ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

BIN
assets/brick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

BIN
assets/font.ttf Normal file

Binary file not shown.

BIN
assets/paddle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

BIN
assets/particle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

33
build.zig Normal file
View File

@ -0,0 +1,33 @@
const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("breakout", "src/breakout.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.addCSourceFile("src/allegro.c", &[_][]const u8{});
exe.linkSystemLibrary("c");
exe.linkSystemLibrary("allegro-5");
exe.linkSystemLibrary("allegro_font-5");
exe.linkSystemLibrary("allegro_image-5");
exe.linkSystemLibrary("allegro_primitives-5");
exe.linkSystemLibrary("allegro_ttf-5");
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
const play_step = b.step("play", "Play the game");
play_step.dependOn(&run_cmd.step);
}

43
src/allegro.c Normal file
View File

@ -0,0 +1,43 @@
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>
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);
}

130
src/allegro.zig Normal file
View File

@ -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;

121
src/breakout.zig Normal file
View File

@ -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;
}
}

355
src/game.zig Normal file
View File

@ -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));
}
}
};

80
src/geometry.zig Normal file
View File

@ -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;
}
};