From f7e3698cb124e0521f1e01acc9eef2f89e2e1aa4 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 17 Feb 2024 18:53:25 +0200 Subject: [PATCH] add basic button ui --- src/main-scene.zig | 357 ++++++++++++++++++++++++++++++++++--------- src/main.zig | 4 +- src/player-input.zig | 67 +++++--- src/ui.zig | 121 +++++++++++++++ 4 files changed, 459 insertions(+), 90 deletions(-) create mode 100644 src/ui.zig diff --git a/src/main-scene.zig b/src/main-scene.zig index 7e7d1c2..171fd92 100644 --- a/src/main-scene.zig +++ b/src/main-scene.zig @@ -3,6 +3,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const PlayerInput = @import("player-input.zig"); +const UI = @import("./ui.zig"); const friction = 0.99; const walkForce = 4000; @@ -51,12 +52,20 @@ const Enemy = struct { return Enemy{ .position = position, .health = 5.0, - .color = rl.ORANGE, + .color = rl.GOLD, .size = 50.0, .acceleration = 3000, .maxSpeed = 50, }; } + + fn vulnerable(self: Enemy) bool { + return self.invincibility == 0; + } + + fn alive(self: Enemy) bool { + return self.health > 0; + } }; const Player = struct { @@ -64,6 +73,24 @@ const Player = struct { velocity: rl.Vector2 = rl.Vector2.zero(), acceleration: rl.Vector2 = rl.Vector2.zero(), handDirection: rl.Vector2 = rl.Vector2{ .x = 1, .y = 0 }, + + input: PlayerInput = PlayerInput.init(), + + health: u32 = 3, + dead: bool = false, + invincibility: f32 = 0, + + fn getHandPosition(self: *const Player) rl.Vector2 { + return self.position.add(self.handDirection.scale(handDistance)); + } + + fn alive(self: Player) bool { + return self.health > 0; + } + + fn vulnerable(self: Player) bool { + return self.invincibility == 0; + } }; const Rope = struct { @@ -153,6 +180,14 @@ const Rope = struct { pos.* = pos.add(offset.scale(0.01)); } } + + fn lastNodePosition(self: *Rope) rl.Vector2 { + return self.nodePositions.get(self.nodePositions.len-1); + } + + fn lastNodeVelocity(self: *Rope) rl.Vector2 { + return self.nodeVelocities.get(self.nodeVelocities.len-1); + } }; allocator: Allocator, @@ -161,9 +196,12 @@ prng: std.rand.DefaultPrng, enemyTimer: f32 = 0, spawnedEnemiesCount: u32 = 0, killCount: u32 = 0, +timePassed: f32 = 0, enemies: std.ArrayList(Enemy), player: Player, rope: Rope, +ui: UI, +should_close: bool = false, camera: rl.Camera2D, @@ -173,14 +211,17 @@ pub fn init(allocator: Allocator) Self { var playerPosition = rl.Vector2{ .x = 0, .y = 0 }; var ropeSize: f32 = 200; var handPosition = playerPosition.add(rl.Vector2{ .x = handDistance, .y = 0}); + var player = Player{ .position = playerPosition }; + player.input.activate(); return Self { .allocator = allocator, .prng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())), .enemies = std.ArrayList(Enemy).init(allocator), - .player = .{ .position = playerPosition }, + .player = player, .rope = Rope.init(handPosition, handPosition.add(.{ .x = ropeSize, .y = 0.002 }), 10), - .camera = .{ .target = playerPosition } + .camera = .{ .target = playerPosition }, + .ui = UI.init() }; } @@ -189,10 +230,40 @@ pub fn reset(self: *Self) void { self.* = Self.init(self.allocator); } -pub fn deinit(self: Self) void { +pub fn deinit(self: *Self) void { + self.player.input.deactivate(); self.enemies.deinit(); } +fn tickPlayer(self: *Self) void { + const dt = rl.GetFrameTime(); + var player = &self.player; + var rope = &self.rope; + + player.acceleration = rl.Vector2.zero(); + + if (player.alive()) { + player.handDirection = player.input.getHandPosition(); + + const moveDir = player.input.getWalkDirection(); + player.acceleration = moveDir; + player.acceleration = rl.Vector2Scale(player.acceleration, walkForce); + } + + player.velocity = rl.Vector2Add(player.velocity, rl.Vector2Scale(player.acceleration, dt)); + player.velocity = rl.Vector2ClampValue(player.velocity, 0, maxSpeed); + player.velocity = rl.Vector2Scale(player.velocity, std.math.pow(f32, (1 - friction), dt)); + + player.position = rl.Vector2Add(player.position, rl.Vector2Scale(player.velocity, dt)); + + const handPosition = player.getHandPosition(); + + const rope_root_node: rl.Vector2 = rope.nodePositions.get(0); + rope.nodePositions.set(0, rope_root_node.lerp(handPosition, 0.25)); + + rope.update(dt); +} + fn tickEnemies(self: *Self) !void { var rng = self.prng.random(); const dt = rl.GetFrameTime(); @@ -205,21 +276,41 @@ fn tickEnemies(self: *Self) !void { self.enemyTimer = 0.5 + rng.float(f32) * 2; self.spawnedEnemiesCount += 1; const enemyPosition = rl.Vector2.randomOnUnitCircle(rng).scale(400); - try enemies.append(Enemy.initNormal(enemyPosition)); + try enemies.append(Enemy.initStrong(enemyPosition)); } + const deathBallPosition = rope.lastNodePosition(); + for (enemies.items) |*enemy| { const toPlayer = player.position.sub(enemy.position); - if (toPlayer.length() <= playerSize + enemy.size) { - self.reset(); - return; + if (player.vulnerable() and toPlayer.length() <= playerSize + enemy.size) { + self.damagePlayer(); } + if (enemy.vulnerable() and rl.Vector2Distance(enemy.position, deathBallPosition) < enemy.size + deathBallSize) { + self.damageEnemy(enemy); + } + + var acceleration = rl.Vector2.zero(); if (enemy.stunned == 0) { const directionToPlayer = toPlayer.normalize(); - enemy.velocity = enemy.velocity.add(directionToPlayer.scale(enemy.acceleration * dt)); + acceleration = acceleration.add(directionToPlayer); } + var enemyPushForce = rl.Vector2.zero(); + for (enemies.items) |*otherEnemy| { + if (otherEnemy == enemy) continue; + + const difference = enemy.position.sub(otherEnemy.position); + const enemyDistance = difference.length(); + if (otherEnemy.size + enemy.size > enemyDistance*1.2) { + enemyPushForce = enemyPushForce.add(difference.normalize()); + } + } + + acceleration = acceleration.add(enemyPushForce); + + enemy.velocity = enemy.velocity.add(acceleration.scale(enemy.acceleration * dt)); enemy.velocity = rl.Vector2ClampValue(enemy.velocity, 0, enemy.maxSpeed); enemy.velocity = rl.Vector2Scale(enemy.velocity, std.math.pow(f32, (1 - enemyFriction), dt)); enemy.position = enemy.position.add(enemy.velocity.scale(dt)); @@ -228,27 +319,15 @@ fn tickEnemies(self: *Self) !void { enemy.stunned = @max(0, enemy.stunned - dt); } - const deathBallPosition = rope.nodePositions.get(rope.nodePositions.len-1); - const deathBallVelocity = rope.nodeVelocities.get(rope.nodePositions.len-1); - - { + { // Remove dead enemies var i: usize = 0; while (i < enemies.items.len) { const enemy = &enemies.items[i]; - if (enemy.invincibility == 0) { - if (rl.Vector2Distance(enemy.position, deathBallPosition) < enemy.size + deathBallSize) { - enemy.health -= 1; - enemy.invincibility = 0.5; - enemy.stunned = 0.75; - enemy.velocity = deathBallVelocity.scale(20000); - } - - if (enemy.health == 0) { - self.killCount += 1; - _ = enemies.swapRemove(i); - continue; - } + if (enemy.health == 0) { + self.killCount += 1; + _ = enemies.swapRemove(i); + continue; } i += 1; @@ -256,55 +335,202 @@ fn tickEnemies(self: *Self) !void { } } +fn damagePlayer(self: *Self) void { + if (self.player.alive()) { + self.player.health -= 1; + } + + if (!self.player.alive()) { + self.player.input.deactivate(); + } +} + +fn damageEnemy(self: *Self, enemy: *Enemy) void { + const deathBallVelocity = self.rope.lastNodeVelocity(); + + if (enemy.alive()) { + enemy.health -= 1; + } + + enemy.invincibility = 0.5; + enemy.stunned = 0.75; + enemy.velocity = deathBallVelocity.scale(20000); +} + +fn getScreenSize() rl.Vector2 { + return rl.Vector2{ + .x = @floatFromInt(rl.GetScreenWidth()), + .y = @floatFromInt(rl.GetScreenHeight()) + }; +} + +fn allocDrawText( + allocator: Allocator, + comptime fmt: []const u8, + fmt_args: anytype, + x: i32, + y: i32, + font_size: i32, + color: rl.Color, +) !void { + const text = try std.fmt.allocPrintZ(allocator, fmt, fmt_args); + defer allocator.free(text); + rl.DrawText(text, x, y, font_size, color); +} + +const UIBox = struct { + x: f32, + y: f32, + width: f32, + height: f32, + + fn init(x: f32, y: f32, width: f32, height: f32) UIBox { + return UIBox{ + .x = x, + .y = y, + .width = width, + .height = height, + }; + } + + fn initScreen() UIBox { + const width: f32 = @floatFromInt(rl.GetScreenWidth()); + const height: f32 = @floatFromInt(rl.GetScreenHeight()); + return UIBox.init(0, 0, width, height); + } + + fn box(self: UIBox, x: f32, y: f32, width: f32, height: f32) UIBox { + return UIBox.init(self.x + x, self.y + y, width, height); + } + + fn rect(self: UIBox) rl.Rectangle { + return rl.Rectangle{ + .x = self.x, + .y = self.y, + .width = self.width, + .height = self.height, + }; + } + + fn left(self: UIBox) f32 { + return self.x; + } + fn center(self: UIBox) f32 { + return self.x + self.width/2; + } + fn right(self: UIBox) f32 { + return self.x + self.width; + } + + fn top(self: UIBox) f32 { + return self.y; + } + fn middle(self: UIBox) f32 { + return self.y + self.height/2; + } + fn bottom(self: UIBox) f32 { + return self.y + self.height; + } + + fn center_top(self: UIBox) rl.Vector2 { + return rl.Vector2{ .x = self.center(), .y = self.top() }; + } + fn center_middle(self: UIBox) rl.Vector { + return rl.Vector2{ .x = self.center(), .y = self.middle() }; + } + + fn margin(self: UIBox, amount: f32) UIBox { + return UIBox.init(self.x + amount, self.y + amount, self.width - 2*amount, self.height - 2*amount); + } +}; + +fn tickUI(self: *Self) !void { + self.ui.begin(); + defer self.ui.end(); + + var allocator = self.allocator; + const screen_box = UIBox.initScreen(); + const font = rl.GetFontDefault(); + + try allocDrawText(allocator, "{d}", .{self.spawnedEnemiesCount}, 10, 10, 24, rl.RED); + try allocDrawText(allocator, "{d}", .{self.killCount}, 10, 30, 24, rl.GREEN); + + const minutes_text = try std.fmt.allocPrint(allocator, "{d:.0}", .{self.timePassed/60}); + defer allocator.free(minutes_text); + + const seconds_text = try std.fmt.allocPrint(allocator, "{d:.3}", .{@mod(self.timePassed, 60)}); + defer allocator.free(seconds_text); + + const time_passed_text = try std.mem.concatWithSentinel(allocator, u8, &.{ minutes_text, ":", seconds_text }, 0); + defer allocator.free(time_passed_text); + + const minutes_text_z = try std.mem.concatWithSentinel(allocator, u8, &.{ minutes_text }, 0); + defer allocator.free(minutes_text_z); + + const font_size = 48; + const time_passed_width: f32 = @floatFromInt(rl.MeasureText(minutes_text_z, font_size) + rl.MeasureText(":000", font_size)); + const time_passed_pos = screen_box.center_top().add(.{ .x = -time_passed_width/2, .y = 30 }); + rl.DrawTextEx( + font, + time_passed_text, + time_passed_pos, + font_size, + 4.8, + rl.GREEN + ); + + if (!self.player.alive()) { + const modal_size = rl.Vector2{ .x = 200, .y = 400 }; + const modal = screen_box.box( + screen_box.center() - modal_size.x/2, + screen_box.middle() - modal_size.y/2, + modal_size.x, + modal_size.y + ); + const content = modal.margin(10); + + rl.DrawRectangleRec(modal.rect(), rl.RAYWHITE); + UI.drawTextAligned( + font, + "You died!", + content.center_top().add(.{ .x = 0, .y = 30 }), + 30, + rl.BLACK + ); + + if (self.ui.button("Restart?", content.left(), content.top() + 70, content.width, 30)) { + self.reset(); + } + + if (self.ui.button("Exit?", content.left(), content.top() + 120, content.width, 30)) { + self.should_close = true; + } + } +} + pub fn tick(self: *Self) !void { rl.ClearBackground(rl.BLACK); - const screenWidth: f32 = @floatFromInt(rl.GetScreenWidth()); - const screenHeight: f32 = @floatFromInt(rl.GetScreenHeight()); + const screenSize = getScreenSize(); - self.camera.offset.x = screenWidth/2; - self.camera.offset.y = screenHeight/2; + self.camera.offset.x = screenSize.x/2; + self.camera.offset.y = screenSize.y/2; self.camera.target = self.player.position; - const dt = rl.GetFrameTime(); var player = &self.player; var enemies = &self.enemies; var rope = &self.rope; - var allocator = self.allocator; - - player.handDirection = PlayerInput.getHandPosition(); - - const moveDir = PlayerInput.getWalkDirection(); - player.acceleration = moveDir; - player.acceleration = rl.Vector2Scale(player.acceleration, walkForce); - - player.velocity = rl.Vector2Add(player.velocity, rl.Vector2Scale(player.acceleration, dt)); - player.velocity = rl.Vector2ClampValue(player.velocity, 0, maxSpeed); - player.velocity = rl.Vector2Scale(player.velocity, std.math.pow(f32, (1 - friction), dt)); - - player.position = rl.Vector2Add(player.position, rl.Vector2Scale(player.velocity, dt)); - - const handPosition = player.position.add(player.handDirection.scale(handDistance)); - - const rope_root_node: rl.Vector2 = rope.nodePositions.get(0); - rope.nodePositions.set(0, rope_root_node.lerp(handPosition, 0.25)); - - rope.update(dt); - - const deathBallPosition = rope.nodePositions.get(rope.nodePositions.len-1); + self.tickPlayer(); try self.tickEnemies(); - { // UI - const enemyCountText = try std.fmt.allocPrintZ(allocator, "{d}", .{self.spawnedEnemiesCount}); - defer allocator.free(enemyCountText); - rl.DrawText(enemyCountText, 10, 10, 24, rl.RED); - - const killCountText = try std.fmt.allocPrintZ(allocator, "{d}", .{self.killCount}); - defer allocator.free(killCountText); - rl.DrawText(killCountText, 10, 30, 24, rl.GREEN); + if (self.player.alive()) { + self.timePassed += rl.GetFrameTime(); } + const deathBallPosition = rope.nodePositions.get(rope.nodePositions.len-1); + const handPosition = player.getHandPosition(); + rl.BeginMode2D(self.camera); { rl.DrawCircle(0, 0, 5, rl.GOLD); @@ -329,13 +555,6 @@ pub fn tick(self: *Self) !void { rl.GREEN ); - rl.DrawCircle( - @intFromFloat(player.position.x + moveDir.x * maxSpeed), - @intFromFloat(player.position.y + moveDir.y * maxSpeed), - 5, - rl.GREEN - ); - for (enemies.items) |*enemy| { var color = enemy.color; if (enemy.invincibility > 0 and @rem(enemy.invincibility, 0.2) < 0.1) { @@ -380,4 +599,6 @@ pub fn tick(self: *Self) !void { ); } rl.EndMode2D(); + + try self.tickUI(); } diff --git a/src/main.zig b/src/main.zig index af31981..5d832d1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,13 +10,13 @@ pub fn main() !void { rl.SetTargetFPS(60); rl.InitWindow(1200, 1200, "Step kill"); - rl.DisableCursor(); + rl.SetExitKey(.KEY_NULL); defer rl.CloseWindow(); var scene = MainScene.init(allocator); defer scene.deinit(); - while (!rl.WindowShouldClose()) { + while (!rl.WindowShouldClose() and !scene.should_close) { rl.BeginDrawing(); defer rl.EndDrawing(); diff --git a/src/player-input.zig b/src/player-input.zig index ec848bd..5e30074 100644 --- a/src/player-input.zig +++ b/src/player-input.zig @@ -1,11 +1,18 @@ const rl = @import("raylib"); const std = @import("std"); -const gamepad: i32 = 0; +gamepad: i32 = 0, +active: bool = false, + +last_hand_position: rl.Vector2 = rl.Vector2.zero(), +mouse_hand_position: rl.Vector2 = rl.Vector2.zero(), -var mouseHandPosition: rl.Vector2 = .{ .x = 0, .y = 0 }; const mouseHandRadius = 100.0; +pub fn init() @This() { + return @This(){}; +} + fn clampVector(vec: rl.Vector2) rl.Vector2 { if (vec.length2() > 1) { return vec.normalize(); @@ -30,10 +37,10 @@ fn getKeyboardWalkDirection() rl.Vector2 { dx += 1; } - return rl.Vector2{ .x = dx, .y = dy }; + return (rl.Vector2{ .x = dx, .y = dy }).normalize(); } -fn getGamepadWalkDirection() ?rl.Vector2 { +fn getGamepadWalkDirection(gamepad: i32) ?rl.Vector2 { if (!rl.IsGamepadAvailable(gamepad)) { return null; } @@ -48,16 +55,20 @@ fn getGamepadWalkDirection() ?rl.Vector2 { return clampVector(.{ .x = x, .y = y }); } -pub fn getWalkDirection() rl.Vector2 { - var walkDirection = getKeyboardWalkDirection().normalize(); - if (getGamepadWalkDirection()) |dir| { - walkDirection = dir; +pub fn getWalkDirection(self: *@This()) rl.Vector2 { + if (!self.active) { + return rl.Vector2.zero(); } - return walkDirection; + var walk_direction = getKeyboardWalkDirection(); + if (getGamepadWalkDirection(self.gamepad)) |dir| { + walk_direction = dir; + } + + return walk_direction; } -fn getGamepadHandPosition() ?rl.Vector2 { +fn getGamepadHandPosition(gamepad: i32) ?rl.Vector2 { if (!rl.IsGamepadAvailable(gamepad)) { return null; } @@ -72,19 +83,35 @@ fn getGamepadHandPosition() ?rl.Vector2 { return clampVector(.{ .x = x, .y = y }); } -fn getMouseHandPosition() rl.Vector2 { - const mouseDelta = rl.GetMouseDelta(); +fn getMouseHandPosition(self: *@This()) rl.Vector2 { + const mouse_delta = rl.GetMouseDelta(); - mouseHandPosition = clampVector(mouseHandPosition.add(mouseDelta.scale(1.0/mouseHandRadius))); - - return mouseHandPosition; + self.mouse_hand_position = clampVector(self.mouse_hand_position.add(mouse_delta.scale(1.0/mouseHandRadius))); + return self.mouse_hand_position; } -pub fn getHandPosition() rl.Vector2 { - var handPosition = getMouseHandPosition(); - if (getGamepadHandPosition()) |pos| { - handPosition = pos; +pub fn getHandPosition(self: *@This()) rl.Vector2 { + if (!self.active) { + return self.last_hand_position; } - return clampVector(handPosition); + var hand_position = self.getMouseHandPosition(); + if (getGamepadHandPosition(self.gamepad)) |pos| { + hand_position = pos; + } + + self.last_hand_position = hand_position; + return hand_position; +} + +pub fn activate(self: *@This()) void { + if (self.active) return; + rl.DisableCursor(); + self.active = true; +} + +pub fn deactivate(self: *@This()) void { + if (!self.active) return; + rl.EnableCursor(); + self.active = false; } diff --git a/src/ui.zig b/src/ui.zig new file mode 100644 index 0000000..7535d60 --- /dev/null +++ b/src/ui.zig @@ -0,0 +1,121 @@ +const rl = @import("raylib"); +const std = @import("std"); + +const gamepad: i32 = 0; + +selected_button: i32 = -1, +last_button_idx: u32 = 0, +button_count: u32 = 0, + +was_mouse_hot: bool = false, +is_down: bool = false, +is_released: bool = false, +first_render: bool = true, + +pub fn init() @This() { + return @This(){}; +} + +fn didMouseMove() bool { + const mouse_delta = rl.GetMouseDelta(); + return (mouse_delta.x != 0 or mouse_delta.y != 0) and !rl.IsCursorHidden(); +} + +pub fn button(self: *@This(), text: [:0]const u8, x: f32, y: f32, width: f32, height: f32) bool { + const button_idx = self.last_button_idx; + self.last_button_idx += 1; + self.button_count += 1; + + if (didMouseMove() and isMouseInBounds(x, y, width, height)) { + self.selected_button = @intCast(button_idx); + self.was_mouse_hot = true; + } + + var is_hot = self.selected_button == @as(i32, @intCast(button_idx)); + + const rect = rl.Rectangle{ + .x = x, + .y = y, + .width = width, + .height = height, + }; + if (is_hot and self.is_down) { + rl.DrawRectangleRec(rect, rl.RED); + } else if (is_hot) { + rl.DrawRectangleRec(rect, rl.ORANGE); + } else { + rl.DrawRectangleRec(rect, rl.GREEN); + } + + drawTextAligned(rl.GetFontDefault(), text, .{ .x = x + width/2, .y = y + height/2 }, height * 0.8, rl.BLACK); + + return is_hot and self.is_released; +} + +pub fn begin(self: *@This()) void { + if (self.first_render and self.button_count > 0) { + if (rl.IsGamepadAvailable(gamepad)) { + self.selected_button = 0; + } + self.first_render = false; + } + + self.last_button_idx = 0; + + self.was_mouse_hot = false; + self.is_down = false; + self.is_released = false; + + if (rl.IsGamepadAvailable(gamepad)) { + const btn = rl.GamepadButton.GAMEPAD_BUTTON_RIGHT_FACE_DOWN; + self.is_down = self.is_down or rl.IsGamepadButtonDown(gamepad, btn); + self.is_released = self.is_released or rl.IsGamepadButtonReleased(gamepad, btn); + + if (self.button_count > 0) { + const pressed_down = rl.IsGamepadButtonPressed(gamepad, .GAMEPAD_BUTTON_LEFT_FACE_DOWN); + const pressed_up = rl.IsGamepadButtonPressed(gamepad, .GAMEPAD_BUTTON_LEFT_FACE_UP); + if ((pressed_up or pressed_down) and self.selected_button == -1) { + self.selected_button = 0; + } else { + if (pressed_down) { + self.selected_button = @mod(self.selected_button + 1, @as(i32, @intCast(self.button_count))); + } + if (pressed_up) { + self.selected_button = @mod(self.selected_button - 1, @as(i32, @intCast(self.button_count))); + } + } + } + } + + if (!rl.IsCursorHidden()) { + const btn = rl.MouseButton.MOUSE_BUTTON_LEFT; + self.is_down = self.is_down or rl.IsMouseButtonDown(btn); + self.is_released = self.is_released or rl.IsMouseButtonReleased(btn); + } + + self.button_count = 0; +} + +pub fn end(self: *@This()) void { + if (!self.was_mouse_hot and didMouseMove()) { + self.selected_button = -1; + } +} + +pub fn drawTextAligned( + font: rl.Font, + text: [*:0]const u8, + position: rl.Vector2, + font_size: f32, + tint: rl.Color, +) void { + const default_font_size: f32 = 10; + const spacing: f32 = @max(font_size/default_font_size, 1); + const text_size = rl.MeasureTextEx(font, text, font_size, spacing); + rl.DrawTextEx(font, text, position.sub(text_size.scale(0.5)), font_size, spacing, tint); +} + +fn isMouseInBounds(x: f32, y: f32, width: f32, height: f32) bool { + const pos = rl.GetMousePosition(); + return (x <= pos.x and pos.x <= x + width) and (y <= pos.y and pos.y <= y + height); +}