diff --git a/README.md b/README.md index efc16c7..539011b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ void ready() { } bool update(float dt) { - drawDebugText("Hello world!"); + drawDebugText("Hello world!", Vec2(8.0)); return false; } diff --git a/TOUR.md b/TOUR.md index d64a9c5..fe87ff9 100644 --- a/TOUR.md +++ b/TOUR.md @@ -12,7 +12,7 @@ void ready() { } bool update(float dt) { - drawDebugText("Hello world!"); + drawDebugText("Hello world!", Vec2(8.0)); return false; } @@ -39,13 +39,13 @@ Here is a breakdown of how this code works: ```d bool update(float dt) { - drawDebugText("Hello world!"); + drawDebugText("Hello world!", Vec2(8.0)); return false; } ``` This function is the main loop of the game. - It is called every frame while the game is running and, in this example, draws the message "Hello world!". + It is called every frame while the game is running and, in this example, draws the message "Hello world!" at position `Vec2(8.0)`. The `return false` statement indicates that the game should continue running. If `true` were returned, the game would stop running. @@ -83,12 +83,12 @@ void drawVec2(Vec2 point, float size, Color color = white); void drawCirc(Circ area, Color color = white); void drawLine(Line area, float size, Color color = white); -void drawTexture(Texture texture, Vec2 position, Rect area, DrawOptions options = DrawOptions()); void drawTexture(Texture texture, Vec2 position, DrawOptions options = DrawOptions()); +void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()); -void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()); -void drawText(Font font, Vec2 position, IStr text, DrawOptions options = DrawOptions()); -void drawDebugText(IStr text, Vec2 position = Vec2(8.0f), DrawOptions options = DrawOptions()); +void drawRune(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()); +void drawText(Font font, IStr text, Vec2 position, DrawOptions options = DrawOptions()); +void drawDebugText(IStr text, Vec2 position, DrawOptions options = DrawOptions()); ``` Additional drawing functions can be found in other modules, such as `popka.tilemap`. diff --git a/examples/camera.d b/examples/camera.d index c90357a..63f273b 100644 --- a/examples/camera.d +++ b/examples/camera.d @@ -18,12 +18,12 @@ bool update(float dt) { // Draw the game world. auto cameraArea = Rect(camera.position, resolution).area(camera.hook).subAll(3); camera.attach(); - drawDebugText("Move with arrow keys."); + drawDebugText("Move with arrow keys.", Vec2(8.0)); drawRect(cameraArea, Color(50, 50, 40, 130)); camera.detach(); // Draw the game UI. - drawDebugText("I am UI!"); + drawDebugText("I am UI!", Vec2(8.0)); drawDebugText("+", resolution * Vec2(0.5)); drawDebugText("+", resolution * Vec2(0.5) + (cameraTarget - camera.position)); return false; diff --git a/examples/coins.d b/examples/coins.d index 6936ec7..183be05 100644 --- a/examples/coins.d +++ b/examples/coins.d @@ -48,9 +48,9 @@ bool update(float dt) { } drawRect(player); if (coins.length == 0) { - drawDebugText("You collected all the coins!"); + drawDebugText("You collected all the coins!", Vec2(8.0)); } else { - drawDebugText("Coins: {}/{}\nMove with arrow keys.".format(maxCoinCount - coins.length, maxCoinCount)); + drawDebugText("Coins: {}/{}\nMove with arrow keys.".format(maxCoinCount - coins.length, maxCoinCount), Vec2(8.0)); } return false; } diff --git a/examples/dialogue.d b/examples/dialogue.d index 5837955..40abfc1 100644 --- a/examples/dialogue.d +++ b/examples/dialogue.d @@ -63,9 +63,9 @@ bool update(float dt) { drawDebugText(" | {}".format(choice), choicePosition); } } else if (dialogue.canUpdate) { - drawDebugText("{}: {}".format(dialogue.actor, dialogue.text)); + drawDebugText("{}: {}".format(dialogue.actor, dialogue.text), Vec2(8.0)); } else { - drawDebugText("The dialogue has ended."); + drawDebugText("The dialogue has ended.", Vec2(8.0)); } // Draw the game info. diff --git a/examples/follower.d b/examples/follower.d index 02da544..8a6b234 100644 --- a/examples/follower.d +++ b/examples/follower.d @@ -3,13 +3,9 @@ import popka; // The game variables. auto atlas = TextureId(); -auto frame = 0.0; -auto frameCount = 2; -auto frameSpeed = 8; -auto framePosition = Vec2(); -auto frameSize = Vec2(16); -auto frameDirection = 1; -auto frameSlowdown = 0.2; +auto sprite = Sprite(16, 16, 0, 128, 2, 8); +auto spritePosition = Vec2(); +auto spriteSlowdown = 0.2; void ready() { lockResolution(320, 180); @@ -21,29 +17,25 @@ void ready() { } bool update(float dt) { - // Move the frame around in a smooth way and update the current frame. - framePosition = framePosition.moveToWithSlowdown(mouseScreenPosition, Vec2(dt), frameSlowdown); - frame = wrap(frame + dt * frameSpeed, 0, frameCount); + // Move the sprite around in a smooth way. + spritePosition = spritePosition.moveToWithSlowdown(mouseScreenPosition, Vec2(dt), spriteSlowdown); - // Check the mouse move direction and make the sprite look at that direction. - auto mouseDirection = framePosition.directionTo(mouseScreenPosition); - if (framePosition.distanceTo(mouseScreenPosition) < 0.2) { - frame = 0; - } else if (mouseDirection.x < 0) { - frameDirection = -1; - } else if (mouseDirection.x > 0) { - frameDirection = 1; + // Update the frame of the sprite. + auto isWaiting = spritePosition.distanceTo(mouseScreenPosition) < 0.2; + if (isWaiting) { + sprite.reset(); + } else { + sprite.update(dt); } - // The drawing options can change the way something is drawn. + // Set the drawing options for the sprite. auto options = DrawOptions(); - options.hook = Hook.center; options.scale = Vec2(2); - options.flip = frameDirection == 1 ? Flip.x : Flip.none; - - // Draw the frame and the mouse position. - drawTexture(atlas, framePosition, Rect(frameSize.x * floor(frame), 128, frameSize), options); - drawVec2(mouseScreenPosition, 8, frame == 0 ? blank : white.alpha(150)); + options.hook = Hook.center; + options.flip = (spritePosition.directionTo(mouseScreenPosition).x > 0) ? Flip.x : Flip.none; + // Draw the sprite and the mouse position. + drawSprite(atlas, sprite, spritePosition, options); + drawVec2(mouseScreenPosition, 8, isWaiting ? blank : white.alpha(130)); return false; } diff --git a/examples/hello.d b/examples/hello.d index 45dda4d..56cb347 100644 --- a/examples/hello.d +++ b/examples/hello.d @@ -9,7 +9,7 @@ void ready() { // The update function. This is called every frame while the game is running. // If true is returned, then the game will stop running. bool update(float dt) { - drawDebugText("Hello world!"); + drawDebugText("Hello world!", Vec2(8.0)); return false; } diff --git a/examples/map.d b/examples/map.d index 21634d9..a4722fe 100644 --- a/examples/map.d +++ b/examples/map.d @@ -3,21 +3,23 @@ import popka; // The game variables. auto atlas = TextureId(); -auto map = TileMap(); +auto tileMap = TileMap(); void ready() { lockResolution(320, 180); setBackgroundColor(toRgb(0x0b0b0b)); // Load the `atlas.png` file from the assets folder. atlas = loadTexture("atlas.png").unwrap(); - // Parse the map CSV file. - map.parse("145,0,65\n21,22,23\n37,38,39\n53,54,55", 16, 16); + // Parse the tile map CSV file. + tileMap.parse("145,0,65\n21,22,23\n37,38,39\n53,54,55", 16, 16); } bool update(float dt) { + // Set the drawing options for the tile map. auto options = DrawOptions(); options.scale = Vec2(2.0f); - drawTileMap(atlas, Vec2(), map, Camera(), options); + // Draw the tile map. + drawTileMap(atlas, tileMap, Vec2(), Camera(), options); return false; } diff --git a/setup/source/app.d b/setup/source/app.d index f441eba..2c05aa8 100755 --- a/setup/source/app.d +++ b/setup/source/app.d @@ -19,7 +19,7 @@ void ready() { } bool update(float dt) { - drawDebugText("Hello world!"); + drawDebugText("Hello world!", Vec2(8.0)); return false; } diff --git a/source/popka/engine.d b/source/popka/engine.d index 24d2cd5..e70e64e 100644 --- a/source/popka/engine.d +++ b/source/popka/engine.d @@ -7,7 +7,6 @@ // --- // TODO: Test the resources code and the tag thing. -// TODO: Make a sprite struct. Something that should help with animation maybe. /// The `engine` module functions as a lightweight 2D game engine. module popka.engine; @@ -1600,7 +1599,7 @@ void drawLine(Line area, float size, Color color = white) { } @trusted -void drawTexture(Texture texture, Vec2 position, Rect area, DrawOptions options = DrawOptions()) { +void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) { if (texture.isEmpty) { return; } else if (area.size.x <= 0.0f || area.size.y <= 0.0f) { @@ -1645,12 +1644,12 @@ void drawTexture(Texture texture, Vec2 position, Rect area, DrawOptions options } } -void drawTexture(TextureId texture, Vec2 position, Rect area, DrawOptions options = DrawOptions()) { - drawTexture(texture.getOr(), position, area, options); +void drawTextureArea(TextureId texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) { + drawTextureArea(texture.getOr(), area, position, options); } void drawTexture(Texture texture, Vec2 position, DrawOptions options = DrawOptions()) { - drawTexture(texture, position, Rect(texture.size), options); + drawTextureArea(texture, Rect(texture.size), position, options); } void drawTexture(TextureId texture, Vec2 position, DrawOptions options = DrawOptions()) { @@ -1658,7 +1657,7 @@ void drawTexture(TextureId texture, Vec2 position, DrawOptions options = DrawOpt } @trusted -void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()) { +void drawRune(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) { if (font.isEmpty) { return; } @@ -1682,12 +1681,12 @@ void drawRune(Font font, Vec2 position, dchar rune, DrawOptions options = DrawOp rl.rlPopMatrix(); } -void drawRune(FontId font, Vec2 position, dchar rune, DrawOptions options = DrawOptions()) { - drawRune(font.getOr(), position, rune, options); +void drawRune(FontId font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) { + drawRune(font.getOr(), rune, position, options); } @trusted -void drawText(Font font, Vec2 position, IStr text, DrawOptions options = DrawOptions()) { +void drawText(Font font, IStr text, Vec2 position, DrawOptions options = DrawOptions()) { if (font.isEmpty || text.length == 0) { return; } @@ -1722,7 +1721,7 @@ void drawText(Font font, Vec2 position, IStr text, DrawOptions options = DrawOpt if (codepoint != ' ' && codepoint != '\t') { auto runeOptions = DrawOptions(); runeOptions.color = options.color; - drawRune(font, Vec2(textOffsetX, textOffsetY), codepoint, runeOptions); + drawRune(font, codepoint, Vec2(textOffsetX, textOffsetY), runeOptions); } if (font.data.glyphs[index].advanceX == 0) { textOffsetX += font.data.recs[index].width + font.runeSpacing; @@ -1736,12 +1735,12 @@ void drawText(Font font, Vec2 position, IStr text, DrawOptions options = DrawOpt rl.rlPopMatrix(); } -void drawText(FontId font, Vec2 position, IStr text, DrawOptions options = DrawOptions()) { - drawText(font.getOr(), position, text, options); +void drawText(FontId font, IStr text, Vec2 position, DrawOptions options = DrawOptions()) { + drawText(font.getOr(), text, position, options); } -void drawDebugText(IStr text, Vec2 position = Vec2(8.0f), DrawOptions options = DrawOptions()) { - drawText(engineFont, position, text, options); +void drawDebugText(IStr text, Vec2 position, DrawOptions options = DrawOptions()) { + drawText(engineFont, text, position, options); } mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int width = 960, int height = 540, IStr title = "Popka") { diff --git a/source/popka/package.d b/source/popka/package.d index a654729..161a029 100644 --- a/source/popka/package.d +++ b/source/popka/package.d @@ -11,5 +11,6 @@ module popka; public import joka; public import popka.dialogue; public import popka.engine; +public import popka.sprite; public import popka.tilemap; public import popka.timer; diff --git a/source/popka/sprite.d b/source/popka/sprite.d new file mode 100644 index 0000000..192ae8c --- /dev/null +++ b/source/popka/sprite.d @@ -0,0 +1,69 @@ +// --- +// Copyright 2024 Alexandros F. G. Kapretsos +// SPDX-License-Identifier: MIT +// Email: alexandroskapretsos@gmail.com +// Project: https://github.com/Kapendev/popka +// Version: v0.0.18 +// --- + +/// The `sprite` module provides a simple and extensible sprite. +module popka.sprite; + +import popka.engine; + +@safe @nogc nothrow: + +// TODO: Think about gaps in an atlas texture. + +struct Sprite { + int width; + int height; + int atlasLeft; + int atlasTop; + int frameCount = 1; + float frameSpeed = 1.0f; + float frameProgress = 0.0f; + + @safe @nogc nothrow: + + this(int width, int height, int atlasLeft, int atlasTop, int frameCount = 1, float frameSpeed = 1.0f) { + this.width = width; + this.height = height; + this.atlasLeft = atlasLeft; + this.atlasTop = atlasTop; + this.frameCount = frameCount; + this.frameSpeed = frameSpeed; + } + + this(int width, int height) { + this(width, height, 0, 0); + } + + int frame() { + return cast(int) frameProgress; + } + + void reset() { + frameProgress = 0.0f; + } + + void update(float dt) { + frameProgress = wrap(frameProgress + frameSpeed * dt, 0.0f, frameCount); + } +} + +void drawSprite(Texture texture, Sprite sprite, Vec2 position, DrawOptions options = DrawOptions()) { + auto gridWidth = max(texture.width - sprite.atlasLeft, 0) / sprite.width; + auto gridHeight = max(texture.height - sprite.atlasTop, 0) / sprite.height; + if (gridWidth == 0 || gridHeight == 0) { + return; + } + auto row = sprite.frame / gridWidth; + auto col = sprite.frame % gridWidth; + auto area = Rect(sprite.atlasLeft + col * sprite.width, sprite.atlasTop + row * sprite.height, sprite.width, sprite.height); + drawTextureArea(texture, area, position, options); +} + +void drawSprite(TextureId texture, Sprite sprite, Vec2 position, DrawOptions options = DrawOptions()) { + drawSprite(texture.getOr(), sprite, position, options); +} diff --git a/source/popka/tilemap.d b/source/popka/tilemap.d index c9c060d..892da64 100644 --- a/source/popka/tilemap.d +++ b/source/popka/tilemap.d @@ -18,11 +18,26 @@ public import joka.types; @safe @nogc nothrow: +// TODO: Think about gaps in an atlas texture. + +struct Tile { + int id; + int width; + int height; + + @safe @nogc nothrow: + + this(int id, int width, int height) { + this.id = id; + this.width = width; + this.height = height; + } +} + struct TileMap { Grid!short data; int tileWidth; int tileHeight; - alias data this; @safe @nogc nothrow: @@ -110,28 +125,28 @@ Result!TileMap loadRawTileMap(IStr path, int tileWidth, int tileHeight) { return toTileMap(temp.unwrap(), tileWidth, tileHeight); } -void drawTile(Texture texture, Vec2 position, int tileID, int tileWidth, int tileHeight, DrawOptions options = DrawOptions()) { - auto gridWidth = cast(int) (texture.width / tileWidth); - auto gridHeight = cast(int) (texture.height / tileHeight); +void drawTile(Texture texture, Tile tile, Vec2 position, DrawOptions options = DrawOptions()) { + auto gridWidth = texture.width / tile.width; + auto gridHeight = texture.height / tile.height; if (gridWidth == 0 || gridHeight == 0) { return; } - auto row = tileID / gridWidth; - auto col = tileID % gridWidth; - auto area = Rect(col * tileWidth, row * tileHeight, tileWidth, tileHeight); - drawTexture(texture, position, area, options); + auto row = tile.id / gridWidth; + auto col = tile.id % gridWidth; + auto area = Rect(col * tile.width, row * tile.height, tile.width, tile.height); + drawTextureArea(texture, area, position, options); } -void drawTile(TextureId texture, Vec2 position, int tileID, int tileWidth, int tileHeight, DrawOptions options = DrawOptions()) { - drawTile(texture.getOr(), position, tileID, tileWidth, tileHeight, options); +void drawTile(TextureId texture, Tile tile, Vec2 position, DrawOptions options = DrawOptions()) { + drawTile(texture.getOr(), tile, position, options); } -void drawTileMap(Texture texture, Vec2 position, TileMap map, Camera camera, DrawOptions options = DrawOptions()) { +void drawTileMap(Texture texture, TileMap tileMap, Vec2 position, Camera camera, DrawOptions options = DrawOptions()) { auto cameraArea = Rect(camera.position, resolution).area(camera.hook); auto topLeft = cameraArea.topLeftPoint; auto bottomRight = cameraArea.bottomRightPoint; - auto targetTileWidth = cast(int) (map.tileWidth * options.scale.x); - auto targetTileHeight = cast(int) (map.tileHeight * options.scale.y); + auto targetTileWidth = cast(int) (tileMap.tileWidth * options.scale.x); + auto targetTileHeight = cast(int) (tileMap.tileHeight * options.scale.y); auto targetTileSize = Vec2(targetTileWidth, targetTileHeight); auto row1 = 0; @@ -139,15 +154,15 @@ void drawTileMap(Texture texture, Vec2 position, TileMap map, Camera camera, Dra auto row2 = 0; auto col2 = 0; if (camera.isAttached) { - row1 = cast(int) floor(clamp((topLeft.y - position.y) / targetTileHeight, 0, map.rowCount)); - col1 = cast(int) floor(clamp((topLeft.x - position.x) / targetTileWidth, 0, map.colCount)); - row2 = cast(int) floor(clamp((bottomRight.y - position.y) / targetTileHeight + 1, 0, map.rowCount)); - col2 = cast(int) floor(clamp((bottomRight.x - position.x) / targetTileWidth + 1, 0, map.colCount)); + row1 = cast(int) floor(clamp((topLeft.y - position.y) / targetTileHeight, 0, tileMap.rowCount)); + col1 = cast(int) floor(clamp((topLeft.x - position.x) / targetTileWidth, 0, tileMap.colCount)); + row2 = cast(int) floor(clamp((bottomRight.y - position.y) / targetTileHeight + 1, 0, tileMap.rowCount)); + col2 = cast(int) floor(clamp((bottomRight.x - position.x) / targetTileWidth + 1, 0, tileMap.colCount)); } else { row1 = cast(int) 0; col1 = cast(int) 0; - row2 = cast(int) map.rowCount; - col2 = cast(int) map.colCount; + row2 = cast(int) tileMap.rowCount; + col2 = cast(int) tileMap.colCount; } if (row1 == row2 || col1 == col2) { @@ -156,12 +171,14 @@ void drawTileMap(Texture texture, Vec2 position, TileMap map, Camera camera, Dra foreach (row; row1 .. row2) { foreach (col; col1 .. col2) { - if (map[row, col] == -1) continue; - drawTile(texture, position + Vec2(col, row) * targetTileSize, map[row, col], map.tileWidth, map.tileHeight, options); + if (tileMap[row, col] == -1) continue; + auto tile = Tile(tileMap[row, col], tileMap.tileWidth, tileMap.tileHeight); + auto tilePosition = position + Vec2(col, row) * targetTileSize; + drawTile(texture, tile, tilePosition, options); } } } -void drawTileMap(TextureId texture, Vec2 position, TileMap map, Camera camera, DrawOptions options = DrawOptions()) { - drawTileMap(texture.getOr(), position, map, camera, options); +void drawTileMap(TextureId texture, TileMap tileMap, Vec2 position, Camera camera, DrawOptions options = DrawOptions()) { + drawTileMap(texture.getOr(), tileMap, position, camera, options); } diff --git a/source/popka/timer.d b/source/popka/timer.d index e72ad8d..a1cd4be 100644 --- a/source/popka/timer.d +++ b/source/popka/timer.d @@ -49,7 +49,7 @@ struct Timer { void stop() { time = duration; - prevTime = duration; + prevTime = duration - 0.1f; } void pause() {