From 25acd69a84af2f72e60cb5135280bf0c9deee116 Mon Sep 17 00:00:00 2001 From: Kapendev Date: Sun, 15 Dec 2024 21:08:03 +0200 Subject: [PATCH] Fixed ui bugs and made examples better. --- examples/README.md | 10 +++++- examples/games/coins.d | 34 ++++++++------------ examples/games/pong.d | 49 +++++++++++------------------ examples/ui/handle.d | 4 +-- examples/ui/hello.d | 2 +- source/parin/engine.d | 71 +++++++++++++++++++++++++++++++++--------- 6 files changed, 100 insertions(+), 70 deletions(-) diff --git a/examples/README.md b/examples/README.md index 0c3ced6..5d5519a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,12 @@ # Examples +This folder provides example projects to help you get started. + > [!NOTE] -> For examples that use textures, the [atlas.png](atlas.png) file must be downloaded and saved in the project's assets folder. +> If an example uses textures, +> be sure to download the [atlas.png](atlas.png) file and place it in the project's assets folder. + +## Example Categories +- [Intro](intro): Basic examples to get familiar with Parin. +- [Games](games): Examples focused on making simple games with Parin. +- [UI](ui): Examples demonstrating how to use the Parin UI toolkit. diff --git a/examples/games/coins.d b/examples/games/coins.d index 6487051..4870270 100644 --- a/examples/games/coins.d +++ b/examples/games/coins.d @@ -1,18 +1,17 @@ -/// This example shows how to create a simple game with Parin. +/// This example shows how to create a simple collect-the-coins game with Parin. + import parin; -// The game variables. auto player = Rect(16, 16); -auto playerSpeed = 120; auto coins = SparseList!Rect(); -auto coinSize = Vec2(8); auto maxCoinCount = 8; void ready() { lockResolution(320, 180); - - // Place the player and create the coins. Every coin will have a random starting position. + // Place the player at the center of the window. player.position = resolution * Vec2(0.5); + // Create the coins. Every coin will have a random starting position. + auto coinSize = Vec2(8); foreach (i; 0 .. maxCoinCount) { auto minPosition = Vec2(0, 40); auto maxPosition = resolution - coinSize - minPosition; @@ -27,24 +26,17 @@ void ready() { bool update(float dt) { // Move the player. - auto playerDirection = Vec2(); - if (Keyboard.left.isDown || 'a'.isDown) playerDirection.x = -1; - if (Keyboard.right.isDown || 'd'.isDown) playerDirection.x = 1; - if (Keyboard.up.isDown || 'w'.isDown) playerDirection.y = -1; - if (Keyboard.down.isDown || 's'.isDown) playerDirection.y = 1; - player.position += playerDirection * Vec2(playerSpeed * dt); - - // Check if the player is touching some coins and remove those coins. + auto playerDirection = Vec2( + Keyboard.right.isDown - Keyboard.left.isDown, + Keyboard.down.isDown - Keyboard.up.isDown, + ); + player.position += playerDirection * Vec2(120 * dt); + // Check if the player is touching coins and remove them. foreach (id; coins.ids) { - if (coins[id].hasIntersection(player)) { - coins.remove(id); - } + if (coins[id].hasIntersection(player)) coins.remove(id); } - // Draw the game. - foreach (coin; coins.items) { - drawRect(coin); - } + foreach (coin; coins.items) drawRect(coin); drawRect(player); if (coins.length == 0) { drawDebugText("You collected all the coins!", Vec2(8)); diff --git a/examples/games/pong.d b/examples/games/pong.d index 03eac75..e7d12f3 100644 --- a/examples/games/pong.d +++ b/examples/games/pong.d @@ -1,12 +1,10 @@ /// This example shows how to create a pong-like game with Parin. + import parin; -// The game variables. auto gameCounter = 0; - auto paddle1 = Rect(2, 25); auto paddle2 = Rect(2, 25); - auto ball = Rect(5, 5); auto ballDirection = Vec2(1, 1); auto ballSpeed = 120; @@ -21,61 +19,52 @@ void ready() { ball.position = center; } +// The objects in this game are centered. +// This means that rectangle data is divided into 2 parts, normal and centered. +// A normal rectangle holds the position of an object. +// A centered rectangle is used for collision checking and drawing. bool update(float dt) { - // The objects in this game are centered. - // This means that rectangle data is divided into 2 parts, normal and centered. - // A normal rectangle holds the position of an object. - // A centered rectangle is used for collision checking and drawing. - // Move the ball. ball.position += ballDirection * Vec2(ballSpeed * dt); // Check if the ball exited the screen from the left or right side. - if (ball.centerArea.leftPoint.x < 0) { - ball.position = resolution * Vec2(0.5); - paddle1.position.y = resolutionHeight * 0.5; - paddle2.position.y = resolutionHeight * 0.5; - gameCounter = 0; - } else if (ball.centerArea.rightPoint.x > resolutionWidth) { + if (ball.centerArea.leftPoint.x < 0 || ball.centerArea.rightPoint.x > resolutionWidth) { ball.position = resolution * Vec2(0.5); paddle1.position.y = resolutionHeight * 0.5; paddle2.position.y = resolutionHeight * 0.5; gameCounter = 0; } // Check if the ball exited the screen from the top or bottom side. - if (ball.centerArea.topPoint.y < 0) { + if (ball.centerArea.topPoint.y < 0 || ball.centerArea.bottomPoint.y > resolutionHeight) { ballDirection.y *= -1; - ball.position.y = ball.size.y * 0.5; - } else if (ball.centerArea.bottomPoint.y > resolutionHeight) { - ballDirection.y *= -1; - ball.position.y = resolutionHeight - ball.size.y * 0.5; } // Move paddle1. - paddle1.position.y = clamp(paddle1.position.y + wasd.y * ballSpeed * dt, paddle1.size.y * 0.5f, resolutionHeight - paddle1.size.y * 0.5f); + paddle1.position.y = clamp( + paddle1.position.y + wasd.y * ballSpeed * dt, + paddle1.size.y * 0.5f, + resolutionHeight - paddle1.size.y * 0.5f + ); // Move paddle2. - auto paddle2Target = ball.position.y; - if (ballDirection.x < 1) { - paddle2Target = paddle2.position.y; - } - paddle2.position.y = paddle2.position.y.moveTo(clamp(paddle2Target, paddle2.size.y * 0.5f, resolutionHeight - paddle2.size.y * 0.5f), ballSpeed * dt); + auto paddle2Target = ballDirection.x < 1 ? paddle2.position.y : ball.position.y; + paddle2.position.y = paddle2.position.y.moveTo( + clamp(paddle2Target, paddle2.size.y * 0.5f, resolutionHeight - paddle2.size.y * 0.5f), + ballSpeed * dt + ); - // Check for paddle and ball collisions. + // Check for collisions. if (paddle1.centerArea.hasIntersection(ball.centerArea)) { ballDirection.x *= -1; - ball.position.x = paddle1.centerArea.rightPoint.x + ball.size.x * 0.5; gameCounter += 1; } if (paddle2.centerArea.hasIntersection(ball.centerArea)) { ballDirection.x *= -1; - ball.position.x = paddle2.centerArea.leftPoint.x - ball.size.x * 0.5; gameCounter += 1; } - // Draw the objects. + // Draw the game. drawRect(ball.centerArea); drawRect(paddle1.centerArea); drawRect(paddle2.centerArea); - // Draw the counter. auto textOptions = DrawOptions(Hook.center); textOptions.scale = Vec2(2); drawDebugText("{}".format(gameCounter), Vec2(resolutionWidth * 0.5, 16), textOptions); diff --git a/examples/ui/handle.d b/examples/ui/handle.d index 05cebb1..976478f 100644 --- a/examples/ui/handle.d +++ b/examples/ui/handle.d @@ -1,7 +1,5 @@ /// This example shows how to use the drag handle. -// TODO: There is a small bug with overlapping UI items. Fix it. - import parin; auto handlePosition = Vec2(120, 60); @@ -18,7 +16,7 @@ bool update(float dt) { if (handleOptions.dragLimit) handleOptions.dragLimit = UiDragLimit.none; else handleOptions.dragLimit = UiDragLimit.viewport; } - // Create the drag handle and return true if it is dragged. + // Create the drag handle and print if it is dragged. if (uiDragHandle(Vec2(60), handlePosition, handleOptions)) { println(handlePosition); } diff --git a/examples/ui/hello.d b/examples/ui/hello.d index add1343..e2a0870 100644 --- a/examples/ui/hello.d +++ b/examples/ui/hello.d @@ -11,7 +11,7 @@ void ready() { bool update(float dt) { // Set the starting point for subsequent UI items. setUiStartPoint(Vec2(8)); - // Create a button and return true if it is clicked. + // Create a button and print if it is clicked. if (uiButton(Vec2(80, 30), buttonText)) { println(buttonText); } diff --git a/source/parin/engine.d b/source/parin/engine.d index ade97f0..19870bf 100644 --- a/source/parin/engine.d +++ b/source/parin/engine.d @@ -6,6 +6,8 @@ // Version: v0.0.29 // --- +// TODO: Try to fix the ui item overlaping bug maybe. +// TODO: Add way to get item point for some stuff. This is nice when making lists. // TODO: Test the ui code and think how to make it better while working on real stuff. // TODO: Test the resource loading code. // TODO: Think about the sound API. @@ -877,13 +879,16 @@ struct UiState { Gamepad gamepadClickAction = Gamepad.a; bool isActOnPress; + Vec2 mousePressedPoint; Vec2 viewportPoint; Vec2 viewportSize; Vec2 viewportScale = Vec2(1); Vec2 startPoint; - Vec2 startPointOffest; short margin; Layout layout; + Vec2 layoutStartPoint; + Vec2 layoutStartPointOffest; + Vec2 layoutMaxItemSize; Vec2 itemDragOffset; Vec2 itemPoint; @@ -2414,15 +2419,21 @@ void prepareUi() { uiState.viewportSize = resolution; uiState.viewportScale = Vec2(1.0f); uiState.startPoint = Vec2(); - uiState.startPointOffest = Vec2(); uiState.margin = 0; uiState.layout = Layout.v; + uiState.layoutStartPoint = Vec2(); + uiState.layoutStartPointOffest = Vec2(); + uiState.layoutMaxItemSize = Vec2(); uiState.itemPoint = Vec2(); uiState.itemSize = Vec2(); uiState.itemId = 0; uiState.hotItemId = 0; uiState.activeItemId = 0; uiState.clickedItemId = 0; + + if (uiState.mouseClickAction.isPressed) { + uiState.mousePressedPoint = uiMouse; + } } Vec2 uiMouse() { @@ -2467,7 +2478,9 @@ Vec2 uiStartPoint() { void setUiStartPoint(Vec2 value) { uiState.itemSize = Vec2(); uiState.startPoint = value; - uiState.startPointOffest = Vec2(); + uiState.layoutStartPoint = value; + uiState.layoutStartPointOffest = Vec2(); + uiState.layoutMaxItemSize = Vec2(); } short uiMargin() { @@ -2479,15 +2492,26 @@ void setUiMargin(short value) { } void useUiLayout(Layout value) { - if (uiState.startPointOffest) { + if (uiState.layoutStartPointOffest) { final switch (value) { case Layout.v: - if (uiState.layout == value) uiState.startPointOffest.x += uiState.itemSize.x + uiState.margin; - uiState.startPointOffest.y = 0; + if (uiState.layoutStartPointOffest.x > uiState.layoutMaxItemSize.x) { + uiState.layoutStartPoint.x = uiState.layoutStartPoint.x + uiState.layoutStartPointOffest.x + uiState.margin; + } else { + uiState.layoutStartPoint.x += uiState.layoutMaxItemSize.x + uiState.margin; + } + uiState.layoutStartPointOffest = Vec2(); + uiState.layoutMaxItemSize.x = 0.0f; break; case Layout.h: - uiState.startPointOffest.x = 0; - if (uiState.layout == value) uiState.startPointOffest.y += uiState.itemSize.y + uiState.margin; + uiState.layoutStartPoint.x = uiState.startPoint.x; + if (uiState.layoutStartPointOffest.y > uiState.layoutMaxItemSize.y) { + uiState.layoutStartPoint.y = uiState.layoutStartPoint.y + uiState.layoutStartPointOffest.y + uiState.margin; + } else { + uiState.layoutStartPoint.y += uiState.layoutMaxItemSize.y + uiState.margin; + } + uiState.layoutStartPointOffest = Vec2(); + uiState.layoutMaxItemSize.y = 0.0f; break; } } @@ -2583,9 +2607,11 @@ void updateUiState(Vec2 itemPoint, Vec2 itemSize, bool isHot, bool isActive, boo uiState.itemPoint = itemPoint; uiState.itemSize = itemSize; uiState.itemId += 1; + if (itemSize.x > uiState.layoutMaxItemSize.x) uiState.layoutMaxItemSize.x = itemSize.x; + if (itemSize.y > uiState.layoutMaxItemSize.y) uiState.layoutMaxItemSize.y = itemSize.y; final switch (uiState.layout) { - case Layout.v: uiState.startPointOffest.y += uiState.itemSize.y + uiState.margin; break; - case Layout.h: uiState.startPointOffest.x += uiState.itemSize.x + uiState.margin; break; + case Layout.v: uiState.layoutStartPointOffest.y += uiState.itemSize.y + uiState.margin; break; + case Layout.h: uiState.layoutStartPointOffest.x += uiState.itemSize.x + uiState.margin; break; } if (isHot) uiState.hotItemId = uiState.itemId; if (isActive) { @@ -2606,11 +2632,22 @@ bool updateUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOpti if (options.font.isEmpty) options.font = engineFont; auto m = uiMouse; auto id = uiState.itemId + 1; - auto area = Rect(uiState.startPoint + uiState.startPointOffest, size); + auto area = Rect(uiState.layoutStartPoint + uiState.layoutStartPointOffest, size); // auto isHot = area.hasPoint(uiMouse) auto isHot = m.x >= area.position.x && m.x < area.position.x + area.size.x && m.y >= area.position.y && m.y < area.position.y + area.size.y; auto isActive = isHot && uiState.mouseClickAction.isDown; - auto isClicked = isHot && (uiState.isActOnPress ? uiState.mouseClickAction.isPressed : uiState.mouseClickAction.isReleased); + auto isClicked = isHot; + if (uiState.isActOnPress) { + isClicked = isClicked && uiState.mouseClickAction.isPressed; + } else { + auto isHotFromMousePressedPoint = + uiState.mousePressedPoint.x >= area.position.x && + uiState.mousePressedPoint.x < area.position.x + area.size.x && + uiState.mousePressedPoint.y >= area.position.y && + uiState.mousePressedPoint.y < area.position.y + area.size.y; + isClicked = isClicked && isHotFromMousePressedPoint && uiState.mouseClickAction.isReleased; + } + if (options.isDisabled) { isHot = false; isActive = false; @@ -2658,6 +2695,7 @@ bool uiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonOptions()) { auto dragLimitX = Vec2(-100000.0f, 100000.0f); auto dragLimitY = Vec2(-100000.0f, 100000.0f); + // NOTE: There is a potential bug here when size is bigger than the limit/viewport. I will ignore it for now. final switch (options.dragLimit) { case UiDragLimit.none: break; case UiDragLimit.viewport: @@ -2665,10 +2703,12 @@ bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonO dragLimitY = Vec2(0.0f, uiState.viewportSize.y); break; case UiDragLimit.viewportAndX: + point.y = clamp(point.y, 0.0f, uiState.viewportSize.y - size.y); dragLimitX = Vec2(0.0f, uiState.viewportSize.x); dragLimitY = Vec2(point.y, point.y + size.y); break; case UiDragLimit.viewportAndY: + point.x = clamp(point.x, 0.0f, uiState.viewportSize.x - size.x); dragLimitX = Vec2(point.x, point.x + size.x); dragLimitY = Vec2(0.0f, uiState.viewportSize.y); break; @@ -2677,10 +2717,12 @@ bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonO dragLimitY = options.dragLimitY; break; case UiDragLimit.customAndX: + point.y = clamp(point.y, 0.0f, options.dragLimitY.y - size.y); dragLimitX = options.dragLimitX; dragLimitY = Vec2(point.y, point.y + size.y); break; case UiDragLimit.customAndY: + point.x = clamp(point.x, 0.0f, options.dragLimitX.y - size.x); dragLimitX = Vec2(point.x, point.x + size.x); dragLimitY = options.dragLimitY; break; @@ -2708,7 +2750,8 @@ bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonO } void uiTexture(Texture texture, UiButtonOptions options = UiButtonOptions()) { - auto point = uiState.startPoint + uiState.startPointOffest; + auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest; + drawRect(Rect(point, texture.size), black); drawTexture(texture, point); updateUiState(point, texture.size, false, false, false); } @@ -2719,7 +2762,7 @@ void uiTexture(TextureId texture, UiButtonOptions options = UiButtonOptions()) { void uiText(IStr text, UiButtonOptions options = UiButtonOptions()) { if (options.font.isEmpty) options.font = engineFont; - auto point = uiState.startPoint + uiState.startPointOffest; + auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest; auto size = measureTextSize(options.font, text); drawText(options.font, text, point); updateUiState(point, size, false, false, false);