<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[erebor engine devlog]]></title><description><![CDATA[Let's build games with Ruby]]></description><link>https://blog.ldk.dev/</link><image><url>https://blog.ldk.dev/favicon.png</url><title>erebor engine devlog</title><link>https://blog.ldk.dev/</link></image><generator>Ghost 5.80</generator><lastBuildDate>Mon, 06 Apr 2026 16:51:06 GMT</lastBuildDate><atom:link href="https://blog.ldk.dev/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Improved integration with the Tiled map editor]]></title><description><![CDATA[Today I finished making some improvements to the engine's built-in integration with the Tiled map editor and the TMX file format.]]></description><link>https://blog.ldk.dev/improved-integration-with-the-tiled-map-editor/</link><guid isPermaLink="false">663fbb8d643f5e0001ca892a</guid><category><![CDATA[Tiled]]></category><category><![CDATA[Map Editor]]></category><category><![CDATA[Game Engine]]></category><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Sat, 11 May 2024 18:59:30 GMT</pubDate><media:content url="https://blog.ldk.dev/content/images/2024/05/Screenshot-2567-05-12-at-01.57.17.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.ldk.dev/content/images/2024/05/Screenshot-2567-05-12-at-01.57.17.png" alt="Improved integration with the Tiled map editor"><p>Today I finished making some improvements to the engine&apos;s built-in integration with the <a href="https://www.mapeditor.org/?ref=blog.ldk.dev">Tiled</a> and the TMX file format:</p>
<ul>
<li>Implemented support for nested layer groups</li>
<li>Fixed a crash that occured on maps with unset background color</li>
<li>Added support for loading tilesets from external *.tsx files</li>
<li>Enabled tilesets that use a single spritesheet image (both embedded and external)</li>
<li>The render layers api (<a href="https://blog.ldk.dev/render-layers/">https://blog.ldk.dev/render-layers/</a>) can now be used to set an explicit render order on individual map layers and objects (so you can blend your environment with non-map entities, like characters hiding in the scenery)</li>
<li>Layers and objects can now be rendered with custom opacity values</li>
<li>Enabled tint color for adjusting the color of objects and layers</li>
</ul>
<p>We&apos;re getting pretty close to full support for every feature of the TMX map format!</p>
<p>The following features are still unsupported:</p>
<ul>
<li>Infinite maps</li>
<li>Image layers</li>
<li>Polygon and polyline objects</li>
<li>Embedded text</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[🥳 Feature complete: everything about displaying text and labels is finished]]></title><description><![CDATA[I've been more or less ignoring the text-rendering part of the project ever since we got to "can display an FPS counter" since throughout development that's all I needed. I have finally caught up and finished everything that I had planned related to displaying text inside a game.]]></description><link>https://blog.ldk.dev/everything-about-displaying-text-and-labels-is-finished/</link><guid isPermaLink="false">65fd82c5643f5e0001ca87dd</guid><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Fri, 22 Mar 2024 13:45:28 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://blog.ldk.dev/content/media/2024/03/untitled_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://blog.ldk.dev/content/media/2024/03/untitled.mp4" poster="https://img.spacergif.org/v1/720x444/0a/spacer.png" width="720" height="444" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://blog.ldk.dev/content/media/2024/03/untitled_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">1:11</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><p>I&apos;ve been more or less ignoring the text-rendering part of the project ever since we got to &quot;can display an FPS counter&quot; since throughout development that&apos;s all I needed, but that was absolutely not enough even for a first release &#x1F923;. I finally caught up and finished everything that I had planned related to displaying text inside a game.</p>
<p>This week I added:</p>
<ul>
<li>Support for loading and caching custom fonts from <em>*.ttf</em> file assets</li>
<li>Support for text rotation</li>
<li>Typewriter animation effect</li>
<li>Smart word wrapping based on a container&apos;s pixel-width</li>
<li>Justifying multiline text to the center of a rect</li>
<li>Functions to measure the dimensions (width, height, center point and y-offset) of a chunk of text (even if it&apos;s rotated, or virtual and not displayed)</li>
<li>Adjustable padding (line-spacing) between lines</li>
<li>Drawing text onto specific entity layers and render targets</li>
<li>Linewise scrolling to prevent text from overflowing the pixel-height of a container</li>
</ul>
<p>I&apos;m also working on a basic RPG dialog scripting package that you&apos;ll be able to drop right into your own games. Until then, you can find this helpful example in <code>examples/dialogue</code></p>
<p>You can see it in action above.</p>
]]></content:encoded></item><item><title><![CDATA[Experimental: Modeling entity relations]]></title><description><![CDATA[has_many, has_one, belongs_to...]]></description><link>https://blog.ldk.dev/experimental-modeling-entity-relations/</link><guid isPermaLink="false">65fd87e5643f5e0001ca880a</guid><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Fri, 22 Mar 2024 13:43:20 GMT</pubDate><media:content url="https://blog.ldk.dev/content/images/2024/03/clocktower-1.webp" medium="image"/><content:encoded><![CDATA[<h1 id="hasmany-hasone-belongsto">has_many, has_one, belongs_to</h1>
<img src="https://blog.ldk.dev/content/images/2024/03/clocktower-1.webp" alt="Experimental: Modeling entity relations"><p>I&apos;ve added an experimental <code>ECS::EntityRelations</code> extension to the ECS to make it easier to model relationships between entities. It&apos;s <em>usually</em> best to avoid that pattern in gamedev, so the design of the framework will continue to discourage overusing it and turning your game into some kind of Rails application... <strong>but</strong> I&apos;ve found that certain kinds of problems in games just cannot be reasonably modeled any other way. The exact API will probably change before it&apos;s settled, but for now we have this:</p>
<p>The EntityBuilder DSL now includes these three new methods: <code>belongs_to</code>, <code>has_one</code>, and <code>has_many</code>. Entities connected in this way will be loosely associated by their entity  ID using a <code>BelongsTo({ relation: :entity, id: nil })</code> component under the hood (it is automatically added to the entity on that side of the relationship).</p>
<p>You can name your relations anything you want - they do not have to match the name of any class. As long as the value of <code>belongs_to[:id]</code> on an association matches the ID of your entity, you can create as many virtual <code>has_one</code> and <code>has_many</code> relations as you like with different sets of filters, and the result set will include whatever mix of entities those filters match.</p>
<h6 id="example">Example</h6>
<pre><code class="language-ruby">components do
	Person({ name: &quot;Unknown&quot; })
	Score({ points: 0 })
