From 7f2b072482262ec5f169eb6a58556f5d2c1bc840 Mon Sep 17 00:00:00 2001 From: Kapendev Date: Sat, 12 Oct 2024 14:28:25 +0300 Subject: [PATCH] Everything is back to normal now. --- .gitignore | 14 ++--- TOUR.md | 2 +- examples/coins.d | 1 - examples/dialogue.d | 1 + examples/map.d | 32 +++++----- examples/pong.d | 1 - examples/scene.d | 2 +- source/parin/dialogue.d | 21 ++++--- source/parin/engine.d | 125 +++++++++++++++++++++++++++++---------- source/parin/package.d | 1 - source/parin/rl/raylib.d | 2 - source/parin/sprite.d | 12 ++-- source/parin/tilemap.d | 58 ++++++++++++++---- source/parin/timer.d | 4 +- 14 files changed, 187 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index e1c37ed..2f80e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,13 @@ docs.json __dummy.html docs/ -/popka -popka.so -popka.dylib -popka.dll -popka.a -popka.lib -popka-test-* +/parin +parin.so +parin.dylib +parin.dll +parin.a +parin.lib +parin-test-* *.exe *.pdb *.o diff --git a/TOUR.md b/TOUR.md index 51e6569..dd05b9a 100644 --- a/TOUR.md +++ b/TOUR.md @@ -115,7 +115,7 @@ void updateSound(SoundId sound); ## Drawing Parin provides a set of drawing functions inside the `parin.engine` module. -While drawing is not pixel-perfect by default, it can be by calling the `setIsPixelPerfect` function. +While drawing is not pixel-perfect by default, it can be by calling the `setIsPixelPerfect` or `setIsPixelSnapped` function. ```d void drawRect(Rect area, Color color = white); diff --git a/examples/coins.d b/examples/coins.d index 7c68a93..6487051 100644 --- a/examples/coins.d +++ b/examples/coins.d @@ -4,7 +4,6 @@ import parin; // The game variables. auto player = Rect(16, 16); auto playerSpeed = 120; - auto coins = SparseList!Rect(); auto coinSize = Vec2(8); auto maxCoinCount = 8; diff --git a/examples/dialogue.d b/examples/dialogue.d index dbfd38d..9f41201 100644 --- a/examples/dialogue.d +++ b/examples/dialogue.d @@ -1,5 +1,6 @@ /// This example shows how to use the Parin dialogue system. import parin; +import parin.dialogue; // The game variables. auto dialogue = Dialogue(); diff --git a/examples/map.d b/examples/map.d index 2ef008a..d1d8734 100644 --- a/examples/map.d +++ b/examples/map.d @@ -19,38 +19,36 @@ void ready() { } bool update(float dt) { - // Make some options. + // Make the drawing options. auto mapOptions = DrawOptions(Hook.center); mapOptions.scale = Vec2(2); auto tileOptions = mapOptions; tileOptions.flip = tileLookDirection > 0 ? Flip.x : Flip.none; - // Move tile and camera. + // Move the tile and camera. tile.position += wasd * Vec2(tileSpeed * dt); - camera.followPosition(tile.position, tileSpeed); + camera.position = tile.position; if (wasd.x != 0) tileLookDirection = cast(int) wasd.normalize.round.x; + // Check for collisions. - auto colRow1 = map.firstMapPosition(camera.area.topLeftPoint, mapOptions); - auto colRow2 = map.lastMapPosition(camera.area.bottomRightPoint, mapOptions); - foreach (row; colRow1.y .. colRow2.y) { - foreach (col; colRow1.x .. colRow2.x) { - if (map[row, col] == -1) continue; - // TODO: Yeah, maybe change it to something better... - auto mapTileRect = Rect(map.worldPosition(row, col, mapOptions), Vec2(16) * mapOptions.scale); - auto myTileRect = Rect(tile.position, tile.size * mapOptions.scale).area(Hook.center); - if (mapTileRect.hasIntersection(myTileRect)) { - tile.position -= wasd * Vec2(tileSpeed * dt); - camera.followPosition(tile.position, tileSpeed); - break; - } + auto collisionArea = Rect(); + foreach (gridPosition; map.gridPositions(camera.topLeftPoint, camera.bottomRightPoint, 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))) { + tile.position -= wasd * Vec2(dt); + camera.position = tile.position; + collisionArea = gridTileArea; } } - // Draw game. + // Draw the game. camera.attach(); drawTileMap(atlas, map, camera, mapOptions); drawTile(atlas, tile, tileOptions); + drawRect(collisionArea, yellow.alpha(120)); camera.detach(); + drawDebugText("Move with arrow keys.", Vec2(8)); return false; } diff --git a/examples/pong.d b/examples/pong.d index e45f065..03eac75 100644 --- a/examples/pong.d +++ b/examples/pong.d @@ -75,7 +75,6 @@ bool update(float dt) { drawRect(ball.centerArea); drawRect(paddle1.centerArea); drawRect(paddle2.centerArea); - // Draw the counter. auto textOptions = DrawOptions(Hook.center); textOptions.scale = Vec2(2); diff --git a/examples/scene.d b/examples/scene.d index e06eef6..76b4140 100644 --- a/examples/scene.d +++ b/examples/scene.d @@ -51,7 +51,7 @@ struct Scene2 { sceneManager.enter!Scene1(); } - drawDebugText("Press enter to change scene.", resolution * Vec2(0.5), DrawOptions(Hook.center)); + drawDebugText("Press space to change scene.", resolution * Vec2(0.5), DrawOptions(Hook.center)); drawDebugText("Scene 2\nNo counter here.", Vec2(8)); return false; } diff --git a/source/parin/dialogue.d b/source/parin/dialogue.d index f321388..e01f5a4 100644 --- a/source/parin/dialogue.d +++ b/source/parin/dialogue.d @@ -40,7 +40,7 @@ struct DialogueUnit { LStr text; DialogueUnitKind kind; - @safe: + @safe @nogc nothrow: void free() { text.free(); @@ -52,7 +52,7 @@ struct DialogueValue { LStr name; long value; - @safe: + @safe @nogc nothrow: void free() { name.free(); @@ -60,7 +60,7 @@ struct DialogueValue { } } -alias DialogueCommandRunner = void function(IStr[] args); +alias DialogueCommandRunner = void function(IStr[] args) @trusted; struct Dialogue { List!DialogueUnit units; @@ -69,7 +69,13 @@ struct Dialogue { IStr actor; Sz unitIndex; - @safe: + @trusted + void run(DialogueCommandRunner runner) { + runner(args); + update(); + } + + @safe @nogc nothrow: bool isEmpty() { return units.length == 0; @@ -169,11 +175,6 @@ struct Dialogue { update(); } - void run(DialogueCommandRunner runner) { - runner(args); - update(); - } - // TODO: Remove the asserts! void update() { if (units.length != 0 && unitIndex < units.length - 1) { @@ -331,6 +332,8 @@ struct Dialogue { } } +@safe @nogc nothrow: + bool isValidDialogueUnitKind(char c) { foreach (kind; DialogueUnitKindChars) { if (c == kind) { diff --git a/source/parin/engine.d b/source/parin/engine.d index beca4fd..62888e4 100644 --- a/source/parin/engine.d +++ b/source/parin/engine.d @@ -24,7 +24,7 @@ public import joka.faults; public import joka.math; public import joka.types; -@safe: +@safe @nogc nothrow: EngineState engineState; @@ -160,7 +160,7 @@ struct DrawOptions { Hook hook = Hook.topLeft; /// An value representing the origin point of the drawn object when origin is set to zero. Flip flip = Flip.none; /// An value representing flipping orientations. - @safe: + @safe @nogc nothrow: /// Initializes the options with the given scale. this(Vec2 scale) { @@ -196,7 +196,7 @@ struct Camera { bool isAttached; /// Indicates whether the camera is currently in use. bool isCentered; /// Determines if the camera's origin is at the center instead of the top left. - @safe: + @safe @nogc nothrow: /// Initializes the camera with the given position and optional centering. this(float x, float y, bool isCentered = false) { @@ -210,11 +210,61 @@ struct Camera { return isCentered ? Hook.center : Hook.topLeft; } + /// Returns the origin of the camera. + Vec2 origin() { + return Rect(position, resolution).origin(hook); + } + /// Returns the area covered by the camera. Rect area() { return Rect(position, resolution).area(hook); } + /// Returns the top left point of the camera. + Vec2 topLeftPoint() { + return area.topLeftPoint; + } + + /// Returns the top point of the camera. + Vec2 topPoint() { + return area.topPoint; + } + + /// Returns the top right point of the camera. + Vec2 topRightPoint() { + return area.topRightPoint; + } + + /// Returns the left point of the camera. + Vec2 leftPoint() { + return area.leftPoint; + } + + /// Returns the center point of the camera. + Vec2 centerPoint() { + return area.centerPoint; + } + + /// Returns the right point of the camera. + Vec2 rightPoint() { + return area.rightPoint; + } + + /// Returns the bottom left point of the camera. + Vec2 bottomLeftPoint() { + return area.bottomLeftPoint; + } + + /// Returns the bottom point of the camera. + Vec2 bottomPoint() { + return area.bottomPoint; + } + + /// Returns the bottom right point of the camera. + Vec2 bottomRightPoint() { + return area.bottomRightPoint; + } + /// Moves the camera to follow the target position at the specified speed. void followPosition(Vec2 target, float speed) { position = position.moveTo(target, Vec2(speed)); @@ -243,7 +293,7 @@ struct Camera { } isAttached = true; auto temp = this.toRl(); - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { temp.target.x = floor(temp.target.x); temp.target.y = floor(temp.target.y); temp.offset.x = floor(temp.offset.x); @@ -267,7 +317,7 @@ struct TextId { GenerationalIndex data; alias data this; - @safe: + @safe @nogc nothrow: /// Returns the length of the text associated with the resource identifier. Sz length() { @@ -304,7 +354,7 @@ struct TextId { struct Texture { rl.Texture2D data; - @safe: + @safe @nogc nothrow: /// Checks if the texture is not loaded. bool isEmpty() { @@ -349,7 +399,7 @@ struct TextureId { GenerationalIndex data; alias data this; - @safe: + @safe @nogc nothrow: /// Returns the width of the texture associated with the resource identifier. int width() { @@ -398,7 +448,7 @@ struct Font { int runeSpacing; /// The spacing between individual characters. int lineSpacing; /// The spacing between lines of text. - @safe: + @safe @nogc nothrow: /// Checks if the font is not loaded. bool isEmpty() { @@ -433,7 +483,7 @@ struct FontId { GenerationalIndex data; alias data this; - @safe: + @safe @nogc nothrow: /// Returns the spacing between individual characters of the font associated with the resource identifier. int runeSpacing() { @@ -480,7 +530,7 @@ struct FontId { struct Sound { Variant!(rl.Sound, rl.Music) data; - @safe: + @safe @nogc nothrow: /// Checks if the sound is not loaded. bool isEmpty() { @@ -575,7 +625,7 @@ struct SoundId { GenerationalIndex data; alias data this; - @safe: + @safe @nogc nothrow: /// Returns the current playback time of the sound associated with the resource identifier. float time() { @@ -621,7 +671,7 @@ struct SoundId { struct Viewport { rl.RenderTexture2D data; - @safe: + @safe @nogc nothrow: /// Checks if the viewport is not loaded. bool isEmpty() { @@ -663,6 +713,7 @@ struct Viewport { struct EngineFlags { bool isUpdating; + bool isPixelSnapped; bool isPixelPerfect; bool isCursorVisible; } @@ -678,7 +729,7 @@ struct EngineResourceGroup(T) { GenerationalList!LStr names; GenerationalList!Sz tags; - @safe: + @safe @nogc nothrow: Sz length() { return data.length; @@ -741,7 +792,7 @@ struct EngineResources { EngineResourceGroup!Font fonts; EngineResourceGroup!Sound sounds; - @safe: + @safe @nogc nothrow: void free(Sz tag = 0) { texts.free(tag); @@ -757,7 +808,7 @@ struct EngineViewport { int targetHeight; alias data this; - @safe: + @safe @nogc nothrow: bool isLocking() { return (targetWidth != 0 && targetHeight != 0) && (data.width != targetWidth && data.height != targetHeight); @@ -792,7 +843,7 @@ struct EngineState { Filter defaultFilter; Sz tickCount; - @safe: + @safe @nogc nothrow: void free() { debug { @@ -1170,8 +1221,9 @@ void openWindow(int width, int height, IStr appPath, IStr title = "Parin") { /// You should avoid calling this function manually. @trusted void updateWindow(bool function(float dt) updateFunc) { - static bool function(float _dt) @trusted _updateFunc; + static bool function(float _dt) @trusted @nogc nothrow _updateFunc; + @trusted @nogc nothrow static bool _updateWindow() { // Begin drawing. if (isResolutionLocked) { @@ -1193,10 +1245,9 @@ void updateWindow(bool function(float dt) updateFunc) { auto ratio = maxSize / minSize; auto minRatio = min(ratio.x, ratio.y); if (isPixelPerfect) { - // TODO: Make an equals function in Joka that can change the epsilon value. auto roundMinRatio = round(minRatio); auto floorMinRation = floor(minRatio); - minRatio = (abs(minRatio - roundMinRatio) < 0.015f) ? roundMinRatio : floorMinRation; + minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation; } auto targetSize = minSize * Vec2(minRatio); @@ -1253,7 +1304,7 @@ void updateWindow(bool function(float dt) updateFunc) { } // Maybe bad idea, but makes life of no-attribute people easier. - _updateFunc = cast(bool function(float _dt) @trusted) updateFunc; + _updateFunc = cast(bool function(float _dt) @trusted @nogc nothrow) updateFunc; engineState.flags.isUpdating = true; version(WebAssembly) { @@ -1284,6 +1335,21 @@ void closeWindow() { rl.CloseWindow(); } +/// Returns true if the drawing is snapped to pixel coordinates. +bool isPixelSnapped() { + return engineState.flags.isPixelSnapped; +} + +/// Sets whether drawing should be snapped to pixel coordinates. +void setIsPixelSnapped(bool value) { + engineState.flags.isPixelSnapped = value; +} + +/// Toggles whether drawing is snapped to pixel coordinates on or off. +void toggleIsPixelSnapped() { + setIsPixelSnapped(!isPixelSnapped); +} + /// Returns true if the drawing is done in a pixel perfect way. bool isPixelPerfect() { return engineState.flags.isPixelPerfect; @@ -1490,10 +1556,9 @@ Vec2 mouseScreenPosition() { auto window = windowSize; auto minRatio = min(window.x / engineState.viewport.width, window.y / engineState.viewport.height); if (isPixelPerfect) { - // TODO: Make an equals function in Joka that can change the epsilon value. auto roundMinRatio = round(minRatio); auto floorMinRation = floor(minRatio); - minRatio = (abs(minRatio - roundMinRatio) < 0.015f) ? roundMinRatio : floorMinRation; + minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation; } auto targetSize = engineState.viewport.size * Vec2(minRatio); // We use touch because it works on desktop, web and mobile. @@ -1781,7 +1846,7 @@ void updateSound(SoundId sound) { /// Draws a rectangle with the specified area and color. @trusted void drawRect(Rect area, Color color = white) { - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.DrawRectanglePro(area.floor().toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl()); } else { rl.DrawRectanglePro(area.toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl()); @@ -1796,7 +1861,7 @@ void drawVec2(Vec2 point, float size, Color color = white) { /// Draws a circle with the specified area and color. @trusted void drawCirc(Circ area, Color color = white) { - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.DrawCircleV(area.position.floor().toRl(), area.radius, color.toRl()); } else { rl.DrawCircleV(area.position.toRl(), area.radius, color.toRl()); @@ -1806,7 +1871,7 @@ void drawCirc(Circ area, Color color = white) { /// Draws a line with the specified area, thickness, and color. @trusted void drawLine(Line area, float size, Color color = white) { - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.DrawLineEx(area.a.floor().toRl(), area.b.floor().toRl(), size, color.toRl()); } else { rl.DrawLineEx(area.a.toRl(), area.b.toRl(), size, color.toRl()); @@ -1839,7 +1904,7 @@ void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions opti } auto origin = options.origin == Vec2() ? target.origin(options.hook) : options.origin; - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.DrawTexturePro( texture.data, area.floor().toRl(), @@ -1885,14 +1950,14 @@ void drawRune(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOp auto rect = toParin(rl.GetGlyphAtlasRec(font.data, rune)); auto origin = options.origin == Vec2() ? rect.origin(options.hook) : options.origin; rl.rlPushMatrix(); - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.rlTranslatef(position.x.floor(), position.y.floor(), 0.0f); } else { rl.rlTranslatef(position.x, position.y, 0.0f); } rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f); rl.rlScalef(options.scale.x, options.scale.y, 1.0f); - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.rlTranslatef(-origin.x.floor(), -origin.y.floor(), 0.0f); } else { rl.rlTranslatef(-origin.x, -origin.y, 0.0f); @@ -1916,14 +1981,14 @@ void drawText(Font font, IStr text, Vec2 position, DrawOptions options = DrawOpt // TODO: Make it work with negative scale values. auto origin = Rect(measureTextSize(font, text)).origin(options.hook); rl.rlPushMatrix(); - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.rlTranslatef(floor(position.x), floor(position.y), 0.0f); } else { rl.rlTranslatef(position.x, position.y, 0.0f); } rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f); rl.rlScalef(options.scale.x, options.scale.y, 1.0f); - if (isPixelPerfect) { + if (isPixelSnapped || isPixelPerfect) { rl.rlTranslatef(floor(-origin.x), floor(-origin.y), 0.0f); } else { rl.rlTranslatef(-origin.x, -origin.y, 0.0f); diff --git a/source/parin/package.d b/source/parin/package.d index 534cc03..1c25da3 100644 --- a/source/parin/package.d +++ b/source/parin/package.d @@ -10,7 +10,6 @@ module parin; public import joka.ascii; public import joka.io; -public import parin.dialogue; public import parin.engine; public import parin.scene; public import parin.sprite; diff --git a/source/parin/rl/raylib.d b/source/parin/rl/raylib.d index b66ed55..ebe342f 100644 --- a/source/parin/rl/raylib.d +++ b/source/parin/rl/raylib.d @@ -1258,7 +1258,6 @@ void OpenURL (const(char)* url); // Open URL with default system browser (if ava // NOTE: Following functions implemented in module [utils] //------------------------------------------------------------------ -void TraceLog (int logLevel, const(char)* text, ...); // Show trace log messages (LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERROR...) void SetTraceLogLevel (int logLevel); // Set the current threshold (minimum) log level void* MemAlloc (uint size); // Internal memory allocator void* MemRealloc (void* ptr, uint size); // Internal memory reallocator @@ -1641,7 +1640,6 @@ const(char)* CodepointToUTF8 (int codepoint, int* utf8Size); // Encode one codep int TextCopy (char* dst, const(char)* src); // Copy one string to another, returns bytes copied bool TextIsEqual (const(char)* text1, const(char)* text2); // Check if two text string are equal uint TextLength (const(char)* text); // Get text length, checks for '\0' ending -const(char)* TextFormat (const(char)* text, ...); // Text formatting with variables (sprintf() style) const(char)* TextSubtext (const(char)* text, int position, int length); // Get a piece of a text string char* TextReplace (char* text, const(char)* replace, const(char)* by); // Replace text string (WARNING: memory must be freed!) char* TextInsert (const(char)* text, const(char)* insert, int position); // Insert text in a position (WARNING: memory must be freed!) diff --git a/source/parin/sprite.d b/source/parin/sprite.d index f123a1a..8fa1b18 100644 --- a/source/parin/sprite.d +++ b/source/parin/sprite.d @@ -14,7 +14,7 @@ module parin.sprite; import parin.engine; -@safe: +@safe @nogc nothrow: struct SpriteAnimation { ubyte frameRow; @@ -28,7 +28,7 @@ struct SpriteAnimationGroup2 { ubyte frameSpeed = 6; enum angleStep = 180.0f; - @safe: + @safe @nogc nothrow: SpriteAnimation pick(float angle) { auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length; @@ -42,7 +42,7 @@ struct SpriteAnimationGroup4 { ubyte frameSpeed = 6; enum angleStep = 90.0f; - @safe: + @safe @nogc nothrow: SpriteAnimation pick(float angle) { // NOTE: This is a hack to make things look better in simple cases. @@ -61,7 +61,7 @@ struct SpriteAnimationGroup8 { ubyte frameSpeed = 6; enum angleStep = 45.0f; - @safe: + @safe @nogc nothrow: SpriteAnimation pick(float angle) { auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length; @@ -75,7 +75,7 @@ struct SpriteAnimationGroup16 { ubyte frameSpeed = 6; enum angleStep = 22.5f; - @safe: + @safe @nogc nothrow: SpriteAnimation pick(float angle) { auto id = (cast(int) round(snap(angle, angleStep) / angleStep)) % frameRows.length; @@ -92,7 +92,7 @@ struct Sprite { SpriteAnimation animation; Vec2 position; - @safe: + @safe @nogc nothrow: this(int width, int height, ushort atlasLeft, ushort atlasTop, SpriteAnimation animation = SpriteAnimation()) { this.width = width; diff --git a/source/parin/tilemap.d b/source/parin/tilemap.d index b106c90..c77243b 100644 --- a/source/parin/tilemap.d +++ b/source/parin/tilemap.d @@ -21,7 +21,7 @@ public import joka.faults; public import joka.math; public import joka.types; -@safe: +@safe @nogc nothrow: struct Tile { Sz id; @@ -29,7 +29,7 @@ struct Tile { int height; Vec2 position; - @safe: + @safe @nogc nothrow: Vec2 size() { return Vec2(width, height); @@ -67,13 +67,13 @@ struct TileMap { Vec2 position; alias data this; - @safe: + @safe @nogc nothrow: this(short value, int tileWidth, int tileHeight) { this.tileWidth = tileWidth; this.tileHeight = tileHeight; - this.estimatedMaxRowCount = data.maxRowCount; - this.estimatedMaxColCount = data.maxColCount; + this.estimatedMaxRowCount = maxRowCount; + this.estimatedMaxColCount = maxColCount; this.data.fill(value); } @@ -134,7 +134,7 @@ struct TileMap { position = position.moveToWithSlowdown(target, Vec2(deltaTime), slowdown); } - /// Returns the top left world position of a tile. + /// Returns the top left world position of a grid position. Vec2 worldPosition(Sz row, Sz col, DrawOptions options = DrawOptions()) { auto targetTileWidth = cast(int) (tileWidth * options.scale.x); auto targetTileHeight = cast(int) (tileHeight * options.scale.y); @@ -147,7 +147,12 @@ struct TileMap { return temp.area(options.hook).position; } - IVec2 firstMapPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) { + /// Returns the top left world position of a grid position. + Vec2 worldPosition(IVec2 gridPosition, DrawOptions options = DrawOptions()) { + return worldPosition(gridPosition.y, gridPosition.x, options); + } + + IVec2 firstGridPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) { auto result = IVec2(); auto targetTileWidth = cast(int) (tileWidth * options.scale.x); auto targetTileHeight = cast(int) (tileHeight * options.scale.y); @@ -156,7 +161,7 @@ struct TileMap { return result; } - IVec2 lastMapPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) { + IVec2 lastGridPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) { auto result = IVec2(); auto targetTileWidth = cast(int) (tileWidth * options.scale.x); auto targetTileHeight = cast(int) (tileHeight * options.scale.y); @@ -165,6 +170,37 @@ struct TileMap { result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, estimatedMaxColCount)); return result; } + + auto gridPositions(Vec2 topLeftWorldPosition, Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) { + static struct Range { + IVec2 first; + IVec2 last; + IVec2 position; + + bool empty() { + return position == last; + } + + IVec2 front() { + return position; + } + + void popFront() { + position.x += 1; + if (position.x >= TileMap.maxColCount) { + position.x = first.x; + position.y += 1; + } + } + } + + auto result = Range( + firstGridPosition(topLeftWorldPosition, options), + lastGridPosition(bottomRightWorldPosition, options), + ); + result.position = result.first; + return result; + } } Result!TileMap toTileMap(IStr csv, int tileWidth, int tileHeight) { @@ -185,6 +221,7 @@ Result!TileMap loadRawTileMap(IStr path, int tileWidth, int tileHeight) { } void drawTile(Texture texture, Tile tile, DrawOptions options = DrawOptions()) { + if (texture.isEmpty) return; drawTextureArea(texture, tile.textureArea(texture.width / tile.width), tile.position, options); } @@ -195,9 +232,8 @@ void drawTile(TextureId texture, Tile tile, DrawOptions options = DrawOptions()) void drawTileMap(Texture texture, TileMap map, Camera camera, DrawOptions options = DrawOptions()) { auto targetTileWidth = cast(int) (map.tileWidth * options.scale.x); auto targetTileHeight = cast(int) (map.tileHeight * options.scale.y); - auto cameraArea = camera.area; - auto colRow1 = map.firstMapPosition(cameraArea.topLeftPoint, options); - auto colRow2 = map.lastMapPosition(cameraArea.bottomRightPoint, options); + auto colRow1 = map.firstGridPosition(camera.topLeftPoint, options); + auto colRow2 = map.lastGridPosition(camera.bottomRightPoint, options); if (colRow1.x == colRow2.x || colRow1.y == colRow2.y) return; foreach (row; colRow1.y .. colRow2.y) { diff --git a/source/parin/timer.d b/source/parin/timer.d index 3732791..9213098 100644 --- a/source/parin/timer.d +++ b/source/parin/timer.d @@ -13,7 +13,7 @@ module parin.timer; import joka.math; -@safe: +@safe @nogc nothrow: struct Timer { float time = 1.0f; @@ -22,7 +22,7 @@ struct Timer { bool isPaused; bool canRepeat; - @safe: + @safe @nogc nothrow: this(float duration, bool canRepeat = false) { this.time = duration;