Fixed ui bugs and made examples better.

This commit is contained in:
Kapendev 2024-12-15 21:08:03 +02:00
parent 381d99461b
commit 25acd69a84
6 changed files with 100 additions and 70 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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