end

Player = entity do
	belongs_to :team
	Person()
end

Goal = entity do
	belongs_to :team
	Score()
end

Team = entity do
	has_many :players, filter: [Person]
	has_one :goal, filter: [Score]
end

world :example_game do
	entity Player, name: &quot;Alice&quot;, as: :alice
	entity Player, name: &quot;Bob&quot;, as: :bob
	entity Team, as :nerds

	systems AssignTeams, UpdateScore
end

AssignTeams = system do
	filter Person
	tick do |args|
		entities.each do |player|
			player.team = world.nerds if player.team.nil?
		end
	end
end

UpdateScore = system do
	tick do |args|
		world.teams.each do |team|
			if team.goal.nil?
				goal = Goal.new({ score: 0 })
				goal.team = team
				world.entities &lt;&lt; [goal]
			end
		end
	end
end

nerds = world.nerds

nerds.players #&gt; [Player&lt;id: 1, Alice&gt;, Player&lt;id: 2, Bob&gt;]
world.alice.team #=&gt; Team&lt;id: 3, players: [Alice, Bob], goal: Goal&lt;id: 4, score: 0&gt;
nerds.goal #=&gt; Goal&lt;id: 4, score: 0, team: Team&lt;id: 3, players: [Alice, Bob]&gt;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[🥳 New feature: entity class composition with reusable component sets]]></title><description><![CDATA[<p>I added the new <code>extends</code> keyword to the EntityBuilder DSL because a lot of entities start out with the same handful of components, and repeating all of this boilerplate in each new entity definition can end up obscuring all the interesting stuff that makes each one of those entities different.</p>]]></description><link>https://blog.ldk.dev/new-feature-entity-class-composition-with-reusable-component-sets/</link><guid isPermaLink="false">65fd877a643f5e0001ca87ff</guid><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Fri, 22 Mar 2024 13:41:38 GMT</pubDate><content:encoded><![CDATA[<p>I added the new <code>extends</code> keyword to the EntityBuilder DSL because a lot of entities start out with the same handful of components, and repeating all of this boilerplate in each new entity definition can end up obscuring all the interesting stuff that makes each one of those entities different. Now you can mix and match component sets and reuse them by <em>extending</em> the components of another class, or even merge several other entities together.</p>
<p>You can use <code>extends</code> to merge the set of components and default attributes from one or more existing entities to create new ones through composition rather than inheritance.</p>
<h6 id="example">Example</h6>
<pre><code class="language-ruby">Shape = entity do
	Position()
	Scale()
	Rotation()
end

Container = entity do
	Position({ x: 0, y: 0})
	Scale({ width: 1024, height: 1024 })
	Centered()
end

Label = entity do
	extends Shape
	Text()
	Font()
end

