Fixed the map and ui modules and worked on examples.

This commit is contained in:
Kapendev 2024-12-16 20:33:43 +02:00
parent 25acd69a84
commit ef058e59e1
15 changed files with 505 additions and 501 deletions

View file

@ -187,7 +187,7 @@ Result!IStr loadTempText(IStr path);
Fault saveText(IStr path, IStr text);
```
Additional loading functions can be found in other modules, such as `parin.tilemap`.
Additional loading functions can be found in other modules, such as `parin.map`.
### Managed Resources
@ -209,8 +209,4 @@ They dont need to be freed manually.
Sprites and tile maps can be implemented in various ways.
To avoid enforcing a specific approach, Parin provides optional modules for these features, allowing users to include or omit them as needed.
Parin provides a sprite type inside the `parin.sprite` module and a tile map type inside the `parin.tilemap` module.
## Scenes
The `parin.scene` module provides a simple scene manager to organize game code by screens, such as the title screen or gameplay.
Parin provides a sprite type inside the `parin.sprite` module and a tile map type inside the `parin.map` module.

View file

@ -6,7 +6,7 @@ This folder provides example projects to help you get started.
> 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
## 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

@ -7,44 +7,41 @@ auto map = TileMap();
auto camera = Camera(0, 0, true);
auto tile = Tile(16, 16, 145);
auto tileSpeed = 120;
auto tileLookDirection = -1;
auto tileFlip = Flip.none;
void ready() {
lockResolution(320, 180);
setBackgroundColor(toRgb(0x0b0b0b));
// Load the `atlas.png` file from the assets folder.
atlas = loadTexture("atlas.png");
// Parse the tile map CSV file.
// Parse a CSV string representing a tile map, where each tile is 16x16 pixels in size.
map.parse("-1,-1,-1\n21,22,23\n37,38,39\n53,54,55", 16, 16);
}
bool update(float dt) {
// Make the drawing options.
// Create the drawing options for the map and tile.
auto mapOptions = DrawOptions(Hook.center);
mapOptions.scale = Vec2(2);
auto tileOptions = mapOptions;
tileOptions.flip = tileLookDirection > 0 ? Flip.x : Flip.none;
// Move the tile and camera.
tileOptions.flip = tileFlip;
if (wasd.x > 0) tileFlip = Flip.x;
else if (wasd.x < 0) tileFlip = Flip.none;
// Move the tile and the camera.
tile.position += wasd * Vec2(tileSpeed * dt);
camera.position = tile.position;
if (wasd.x != 0) tileLookDirection = cast(int) wasd.normalize.round.x;
// Check for collisions.
auto collisionArea = Rect();
foreach (gridPosition; map.gridPositions(camera.area, mapOptions)) {
if (map[gridPosition] == -1) continue;
auto gridTileArea = Rect(map.worldPosition(gridPosition, mapOptions), Vec2(16) * mapOptions.scale);
while (gridTileArea.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale).area(tileOptions.hook))) {
// Check for collisions between the tile and the map and resolve the collision.
foreach (position; map.gridPositions(camera.area, mapOptions)) {
if (map[position] < 0) continue;
auto area = Rect(map.worldPosition(position, mapOptions), map.tileSize * mapOptions.scale);
while (area.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale).area(mapOptions.hook))) {
tile.position -= wasd * Vec2(dt);
camera.position = tile.position;
collisionArea = gridTileArea;
}
}
// Draw the game.
// Draw the tile and the map.
camera.attach();
drawTileMap(atlas, map, camera, mapOptions);
drawTile(atlas, tile, tileOptions);
drawRect(collisionArea, yellow.alpha(120));
drawTileMap(atlas, map, camera, mapOptions);
camera.detach();
drawDebugText("Move with arrow keys.", Vec2(8));
return false;

View file

@ -1,6 +1,7 @@
/// This example shows how to use the scene manager of Parin.
import parin;
import parin.scene;
auto sceneManager = SceneManager();

View file

@ -11,36 +11,24 @@ auto walkAnimation = SpriteAnimation(0, 2, 6);
void ready() {
lockResolution(320, 180);
setBackgroundColor(toRgb(0x0b0b0b));
setIsCursorVisible(false);
// Load the `atlas.png` file from the assets folder.
atlas = loadTexture("atlas.png");
}
bool update(float dt) {
// The sprite should be updated every frame, regardless of whether it is running.
sprite.update(dt);
// Get some basic info about the mouse.
auto mouseDistance = sprite.position.distanceTo(mouse);
auto mouseDirection = sprite.position.directionTo(mouse);
// Move the sprite around in a smooth way.
sprite.followPositionWithSlowdown(mouse, 0.2);
// Play the right animation.
// Play the correct animation.
auto isWaiting = mouseDistance < 0.2;
if (isWaiting) {
sprite.play(idleAnimation);
} else {
sprite.play(walkAnimation);
}
if (isWaiting) sprite.play(idleAnimation);
else sprite.play(walkAnimation);
// Flip the sprite based on the mouse direction.
if (mouseDirection.x > 0) {
spriteFlip = Flip.x;
} else if (mouseDirection.x < 0) {
spriteFlip = Flip.none;
}
if (mouseDirection.x > 0) spriteFlip = Flip.x;
else if (mouseDirection.x < 0) spriteFlip = Flip.none;
// Check if 1, 2, or 3 is pressed and change the character.
foreach (i, digit; digitChars[1 .. 4]) {
if (digit.isPressed) {
@ -49,14 +37,11 @@ bool update(float dt) {
}
}
// Set the drawing options for the sprite.
// Draw the sprite and some info.
auto options = DrawOptions(Hook.center);
options.flip = spriteFlip;
options.scale = Vec2(2);
// Draw the sprite, the mouse position and some info.
drawSprite(atlas, sprite, options);
drawVec2(mouse, 8, isWaiting ? blank : white.alpha(150));
drawDebugText("Press 1, 2 or 3 to change the character.", Vec2(8));
return false;
}

View file

@ -10,6 +10,7 @@ void ready() {
}
bool update(float dt) {
prepareUi();
setUiStartPoint(Vec2(8));
// Toggle the limit of the drag handle.
if (uiButton(Vec2(80, 30), "Limit: {}".format(handleOptions.dragLimit))) {

View file

@ -9,6 +9,8 @@ void ready() {
}
bool update(float dt) {
// Prepare the UI for this frame. This call is required for the UI to function as expected.
prepareUi();
// Set the starting point for subsequent UI items.
setUiStartPoint(Vec2(8));
// Create a button and print if it is clicked.

View file

@ -9,6 +9,7 @@ void ready() {
}
bool update(float dt) {
prepareUi();
// Set the margin between subsequent UI items.
setUiMargin(2);
setUiStartPoint(Vec2(8));

View file

@ -12,6 +12,7 @@ void ready() {
}
bool update(float dt) {
prepareUi();
// Set the viewport state for subsequent UI items.
setUiViewportState(viewportPosition, viewport.size, viewportScale);
viewport.attach();

View file

@ -14,9 +14,6 @@ module parin.dialogue;
import joka.ascii;
import parin.engine;
public import joka.containers;
public import joka.faults;
public import joka.types;
@safe:

View file

@ -6,9 +6,7 @@
// 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: Add way to align text. I neeed this for the UI.
// TODO: Test the resource loading code.
// TODO: Think about the sound API.
// TODO: Make sounds loop based on a variable and not on the file type.
@ -20,9 +18,9 @@ module parin.engine;
import stdc = joka.stdc;
import rl = parin.rl;
import joka.unions;
import joka.ascii;
import joka.io;
import joka.unions;
import parin.timer;
public import joka.colors;
@ -38,14 +36,6 @@ IStr[64] engineEnvArgsBuffer;
Sz engineEnvArgsBufferLength;
IStr[64] engineDroppedFilePathsBuffer;
rl.FilePathList engineDroppedFilePathsDataBuffer;
UiState uiPreviousState;
UiState uiState;
enum defaultUiAlpha = 230;
enum defaultUiDisabledColor = 0x202020.toRgb().alpha(defaultUiAlpha);
enum defaultUiIdleColor = 0x414141.toRgb().alpha(defaultUiAlpha);
enum defaultUiHotColor = 0x818181.toRgb().alpha(defaultUiAlpha);
enum defaultUiActiveColor = 0xBABABA.toRgb().alpha(defaultUiAlpha);
/// A type representing layout orientations.
enum Layout : ubyte {
@ -53,6 +43,13 @@ enum Layout : ubyte {
h, /// Horizontal layout.
}
/// A type representing alignment orientations.
enum Align : ubyte {
left, /// Align to the left.
center, /// Align to the center.
right, /// Align to the right.
}
/// A type representing flipping orientations.
enum Flip : ubyte {
none, /// No flipping.
@ -839,68 +836,6 @@ struct Camera {
}
}
/// A type representing the constraints on drag movement.
enum UiDragLimit: ubyte {
none, /// No limits.
viewport, /// Limited to the viewport.
viewportAndX, /// Limited to the viewport and on the X-axis.
viewportAndY, /// Limited to the viewport and on the Y-axis.
custom, /// Limited to custom limits.
customAndX, /// Limited to custom limits and on the X-axis.
customAndY, /// Limited to custom limits and on the Y-axis.
}
struct UiButtonOptions {
Color disabledColor = defaultUiDisabledColor;
Color idleColor = defaultUiIdleColor;
Color hotColor = defaultUiHotColor;
Color activeColor = defaultUiActiveColor;
Font font;
bool isDisabled;
UiDragLimit dragLimit;
Vec2 dragLimitX = Vec2(-100000.0f, 100000.0f);
Vec2 dragLimitY = Vec2(-100000.0f, 100000.0f);
@safe @nogc nothrow:
this(bool isDisabled) {
this.isDisabled = isDisabled;
}
this(UiDragLimit dragLimit) {
this.dragLimit = dragLimit;
}
}
struct UiState {
Mouse mouseClickAction = Mouse.left;
Keyboard keyboardClickAction = Keyboard.space;
Gamepad gamepadClickAction = Gamepad.a;
bool isActOnPress;
Vec2 mousePressedPoint;
Vec2 viewportPoint;
Vec2 viewportSize;
Vec2 viewportScale = Vec2(1);
Vec2 startPoint;
short margin;
Layout layout;
Vec2 layoutStartPoint;
Vec2 layoutStartPointOffest;
Vec2 layoutMaxItemSize;
Vec2 itemDragOffset;
Vec2 itemPoint;
Vec2 itemSize;
short itemId;
short hotItemId;
short activeItemId;
short clickedItemId;
short draggedItemId;
short focusedItemId;
}
struct EngineFlags {
bool isUpdating;
bool isPixelSnapped;
@ -1513,7 +1448,6 @@ void updateWindow(bool function(float dt) updateFunc) {
}
}
prepareUi();
auto dt = deltaTime;
auto result = _updateFunc(dt);
engineState.tickCount = (engineState.tickCount + 1) % engineState.tickCount.max;
@ -1613,7 +1547,6 @@ void updateWindow(bool function(float dt) updateFunc) {
@trusted
void closeWindow() {
if (!rl.IsWindowReady) return;
uiState = UiState();
engineState.free();
rl.CloseAudioDevice();
rl.CloseWindow();
@ -2413,357 +2346,3 @@ mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int
}
}
}
void prepareUi() {
uiState.viewportPoint = Vec2();
uiState.viewportSize = resolution;
uiState.viewportScale = Vec2(1.0f);
uiState.startPoint = 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() {
auto result = (mouse - uiState.viewportPoint) / uiState.viewportScale;
if (result.x < 0) result.x = -100000.0f;
else if (result.x > uiState.viewportSize.x) result.x = 100000.0f;
if (result.y < 0) result.y = -100000.0f;
else if (result.y > uiState.viewportSize.y) result.y = 100000.0f;
return result;
}
void setUiClickAction(Mouse value) {
uiState.mouseClickAction = value;
}
void setUiClickAction(Keyboard value) {
uiState.keyboardClickAction = value;
}
void setUiClickAction(Gamepad value) {
uiState.gamepadClickAction = value;
}
bool isUiActOnPress() {
return uiState.isActOnPress;
}
void setIsUiActOnPress(bool value) {
uiState.isActOnPress = value;
}
void setUiViewportState(Vec2 point, Vec2 size, Vec2 scale) {
uiState.viewportPoint = point;
uiState.viewportSize = size;
uiState.viewportScale = scale;
}
Vec2 uiStartPoint() {
return uiState.startPoint;
}
void setUiStartPoint(Vec2 value) {
uiState.itemSize = Vec2();
uiState.startPoint = value;
uiState.layoutStartPoint = value;
uiState.layoutStartPointOffest = Vec2();
uiState.layoutMaxItemSize = Vec2();
}
short uiMargin() {
return uiState.margin;
}
void setUiMargin(short value) {
uiState.margin = value;
}
void useUiLayout(Layout value) {
if (uiState.layoutStartPointOffest) {
final switch (value) {
case Layout.v:
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.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;
}
}
uiState.layout = value;
}
bool isUiItemHot() {
return uiState.itemId == uiState.hotItemId;
}
bool isUiHot() {
return uiState.hotItemId > 0;
}
bool isUiItemActive() {
return uiState.itemId == uiState.activeItemId;
}
bool isUiActive() {
return uiState.activeItemId > 0;
}
bool isUiItemClicked() {
return uiState.itemId == uiState.clickedItemId;
}
bool isUiClicked() {
return uiState.clickedItemId > 0;
}
bool isUiItemDragged() {
return uiState.itemId == uiState.draggedItemId && deltaMouse;
}
bool isUiDragged() {
return uiState.draggedItemId > 0 && deltaMouse;
}
Vec2 uiDragOffset() {
return uiState.itemDragOffset;
}
int uiFocus() {
return uiState.focusedItemId;
}
void setUiFocus(short id) {
uiState.focusedItemId = id;
}
void clampUiFocus(short step, Sz length) {
auto min = cast(short) (uiState.itemId + 1);
auto max = cast(short) (length - 1 + min);
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
if (step == 0) {
uiState.focusedItemId = min;
return;
}
if (isOutside) {
if (step < 0) {
uiState.focusedItemId = max;
return;
} else {
uiState.focusedItemId = min;
return;
}
}
uiState.focusedItemId = clamp(cast(short) (uiState.focusedItemId + step), min, max);
}
void wrapUiFocus(short step, Sz length) {
auto min = cast(short) (uiState.itemId + 1);
auto max = cast(short) (length - 1 + min);
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
if (step == 0) {
uiState.focusedItemId = min;
return;
}
if (isOutside) {
if (step < 0) {
uiState.focusedItemId = max;
return;
} else {
uiState.focusedItemId = min;
return;
}
}
uiState.focusedItemId = wrap(cast(short) (uiState.focusedItemId + step), min, cast(short) (max + 1));
}
void updateUiState(Vec2 itemPoint, Vec2 itemSize, bool isHot, bool isActive, bool isClicked) {
uiPreviousState = uiState;
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.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) {
uiState.activeItemId = uiState.itemId;
uiState.focusedItemId = uiState.itemId;
}
if (isClicked) uiState.clickedItemId = uiState.itemId;
if (uiState.draggedItemId) {
if (uiState.mouseClickAction.isReleased) uiState.draggedItemId = 0;
} else if (uiState.mouseClickAction.isPressed && uiState.itemId == uiState.activeItemId) {
auto m = uiMouse;
uiState.itemDragOffset = uiState.itemPoint - m;
uiState.draggedItemId = uiState.itemId;
}
}
bool updateUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto m = uiMouse;
auto id = uiState.itemId + 1;
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;
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;
isClicked = false;
} else if (id == uiState.focusedItemId) {
isHot = true;
if (uiState.keyboardClickAction.isDown || uiState.gamepadClickAction.isDown) isActive = true;
if (uiState.isActOnPress) {
if (uiState.keyboardClickAction.isPressed || uiState.gamepadClickAction.isPressed) isClicked = true;
} else {
if (uiState.keyboardClickAction.isReleased || uiState.gamepadClickAction.isReleased) isClicked = true;
}
}
updateUiState(area.position, size, isHot, isActive, isClicked);
return isClicked;
}
void drawUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto area = Rect(uiState.itemPoint, size);
if (options.isDisabled) {
drawRect(area, options.disabledColor);
} else if (isUiItemActive) {
drawRect(area, options.activeColor);
} else if (isUiItemHot) {
drawRect(area, options.hotColor);
} else {
drawRect(area, options.idleColor);
}
if (options.isDisabled) {
auto tempOptions = DrawOptions(Hook.center);
tempOptions.color.a = defaultUiAlpha / 2;
drawText(options.font, text, area.centerPoint, tempOptions);
} else {
drawText(options.font, text, area.centerPoint, DrawOptions(Hook.center));
}
}
bool uiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
auto result = updateUiButton(size, text, options);
drawUiButton(size, text, options);
return result;
}
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:
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
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;
case UiDragLimit.custom:
dragLimitX = options.dragLimitX;
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;
}
size.x = clamp(size.x, 0.0f, dragLimitX.y - dragLimitX.x);
size.y = clamp(size.y, 0.0f, dragLimitY.y - dragLimitY.x);
point.x = clamp(point.x, dragLimitX.x, dragLimitX.y - size.x);
point.y = clamp(point.y, dragLimitY.x, dragLimitY.y - size.y);
setUiStartPoint(point);
updateUiButton(size, "", options);
if (isUiItemDragged) {
auto m = (mouse - uiState.viewportPoint) / uiState.viewportScale; // NOTE: Maybe this should be a function?
point.y = clamp(m.y + uiDragOffset.y, dragLimitY.x, dragLimitY.y - size.y);
point.x = clamp(m.x + uiDragOffset.x, dragLimitX.x, dragLimitX.y - size.x);
uiState = uiPreviousState;
setUiStartPoint(point);
updateUiButton(size, "", options);
drawUiButton(size, "", options);
return true;
} else {
drawUiButton(size, "", options);
return false;
}
}
void uiTexture(Texture texture, UiButtonOptions options = UiButtonOptions()) {
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
drawRect(Rect(point, texture.size), black);
drawTexture(texture, point);
updateUiState(point, texture.size, false, false, false);
}
void uiTexture(TextureId texture, UiButtonOptions options = UiButtonOptions()) {
uiTexture(texture.get(), options);
}
void uiText(IStr text, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
auto size = measureTextSize(options.font, text);
drawText(options.font, text, point);
updateUiState(point, size, false, false, false);
}

View file

@ -9,15 +9,11 @@
// TODO: Think about gaps in an atlas texture.
// TODO: Update all the doc comments here.
/// The `tilemap` module provides a simple and fast tile map.
module parin.tilemap;
/// The `map` module provides a simple and fast tile map.
module parin.map;
import joka.ascii;
import parin.engine;
public import joka.containers;
public import joka.faults;
public import joka.math;
public import joka.types;
@safe @nogc nothrow:
@ -130,11 +126,11 @@ struct TileMap {
}
Sz rowCount() {
return data.length == 0 ? 0 : softMaxRowCount;
return data.length ? softMaxRowCount : 0;
}
Sz colCount() {
return data.length == 0 ? 0 : softMaxColCount;
return data.length ? softMaxColCount : 0;
}
bool isEmpty() {
@ -142,7 +138,7 @@ struct TileMap {
}
bool has(Sz row, Sz col) {
return row < softMaxRowCount && col < softMaxColCount;
return row < rowCount && col < colCount;
}
bool has(IVec2 position) {
@ -160,11 +156,11 @@ struct TileMap {
}
int width() {
return cast(int) (softMaxColCount * tileWidth);
return cast(int) (colCount * tileWidth);
}
int height() {
return cast(int) (softMaxRowCount * tileHeight);
return cast(int) (rowCount * tileHeight);
}
/// Returns the size of the tile map.
@ -230,32 +226,35 @@ struct TileMap {
}
IVec2 firstGridPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) {
if (rowCount == 0 || colCount == 0) return IVec2();
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
result.y = cast(int) floor(clamp((topLeftWorldPosition.y - position.y) / targetTileHeight, 0, softMaxRowCount));
result.x = cast(int) floor(clamp((topLeftWorldPosition.x - position.x) / targetTileWidth, 0, softMaxColCount));
result.y = cast(int) floor(clamp((topLeftWorldPosition.y - position.y) / targetTileHeight, 0, rowCount - 1));
result.x = cast(int) floor(clamp((topLeftWorldPosition.x - position.x) / targetTileWidth, 0, colCount - 1));
return result;
}
IVec2 lastGridPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
if (rowCount == 0 || colCount == 0) return IVec2();
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
auto extraTileCount = options.hook == Hook.topLeft ? 1 : 2;
result.y = cast(int) floor(clamp((bottomRightWorldPosition.y - position.y) / targetTileHeight + extraTileCount, 0, softMaxRowCount));
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, softMaxColCount));
result.y = cast(int) floor(clamp((bottomRightWorldPosition.y - position.y) / targetTileHeight + extraTileCount, 0, rowCount - 1));
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, colCount - 1));
return result;
}
auto gridPositions(Vec2 topLeftWorldPosition, Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
static struct Range {
Sz colCount;
IVec2 first;
IVec2 last;
IVec2 position;
bool empty() {
return position == last;
return position.x > last.x || position.y > last.y;
}
IVec2 front() {
@ -264,7 +263,7 @@ struct TileMap {
void popFront() {
position.x += 1;
if (position.x >= maxColCount) {
if (position.x >= colCount) {
position.x = first.x;
position.y += 1;
}
@ -272,6 +271,7 @@ struct TileMap {
}
auto result = Range(
colCount,
firstGridPosition(topLeftWorldPosition, options),
lastGridPosition(bottomRightWorldPosition, options),
);
@ -321,8 +321,8 @@ void drawTileMap(Texture texture, TileMap map, Camera camera, DrawOptions option
if (colRow1.x == colRow2.x || colRow1.y == colRow2.y) return;
auto textureArea = Rect(map.tileWidth, map.tileHeight);
foreach (row; colRow1.y .. colRow2.y) {
foreach (col; colRow1.x .. colRow2.x) {
foreach (row; colRow1.y .. colRow2.y + 1) {
foreach (col; colRow1.x .. colRow2.x + 1) {
auto id = map[row, col];
if (id < 0) continue;
textureArea.position.x = (id % textureColCount) * map.tileWidth;

View file

@ -11,7 +11,7 @@ module parin;
public import joka.ascii;
public import joka.io;
public import parin.engine;
public import parin.scene;
public import parin.map;
public import parin.sprite;
public import parin.tilemap;
public import parin.timer;
public import parin.ui;

View file

@ -6,6 +6,7 @@
// Version: v0.0.29
// ---
// NOTE(Kapendev): I am not a fan of this module and I would remove it, but maybe someone is using it.
// TODO: Update all the doc comments here.
/// The `scene` module provides a simple scene manager.

443
source/parin/ui.d Normal file
View file

@ -0,0 +1,443 @@
// ---
// Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT
// Email: alexandroskapretsos@gmail.com
// Project: https://github.com/Kapendev/parin
// Version: v0.0.29
// ---
// TODO: Clean maybe the UiState struct and prepareUi func.
// TODO: Add way to get item point for some stuff. This is nice when making lists.
// TODO: Add focus style.
// TODO: Add way to align text in buttons.
// TODO: Look at the API again.
// TODO: Test the ui code and think how to make it better while working on real stuff.
/// The `ui` module functions as a immediate mode UI toolkit.
module parin.ui;
import parin.engine;
UiState uiState;
UiState uiPreviousState;
enum defaultUiAlpha = 230;
enum defaultUiDisabledColor = 0x202020.toRgb().alpha(defaultUiAlpha);
enum defaultUiIdleColor = 0x414141.toRgb().alpha(defaultUiAlpha);
enum defaultUiHotColor = 0x818181.toRgb().alpha(defaultUiAlpha);
enum defaultUiActiveColor = 0xBABABA.toRgb().alpha(defaultUiAlpha);
/// A type representing the constraints on drag movement.
enum UiDragLimit: ubyte {
none, /// No limits.
viewport, /// Limited to the viewport.
viewportAndX, /// Limited to the viewport and on the X-axis.
viewportAndY, /// Limited to the viewport and on the Y-axis.
custom, /// Limited to custom limits.
customAndX, /// Limited to custom limits and on the X-axis.
customAndY, /// Limited to custom limits and on the Y-axis.
}
struct UiButtonOptions {
Color disabledColor = defaultUiDisabledColor;
Color idleColor = defaultUiIdleColor;
Color hotColor = defaultUiHotColor;
Color activeColor = defaultUiActiveColor;
Font font;
bool isDisabled;
UiDragLimit dragLimit;
Vec2 dragLimitX = Vec2(-100000.0f, 100000.0f);
Vec2 dragLimitY = Vec2(-100000.0f, 100000.0f);
@safe @nogc nothrow:
this(bool isDisabled) {
this.isDisabled = isDisabled;
}
this(UiDragLimit dragLimit) {
this.dragLimit = dragLimit;
}
}
struct UiState {
Mouse mouseClickAction = Mouse.left;
Keyboard keyboardClickAction = Keyboard.space;
Gamepad gamepadClickAction = Gamepad.a;
bool isActOnPress;
Vec2 mousePressedPoint;
Vec2 viewportPoint;
Vec2 viewportSize;
Vec2 viewportScale = Vec2(1);
Vec2 startPoint;
short margin;
Layout layout;
Vec2 layoutStartPoint;
Vec2 layoutStartPointOffest;
Vec2 layoutMaxItemSize;
Vec2 itemDragOffset;
Vec2 itemPoint;
Vec2 itemSize;
short itemId;
short hotItemId;
short activeItemId;
short clickedItemId;
short draggedItemId;
short focusedItemId;
}
void prepareUi() {
setUiViewportState(Vec2(), resolution, Vec2(1.0f));
uiState.startPoint = 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;
}
Vec2 uiMouse() {
auto result = (mouse - uiState.viewportPoint) / uiState.viewportScale;
if (result.x < 0) result.x = -100000.0f;
else if (result.x > uiState.viewportSize.x) result.x = 100000.0f;
if (result.y < 0) result.y = -100000.0f;
else if (result.y > uiState.viewportSize.y) result.y = 100000.0f;
return result;
}
void setUiClickAction(Mouse value) {
uiState.mouseClickAction = value;
}
void setUiClickAction(Keyboard value) {
uiState.keyboardClickAction = value;
}
void setUiClickAction(Gamepad value) {
uiState.gamepadClickAction = value;
}
bool isUiActOnPress() {
return uiState.isActOnPress;
}
void setIsUiActOnPress(bool value) {
uiState.isActOnPress = value;
}
void setUiViewportState(Vec2 point, Vec2 size, Vec2 scale) {
uiState.viewportPoint = point;
uiState.viewportSize = size;
uiState.viewportScale = scale;
if (uiState.mouseClickAction.isPressed) {
uiState.mousePressedPoint = uiMouse;
}
}
Vec2 uiStartPoint() {
return uiState.startPoint;
}
void setUiStartPoint(Vec2 value) {
uiState.itemSize = Vec2();
uiState.startPoint = value;
uiState.layoutStartPoint = value;
uiState.layoutStartPointOffest = Vec2();
uiState.layoutMaxItemSize = Vec2();
}
short uiMargin() {
return uiState.margin;
}
void setUiMargin(short value) {
uiState.margin = value;
}
void useUiLayout(Layout value) {
if (uiState.layoutStartPointOffest) {
final switch (value) {
case Layout.v:
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.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;
}
}
uiState.layout = value;
}
bool isUiItemHot() {
return uiState.itemId == uiState.hotItemId;
}
bool isUiHot() {
return uiState.hotItemId > 0;
}
bool isUiItemActive() {
return uiState.itemId == uiState.activeItemId;
}
bool isUiActive() {
return uiState.activeItemId > 0;
}
bool isUiItemClicked() {
return uiState.itemId == uiState.clickedItemId;
}
bool isUiClicked() {
return uiState.clickedItemId > 0;
}
bool isUiItemDragged() {
return uiState.itemId == uiState.draggedItemId && deltaMouse;
}
bool isUiDragged() {
return uiState.draggedItemId > 0 && deltaMouse;
}
Vec2 uiDragOffset() {
return uiState.itemDragOffset;
}
int uiFocus() {
return uiState.focusedItemId;
}
void setUiFocus(short id) {
uiState.focusedItemId = id;
}
void clampUiFocus(short step, Sz length) {
auto min = cast(short) (uiState.itemId + 1);
auto max = cast(short) (length - 1 + min);
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
if (step == 0) {
uiState.focusedItemId = min;
return;
}
if (isOutside) {
if (step < 0) {
uiState.focusedItemId = max;
return;
} else {
uiState.focusedItemId = min;
return;
}
}
uiState.focusedItemId = clamp(cast(short) (uiState.focusedItemId + step), min, max);
}
void wrapUiFocus(short step, Sz length) {
auto min = cast(short) (uiState.itemId + 1);
auto max = cast(short) (length - 1 + min);
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
if (step == 0) {
uiState.focusedItemId = min;
return;
}
if (isOutside) {
if (step < 0) {
uiState.focusedItemId = max;
return;
} else {
uiState.focusedItemId = min;
return;
}
}
uiState.focusedItemId = wrap(cast(short) (uiState.focusedItemId + step), min, cast(short) (max + 1));
}
void updateUiState(Vec2 itemPoint, Vec2 itemSize, bool isHot, bool isActive, bool isClicked) {
uiPreviousState = uiState;
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.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) {
uiState.activeItemId = uiState.itemId;
uiState.focusedItemId = uiState.itemId;
}
if (isClicked) uiState.clickedItemId = uiState.itemId;
if (uiState.mouseClickAction.isPressed && uiState.itemId == uiState.activeItemId) {
auto m = uiMouse;
uiState.itemDragOffset = uiState.itemPoint - m;
uiState.draggedItemId = uiState.itemId;
}
if (uiState.draggedItemId) {
if (uiState.mouseClickAction.isReleased) uiState.draggedItemId = 0;
}
}
bool updateUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto m = uiMouse;
auto id = uiState.itemId + 1;
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;
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;
isClicked = false;
} else if (id == uiState.focusedItemId) {
isHot = true;
if (uiState.keyboardClickAction.isDown || uiState.gamepadClickAction.isDown) isActive = true;
if (uiState.isActOnPress) {
if (uiState.keyboardClickAction.isPressed || uiState.gamepadClickAction.isPressed) isClicked = true;
} else {
if (uiState.keyboardClickAction.isReleased || uiState.gamepadClickAction.isReleased) isClicked = true;
}
}
updateUiState(area.position, size, isHot, isActive, isClicked);
return isClicked;
}
void drawUiButton(Vec2 size, IStr text, Vec2 point, bool isHot, bool isActive, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto area = Rect(point, size);
if (options.isDisabled) {
drawRect(area, options.disabledColor);
} else if (isActive) {
drawRect(area, options.activeColor);
} else if (isHot) {
drawRect(area, options.hotColor);
} else {
drawRect(area, options.idleColor);
}
if (options.isDisabled) {
auto tempOptions = DrawOptions(Hook.center);
tempOptions.color.a = defaultUiAlpha / 2;
drawText(options.font, text, area.centerPoint, tempOptions);
} else {
drawText(options.font, text, area.centerPoint, DrawOptions(Hook.center));
}
}
bool uiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
auto result = updateUiButton(size, text, options);
drawUiButton(size, text, uiState.itemPoint, isUiItemHot, isUiItemActive, options);
return result;
}
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:
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
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;
case UiDragLimit.custom:
dragLimitX = options.dragLimitX;
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;
}
size.x = clamp(size.x, 0.0f, dragLimitX.y - dragLimitX.x);
size.y = clamp(size.y, 0.0f, dragLimitY.y - dragLimitY.x);
point.x = clamp(point.x, dragLimitX.x, dragLimitX.y - size.x);
point.y = clamp(point.y, dragLimitY.x, dragLimitY.y - size.y);
setUiStartPoint(point);
updateUiButton(size, "", options);
if (isUiItemDragged) {
auto m = (mouse - uiState.viewportPoint) / uiState.viewportScale; // NOTE: Maybe this should be a function?
point.y = clamp(m.y + uiDragOffset.y, dragLimitY.x, dragLimitY.y - size.y);
point.x = clamp(m.x + uiDragOffset.x, dragLimitX.x, dragLimitX.y - size.x);
uiState = uiPreviousState;
setUiStartPoint(point);
updateUiButton(size, "", options);
drawUiButton(size, "", uiState.itemPoint, isUiItemHot, isUiItemActive, options);
return true;
} else {
drawUiButton(size, "", uiState.itemPoint, isUiItemHot, isUiItemActive, options);
return false;
}
}
void uiTexture(Texture texture, UiButtonOptions options = UiButtonOptions()) {
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
drawRect(Rect(point, texture.size), black);
drawTexture(texture, point);
updateUiState(point, texture.size, false, false, false);
}
void uiTexture(TextureId texture, UiButtonOptions options = UiButtonOptions()) {
uiTexture(texture.get(), options);
}
void uiText(IStr text, UiButtonOptions options = UiButtonOptions()) {
if (options.font.isEmpty) options.font = engineFont;
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
auto size = measureTextSize(options.font, text);
drawText(options.font, text, point);
updateUiState(point, size, false, false, false);
}