Title = entity do
	extends Label, Container
	Text({ color: &quot;#cca533&quot;.rgb.lighten(0.25) })
	Font({ size: 48 })
	Visible()
end
</code></pre>
]]></content:encoded></item><item><title><![CDATA[New feature: Render Layers]]></title><description><![CDATA[Now it's much easier to manage the z-order position when rendering multiple entity outputs. For example:]]></description><link>https://blog.ldk.dev/render-layers/</link><guid isPermaLink="false">65fd9411643f5e0001ca889c</guid><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Thu, 14 Mar 2024 14:29:00 GMT</pubDate><content:encoded><![CDATA[<p>Now it&apos;s much easier to manage the z-order position when rendering multiple entity outputs. For example:</p><p></p><pre><code class="language-ruby">```ruby
world :previous_behavior do
    # Automatically rendered in ascending entity ID order (the pre-existing behavior)
    entity BackgroundImage
    entity Panel
    entity Button
    entity MouseCursor
    # Previously these entities would be rendered to args.outputs,
    # now this has changed to args.outputs[:default].
end

# Set a named output target (args.outputs[KEY]) for all instances of an entity class
entity :background_image do
    # ...components
    layer :background
end

entity :panel do; layer :ui; end
entity :mouse_cursor do; layer :top; end

world :rendering_layers do
    entity BackgroundImage
    entity Panel
    entity Button, layer: { key: :ui } # Same as above, but specific to this entity
    entity MouseCursor

    # List the layers in the order they should be rendered. Entities of any other layer
    # (except for :default) not included in this list will not be rendered.
    # Within each layer, entities will be rendered in order of ascending id.
    render :background, :ui, :default, :top

    # You can omit the `render` line if you only want to render :default
    # render :default # (implied)
end

world :using_layer_blocks do
    # You can also use a `layer` block to assign a layer to every entity inside
    layer :background do
        entity BackgroundImage
        entity Panel
        entity SplashImage
    end

    # You can declare your layer blocks in any order.
    layer :top do; entity MouseCursor; end
    layer :ui do; entity Button; end

    # Rendered to :default (unless a layer is specified in the entity class)
    entity Image

    # If you omit :default, it will be added at the end of the list.
    render :background, :ui, :top # =&gt; [:background, :ui, :top, :default]
end
```</code></pre>]]></content:encoded></item><item><title><![CDATA[Mouse input is now feature complete]]></title><description><![CDATA[Mouse input is now fully implemented, with an updated and simpler to to use API]]></description><link>https://blog.ldk.dev/mouse-input-is-now-feature-complete/</link><guid isPermaLink="false">65fd9143643f5e0001ca886f</guid><dc:creator><![CDATA[Logan Koester]]></dc:creator><pubDate>Thu, 14 Mar 2024 14:18:00 GMT</pubDate><media:content url="https://blog.ldk.dev/content/images/2024/03/mouse_input_demo_header.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.ldk.dev/content/images/2024/03/mouse_input_demo_header.png" alt="Mouse input is now feature complete"><p>Mouse input is now fully implemented, featuring</p>
<ul>
<li>Support for Left, right, and middle mouse buttons each with<code>down</code>, <code>up</code>, <code>pressed</code>, and <code>released</code> states</li>
<li>Mouse wheel</li>
<li>Current position and delta position since the previous frame</li>
<li>Option to grab the mouse cursor and keep it inside of the game window</li>
<li>Option show or hide the mouse cursor</li>
<li>Replacing the cursor with a custom sprite is demonstrated in <code>examples/input_mouse</code></li>
</ul>
<p>See demo:</p>
<figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://blog.ldk.dev/content/media/2024/03/mouse_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://blog.ldk.dev/content/media/2024/03/mouse.mp4" poster="https://img.spacergif.org/v1/2320x1434/0a/spacer.png" width="2320" height="1434" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://blog.ldk.dev/content/media/2024/03/mouse_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:47</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><h2 id="the-updated-mouse-input-api-is-as-follows">The updated mouse input API is as follows:</h2><pre><code class="language-ruby"># Mouse input API

Mouse.position # Current mouse cursor position (x &amp; y)
Mouse.delta_position # Delta position since the previous frame

Mouse.cursor.show! # Show the mouse cursor
Mouse.cursor.hide! # Hide the mouse cursor

Mouse.cursor.grab! # Force the cursor to stay inside the game window
Mouse.cursor.release! # Allow the cursor to move outside of the window

Mouse.wheel # (x, y)
Mouse.buttons.[left|right|middle].[down?|up?|pressed?|released?]
</code></pre>
<p></p>]]></content:encoded></item></channel></rss>