<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>bex edmondson | Blog</title><description/><link>https://bexedmondson.com/</link><language>en</language><item><title>Hexbuilder: Devlog #3</title><link>https://bexedmondson.com/blog/2026-01-25-hexbuilder-devlog-3/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2026-01-25-hexbuilder-devlog-3/</guid><description>New year, big changes!

</description><pubDate>Sun, 25 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;meta name=&quot;astro-view-transitions-enabled&quot; content=&quot;true&quot;&gt;&lt;meta name=&quot;astro-view-transitions-fallback&quot; content=&quot;animate&quot;&gt;
&lt;p&gt;Catch up on the Hexbuilder progress starting &lt;a href=&quot;../2025-11-09-hexbuilder-devlog-0&quot;&gt;here!&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;announcement&quot;&gt;Announcement&lt;/h2&gt;&lt;a href=&quot;#announcement&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Announcement”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I’m happy (and slightly scared) to publicly commit to releasing Hexbuilder this year! I’m more excited about this project then I have been about any other, and I think have the ability, capacity and resources to get it there. I’m not sure of the specifics of how yet, but saying I’m going to is the first step. So: I’m going to!&lt;/p&gt;
&lt;p&gt;Now that’s out there, on to what’s changed since my last update…&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;screenshots&quot;&gt;Screenshots&lt;/h2&gt;&lt;a href=&quot;#screenshots&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Screenshots”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;section id=&quot;gallery-hexbuilder-log3&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.CeVWZQVA_MiWKE.webp&quot; srcset=&quot;/_astro/map.CeVWZQVA_MiWKE.webp 300w, /_astro/map.CeVWZQVA_bVikX.webp 600w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.CeVWZQVA_1zd1W5.webp&quot; srcset=&quot;/_astro/map.CeVWZQVA_ZSod5y.webp 640w, /_astro/map.CeVWZQVA_r7UfU.webp 750w, /_astro/map.CeVWZQVA_1LetSE.webp 828w, /_astro/map.CeVWZQVA_Z1GGcqd.webp 1080w, /_astro/map.CeVWZQVA_wEm3O.webp 1280w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1329&quot; height=&quot;748&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Hex Map as of dev log #3 &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;&lt;a href=&quot;#updates&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Updates”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;In my last post, I talked about adding storage and continuing to improve information accessibility, and that I was considering adding a concept of resident happiness. I have updates on all three of those, plus more!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;storage&quot;&gt;Storage&lt;/h3&gt;&lt;a href=&quot;#storage&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Storage”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;We have storage! Resource quantities are now capped, with caps increasing based on which buildings are present in your town. This has added a lot of needed complexity to the game and provides good motivation for players to build more. Also, due to the added restrictions I’ve been able to delineate very early gameplay decisions that will teach players the basics.&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/building-stockpile.B_lxV5JC_ZQ8BIJ.webp&quot; alt=&quot;A top down view of a hex tile with some boxes, crates and barrels stacked in a pile.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1329&quot; height=&quot;748&quot;&gt;
&lt;p&gt;And I added a couple of new buildings to fit the theme, like this stockpile!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;information-accessibility&quot;&gt;Information accessibility&lt;/h3&gt;&lt;a href=&quot;#information-accessibility&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Information accessibility”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I spent a lot of time improving which information was displayed and where before my last post, but there was still a lot left to do! I’ve added a lot and improved a lot of existing visuals, and refactored things to simplify implementing UI as I go in the future.&lt;/p&gt;
&lt;section id=&quot;gallery-hexbuilder-log3_infoui&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A popup showing details of a mill, showing that it will produce 2 food by default. The popup also says that the fields to the north and north east are each adding an extra 1 food production.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopup.BkOZWacy_Z1cut6D.webp&quot; srcset=&quot;/_astro/unlockedpopup.BkOZWacy_Z1cut6D.webp 300w, /_astro/unlockedpopup.BkOZWacy_Z2q1hC.webp 600w&quot; alt=&quot;A popup showing details of a mill, showing that it will produce 2 food by default. The popup also says that the fields to the north and north east are each adding an extra 1 food production.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;171&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A popup showing that a cabin, granary and mine can be built on this tile. Each option has a build cost, icon, and green info button on it.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopupbuildtab.BdLxbSFS_2u5NqX.webp&quot; srcset=&quot;/_astro/unlockedpopupbuildtab.BdLxbSFS_2u5NqX.webp 300w, /_astro/unlockedpopupbuildtab.BdLxbSFS_Z1lJXV7.webp 600w&quot; alt=&quot;A popup showing that a cabin, granary and mine can be built on this tile. Each option has a build cost, icon, and green info button on it.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;2&quot; aria-label=&quot;View image 3: Four resource icons with quantities and capacities next to them. Full storage (i.e. with text like 20/20 next to the icon) is indicated with a small bag icon with a no entry symbol over it.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/capacityindicators.BbxFEQV1_2eH91J.webp&quot; alt=&quot;Four resource icons with quantities and capacities next to them. Full storage (i.e. with text like 20/20 next to the icon) is indicated with a small bag icon with a no entry symbol over it.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;3&quot; aria-label=&quot;View image 4: The encyclopedia popup open to the smelter page. There is one foldout visible on the right details panel, showing that adjacent mines will provide 1 bonus stone production.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/encyclopedia.qYd7auxw_Z1bdPja.webp&quot; srcset=&quot;/_astro/encyclopedia.qYd7auxw_Z1bdPja.webp 300w, /_astro/encyclopedia.qYd7auxw_Z3THvq.webp 600w&quot; alt=&quot;The encyclopedia popup open to the smelter page. There is one foldout visible on the right details panel, showing that adjacent mines will provide 1 bonus stone production.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;4&quot; aria-label=&quot;View image 5: The encyclopedia popup open to the large rowboat page. There is three foldouts visible on the right details panel, and one is expanded to show that to unlock this building the player needs to build more rowboats and gather more food (the latter is checked off).&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockrequirements.CjzYqCAv_2ko7ez.webp&quot; srcset=&quot;/_astro/unlockrequirements.CjzYqCAv_2ko7ez.webp 300w, /_astro/unlockrequirements.CjzYqCAv_2wc5bo.webp 600w&quot; alt=&quot;The encyclopedia popup open to the large rowboat page. There is three foldouts visible on the right details panel, and one is expanded to show that to unlock this building the player needs to build more rowboats and gather more food (the latter is checked off).&quot; loading=&quot;eager&quot; data-index=&quot;4&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;172&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopup.BkOZWacy_21oL2j.webp&quot; srcset=&quot;/_astro/unlockedpopup.BkOZWacy_Z2vydFm.webp 640w, /_astro/unlockedpopup.BkOZWacy_c7M7q.webp 750w, /_astro/unlockedpopup.BkOZWacy_YBQIx.webp 828w&quot; alt=&quot;A popup showing details of a mill, showing that it will produce 2 food by default. The popup also says that the fields to the north and north east are each adding an extra 1 food production.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1002&quot; height=&quot;571&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Unlocked popup improvements &lt;/p&gt; &lt;p&gt;Reworked this popup again, to make it more flexible when adding new information.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopupbuildtab.BdLxbSFS_1s17ly.webp&quot; srcset=&quot;/_astro/unlockedpopupbuildtab.BdLxbSFS_ZvSKou.webp 640w, /_astro/unlockedpopupbuildtab.BdLxbSFS_Z1s99Ub.webp 750w, /_astro/unlockedpopupbuildtab.BdLxbSFS_Z17Lj5j.webp 828w&quot; alt=&quot;A popup showing that a cabin, granary and mine can be built on this tile. Each option has a build cost, icon, and green info button on it.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1074&quot; height=&quot;604&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. Build tab improvements &lt;/p&gt; &lt;p&gt;The green i icon opens the encyclopedia to that tile&amp;#39;s info page&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-2&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/capacityindicators.BbxFEQV1_62i7O.webp&quot; alt=&quot;Four resource icons with quantities and capacities next to them. Full storage (i.e. with text like 20/20 next to the icon) is indicated with a small bag icon with a no entry symbol over it.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;286&quot; height=&quot;161&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 3. Full storage indicators &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-3&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/encyclopedia.qYd7auxw_ayD2b.webp&quot; srcset=&quot;/_astro/encyclopedia.qYd7auxw_27fC6x.webp 640w, /_astro/encyclopedia.qYd7auxw_ZvvnzS.webp 750w, /_astro/encyclopedia.qYd7auxw_Z1Qrm3l.webp 828w&quot; alt=&quot;The encyclopedia popup open to the smelter page. There is one foldout visible on the right details panel, showing that adjacent mines will provide 1 bonus stone production.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1074&quot; height=&quot;604&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 4. Encyclopedia foldouts &lt;/p&gt; &lt;p&gt;Foldouts are new in Godot 4.5, and they make a nice way to allow players to see what they want to see without overwhelming them with too much info on display at first glance.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-4&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockrequirements.CjzYqCAv_VWuOo.webp&quot; srcset=&quot;/_astro/unlockrequirements.CjzYqCAv_q7jIh.webp 640w, /_astro/unlockrequirements.CjzYqCAv_1okUtK.webp 750w, /_astro/unlockrequirements.CjzYqCAv_Z13M3tF.webp 828w&quot; alt=&quot;The encyclopedia popup open to the large rowboat page. There is three foldouts visible on the right details panel, and one is expanded to show that to unlock this building the player needs to build more rowboats and gather more food (the latter is checked off).&quot; loading=&quot;lazy&quot; data-index=&quot;4&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1037&quot; height=&quot;595&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 5. Unlock requirements UI &lt;/p&gt; &lt;p&gt;Unlock requirements are now nicely visualised in the encyclopedia - before this it was impossible to know how to unlock a building so this was very needed!&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h4 id=&quot;adjacency-effects&quot;&gt;Adjacency effects&lt;/h4&gt;&lt;a href=&quot;#adjacency-effects&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Adjacency effects”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Adjacency effect visuals got a big visual improvement with this fancy effect:&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/adjacency-effect.TDqFPbGb_1xlkV8.webp&quot; alt=&quot;A top down view of two hex tiles showing a mine and a smelter. A little green stone icon animates quickly from the mine to the smelter.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1024&quot; height=&quot;577&quot;&gt;
&lt;p&gt;It took about 3 days to get working right, but I’m really pleased with the result!&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;toasts&quot;&gt;Toasts&lt;/h4&gt;&lt;a href=&quot;#toasts&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Toasts”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;A new notification system that lets the player know all sorts of information! I originally used the &lt;a href=&quot;https://github.com/godot-journey-adventures/toastparty&quot;&gt;Toast Party&lt;/a&gt; plugin, but it wasn’t as flexible as I needed and was written in GDScript, so I ported it over to C#. And then completely refactored it about three times! I ended up with something really clean and flexible - I even used this system to display a currency change animation&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/toast.DJciq8M9_Z1in6Df.webp&quot; alt=&quot;A blue box containing the words &amp;#34;new resident: Jimberley&amp;#34; transitions down into view, pauses for a second, and then fades out.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;512&quot; height=&quot;98&quot;&gt;
&lt;div&gt;&lt;h3 id=&quot;resident-happiness&quot;&gt;Resident happiness&lt;/h3&gt;&lt;a href=&quot;#resident-happiness&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Resident happiness”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Easily the biggest gameplay change since last time: residents gained sentience! Now each one can have a range of happiness values, affected by a number of different needs.&lt;/p&gt;
&lt;p&gt;Needs get assigned under specific conditions and have a list of satisfaction requirements. These are communicated in the reworked residents popup, and provide a sort of rudimentary tutorial or achievement system - a bit like wants in The Sims.&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/resident-needs.CMriyJLo_6MvgS.webp&quot; alt=&quot;A popup showing details of two residents. The resident listed first, Jonstance, has a sad icon next to their name and a speech bubble under that which says &amp;#34;we need to build more tents!&amp;#34;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1032&quot; height=&quot;241&quot;&gt;
&lt;p&gt;Resident needs don’t currently have much of a direct gameplay impact yet, though that should be very straightforward to change if I think it’s needed. Changes in happiness do get communicated to the player through the new [[#Toasts|toast system]] though so they’re quite front-and-center for motivation purposes. There’s a lot I want to do from here to add depth to this system, so this will be something I work on soon.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;big-changes&quot;&gt;Big changes&lt;/h2&gt;&lt;a href=&quot;#big-changes&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Big changes”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I got a whole lot done other than following up on what I mentioned last time! Here are the three biggest headlines:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;worker-counts-have-more-meaning-part-2&quot;&gt;Worker counts have more meaning, part 2&lt;/h3&gt;&lt;a href=&quot;#worker-counts-have-more-meaning-part-2&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Worker counts have more meaning, part 2”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Worker counts gained a purpose in my last post, and in this one I added something small but powerful - maxxing out worker capacity at a building can now provide a production bonus, indicated by this nice little shine effect:&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/max-worker-bonus.C6UskL6q_Z1MWx0U.webp&quot; alt=&quot;A top down view of a hex tile with a cow field, with UI indicating 2 of 2 workers are assigned. The numbers and worker icon have a white diagonal shine that transitions across them every second or so.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;512&quot; height=&quot;386&quot;&gt;
&lt;p&gt;And I added this feature to buildings using…✨components✨!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;componentising-building-data&quot;&gt;Componentising building data&lt;/h3&gt;&lt;a href=&quot;#componentising-building-data&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Componentising building data”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;A huge overhaul of how building data is stored! I used a pattern that I’m very familiar with from previous work, and broke each data chunk into its own class which inherits from &lt;code dir=&quot;auto&quot;&gt;AbstractTileDataComponent&lt;/code&gt;. Each &lt;code dir=&quot;auto&quot;&gt;CustomTileData&lt;/code&gt; instance then has just one array of abstract components, and at runtime these are mapped into a dictionary with the component type as the key. There are some minor limitations around component type inheritance, but overall I’m way happier with this system. It’s much, much more flexible and readable, and makes adding new types of building data very easy.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;locked-cell-visuals&quot;&gt;Locked cell visuals&lt;/h3&gt;&lt;a href=&quot;#locked-cell-visuals&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Locked cell visuals”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I figured out how to grey out individual cells using alternate tiles, so the ugly hexagonal overlay is now gone!&lt;/p&gt;
&lt;section id=&quot;gallery-hexbuilder-log3_locked&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot of a hexagonal tile, with a hexagonal grey overlay on top. The tile is of a forest and the trees stick out above the overlay, making the tops look oddly highlighted.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/before.B92gTL-L_ZSS00F.webp&quot; srcset=&quot;/_astro/before.B92gTL-L_ZSS00F.webp 300w, /_astro/before.B92gTL-L_yO92h.webp 600w&quot; alt=&quot;A screenshot of a hexagonal tile, with a hexagonal grey overlay on top. The tile is of a forest and the trees stick out above the overlay, making the tops look oddly highlighted.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;207&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A screenshot of a hexagonal tile of a forest, where the whole tile image is darkened perfectly uniformly.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/after.DXb9DUsk_1vpefY.webp&quot; srcset=&quot;/_astro/after.DXb9DUsk_1vpefY.webp 300w, /_astro/after.DXb9DUsk_1KOkOk.webp 600w&quot; alt=&quot;A screenshot of a hexagonal tile of a forest, where the whole tile image is darkened perfectly uniformly.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;222&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/before.B92gTL-L_1QJepL.webp&quot; srcset=&quot;/_astro/before.B92gTL-L_Z4A96f.webp 640w, /_astro/before.B92gTL-L_1xNA7K.webp 750w, /_astro/before.B92gTL-L_Z14FnBp.webp 828w&quot; alt=&quot;A screenshot of a hexagonal tile, with a hexagonal grey overlay on top. The tile is of a forest and the trees stick out above the overlay, making the tops look oddly highlighted.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1038&quot; height=&quot;715&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. before &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/after.DXb9DUsk_Zfd6rR.webp&quot; srcset=&quot;/_astro/after.DXb9DUsk_BYFnV.webp 640w&quot; alt=&quot;A screenshot of a hexagonal tile of a forest, where the whole tile image is darkened perfectly uniformly.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;723&quot; height=&quot;535&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. before &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h2&gt;&lt;a href=&quot;#whats-next&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What’s next?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Making the map more dynamic by adding map interactions, e.g. clearing forest tiles, growing grass&lt;/li&gt;
&lt;li&gt;Adding more resident needs and building adjacency effects&lt;/li&gt;
&lt;li&gt;Adding more buildings&lt;/li&gt;
&lt;li&gt;Solidifying the order in which different resources show up in early game&lt;/li&gt;
&lt;li&gt;Adding adjacency requirements to new buildings, e.g. ensuring a smelter must be built next to a mine&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;section data-footnotes=&quot;&quot;&gt;&lt;div&gt;&lt;h2 id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;&lt;a href=&quot;#footnote-label&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Footnotes”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;em&gt;Admittedly, this is kind of a hack. But it works!&lt;/em&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>godot</category><category>personal project</category></item><item><title>Hexbuilder: Devlog #2</title><link>https://bexedmondson.com/blog/2025-12-23-hexbuilder-devlog-2/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2025-12-23-hexbuilder-devlog-2/</guid><description>Progression in code but not in mechanics

</description><pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;meta name=&quot;astro-view-transitions-enabled&quot; content=&quot;true&quot;&gt;&lt;meta name=&quot;astro-view-transitions-fallback&quot; content=&quot;animate&quot;&gt;
&lt;p&gt;Catch up on the Hexbuilder progress starting &lt;a href=&quot;../2025-11-09-hexbuilder-devlog-0&quot;&gt;here!&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;screenshots&quot;&gt;Screenshots&lt;/h2&gt;&lt;a href=&quot;#screenshots&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Screenshots”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;section id=&quot;gallery-hexbuilder-log2&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.B6Koh_7H_Z26HrCI.webp&quot; srcset=&quot;/_astro/map.B6Koh_7H_Z26HrCI.webp 300w, /_astro/map.B6Koh_7H_Z2h2aiq.webp 600w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;167&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A popup showing details of a smelter, with one worker in it called Bilip. The popup also says that the mine to the north east is adding an extra 1 stone production.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopup.DlXKs3c6_18rnr3.webp&quot; srcset=&quot;/_astro/unlockedpopup.DlXKs3c6_18rnr3.webp 300w, /_astro/unlockedpopup.DlXKs3c6_Z1eLYn5.webp 600w&quot; alt=&quot;A popup showing details of a smelter, with one worker in it called Bilip. The popup also says that the mine to the north east is adding an extra 1 stone production.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;167&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;2&quot; aria-label=&quot;View image 3: A popup showing details of the mill building tile, where mill is selected in a list on the left. It says that a mill by default produces +2 food, and an adjacent field will provide an extra +2 per worker. The mill icon is greyscale, because it&apos;s not unlocked yet.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/encyclopedia.TvF7D2hx_1VXhol.webp&quot; srcset=&quot;/_astro/encyclopedia.TvF7D2hx_1VXhol.webp 300w, /_astro/encyclopedia.TvF7D2hx_Z1WSeDq.webp 600w&quot; alt=&quot;A popup showing details of the mill building tile, where mill is selected in a list on the left. It says that a mill by default produces +2 food, and an adjacent field will provide an extra +2 per worker. The mill icon is greyscale, because it&apos;s not unlocked yet.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;167&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;3&quot; aria-label=&quot;View image 4: A screenshot of a hexagonal grid on a grey background, showing a pit mine, a large sailboat, an orchard, a weaver, and a flax field.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/newbuildings.D4fJ2-b2_15Lhfp.webp&quot; srcset=&quot;/_astro/newbuildings.D4fJ2-b2_15Lhfp.webp 300w, /_astro/newbuildings.D4fJ2-b2_2g6T1z.webp 600w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, showing a pit mine, a large sailboat, an orchard, a weaver, and a flax field.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;167&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.B6Koh_7H_hGRtj.webp&quot; srcset=&quot;/_astro/map.B6Koh_7H_1zuXdU.webp 640w, /_astro/map.B6Koh_7H_NimC5.webp 750w, /_astro/map.B6Koh_7H_Z2ptssV.webp 828w, /_astro/map.B6Koh_7H_1AajTD.webp 1080w, /_astro/map.B6Koh_7H_1XkEgv.webp 1280w, /_astro/map.B6Koh_7H_ZCz6ld.webp 1668w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1927&quot; height=&quot;1070&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Hex Map as of dev log #2 &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedpopup.DlXKs3c6_1LPur3.webp&quot; srcset=&quot;/_astro/unlockedpopup.DlXKs3c6_ZFM3Rw.webp 640w, /_astro/unlockedpopup.DlXKs3c6_Z2nsR0x.webp 750w, /_astro/unlockedpopup.DlXKs3c6_1bh7yS.webp 828w, /_astro/unlockedpopup.DlXKs3c6_Z1t93Hg.webp 1080w, /_astro/unlockedpopup.DlXKs3c6_ZNQeuB.webp 1280w, /_astro/unlockedpopup.DlXKs3c6_ZPBwTz.webp 1668w&quot; alt=&quot;A popup showing details of a smelter, with one worker in it called Bilip. The popup also says that the mine to the north east is adding an extra 1 stone production.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1927&quot; height=&quot;1070&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. Unlocked Popup improvements &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-2&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/encyclopedia.TvF7D2hx_27i7dK.webp&quot; srcset=&quot;/_astro/encyclopedia.TvF7D2hx_DiLKl.webp 640w, /_astro/encyclopedia.TvF7D2hx_NWKCW.webp 750w, /_astro/encyclopedia.TvF7D2hx_ZUzbF7.webp 828w, /_astro/encyclopedia.TvF7D2hx_Z21E220.webp 1080w, /_astro/encyclopedia.TvF7D2hx_ZiiI3L.webp 1280w, /_astro/encyclopedia.TvF7D2hx_Z12Txqp.webp 1668w&quot; alt=&quot;A popup showing details of the mill building tile, where mill is selected in a list on the left. It says that a mill by default produces +2 food, and an adjacent field will provide an extra +2 per worker. The mill icon is greyscale, because it&apos;s not unlocked yet.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1927&quot; height=&quot;1070&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 3. Encyclopedia &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-3&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/newbuildings.D4fJ2-b2_11bKLs.webp&quot; srcset=&quot;/_astro/newbuildings.D4fJ2-b2_ZcSdnA.webp 640w, /_astro/newbuildings.D4fJ2-b2_Z2eeuY.webp 750w, /_astro/newbuildings.D4fJ2-b2_Z1LLbO3.webp 828w, /_astro/newbuildings.D4fJ2-b2_1WqKkD.webp 1080w, /_astro/newbuildings.D4fJ2-b2_Z1op4v4.webp 1280w, /_astro/newbuildings.D4fJ2-b2_Z290SRH.webp 1668w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, showing a pit mine, a large sailboat, an orchard, a weaver, and a flax field.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1927&quot; height=&quot;1070&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 4. Some of the new buildings in action &lt;/p&gt; &lt;p&gt;Clockwise from the left-most: orchard, pit mine, weaver, flax field, and sail boat.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;what-have-i-done-since-the-last-post&quot;&gt;What have I done since the last post?&lt;/h2&gt;&lt;a href=&quot;#what-have-i-done-since-the-last-post&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What have I done since the last post?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I have made a LOT of changes since the last time, but I want to get this post up before the changelist grows so much as to be overwhelming. So here’s the quick-fire round :)&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;3d-modelling&quot;&gt;3D Modelling&lt;/h3&gt;&lt;a href=&quot;#3d-modelling&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “3D Modelling”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I started some very rudimentary modelling in Blender a little bit! Began by merging existing assets to switch up some things, but then I had to get brave and actually build something as there was no existing asset for the weaver building I wanted to make. So I made a loom! Very simple but I think it fits nicely with the art style of everything else, and it communicates what it needs to. Plus I learned a lot for the next time I need to do something like this :)&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/building-weaver.DhwbgRrO_Z2qbFzT.webp&quot; alt=&quot;A top down view of a hex tile with a building and a couple of trees, with a wooden frame loom outside the front.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;480&quot; height=&quot;480&quot;&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;My biggest slowdowns in Blender have all just been knowing where things are. I fully expect to get a lot faster with time, and depending on if I feel like I hit a plateau somewhere, I’ll take a detour and do some tutorials to learn things a bit more “properly”. But my current haphazard picking up of things as I need them is working alright for me at the moment at least!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;new-buildings-and-currencies&quot;&gt;New buildings and currencies&lt;/h3&gt;&lt;a href=&quot;#new-buildings-and-currencies&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “New buildings and currencies”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I built out some upgrade paths a little bit with some new buildings, including some that produce the new currency I added this month: fabric!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;showing-info-everywhere&quot;&gt;Showing info everywhere&lt;/h3&gt;&lt;a href=&quot;#showing-info-everywhere&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Showing info everywhere”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;The unlocked cell popup has been completely overhauled, and now has a ton of information on it. All buildings show the default production rates for the selected building, plus more specific info depending on the kind of building it is. For example, if it’s a workplace, this popup now shows the effects provided to any relevant buildings around it, and any effects received from surrounding buildings as well. Also you can assign and unassign workers from this popup as well, in the same way that you can from the overall workplaces popup - much more convenient for one-off worker assignments!&lt;/p&gt;
&lt;p&gt;I also added an encyclopedia popup - basically a list of all available buildings and what they do. I added this because in order to make progression desirable, you need to know that progression exists! So I thought that I should prioritise showing buildings that you have yet to unlock - and as a nice visual touch, I also wrote a very quick greyscale shader that I’m using to show which buildings are locked.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;tool-improvements-again&quot;&gt;Tool improvements again&lt;/h3&gt;&lt;a href=&quot;#tool-improvements-again&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Tool improvements again”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;As I was introducing more building upgrade paths, I thought it would be useful to be able to visualise them. So now I can! It was very straightforward to add this into the existing placement tool I made, and it’s made things much faster.&lt;/p&gt;
&lt;p&gt;I also added a shortcut to be able to jump to the focussed view of a tile that’s in the upgrade tree. A small but very convenient upgrade!&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/placement-tree.STiO_1CO_1mjVFt.webp&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the house tile to a box for the tent tile, and then three more links from the tent tile to the grass, dirt and forest tiles.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;2256&quot; height=&quot;1131&quot;&gt;
&lt;div&gt;&lt;h3 id=&quot;worker-counts-mean-something-now&quot;&gt;Worker counts mean something now&lt;/h3&gt;&lt;a href=&quot;#worker-counts-mean-something-now&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Worker counts mean something now”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Until this point, having more than one worker at a workplace had no effect on anything. This is still the case, but I’ve now laid the groundwork for allowing production rates and adjacency effects to change based on worker count.&lt;/p&gt;
&lt;p&gt;Adjacency effects are now based on the number of workers in neighbouring workplaces, rather than the number of adjacent workplaces overall. Building production&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; is a bit more complex, and not yet implemented - the idea is to have a baseline production at a workplace for one worker, and then an increase for each subsequent worker. I’m also considering a double increase for the last worker to make maxxing out a workplace feel extra fun, though that might be a bit overpowered - I’ll have to try it and see!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;godot-updates&quot;&gt;Godot Updates!&lt;/h3&gt;&lt;a href=&quot;#godot-updates&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Godot Updates!”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I moved to Godot 4.6 beta 1 for the native Wayland game embedding! No more blurry engine fonts! And the new theme is just icing on the cake - the dark grey is so much nicer on the eyes.&lt;/p&gt;
&lt;p&gt;Obviously being on a beta version comes with a few bumps in the road - e.g. turning on a specific setting in the font import window seems to break my theme at the moment, still narrowing down the full cause for a bug report at the moment - but it’s SO worth it. I’ll likely keep on the latest beta version until 4.6 comes out properly.&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/godot_beforeafter.B-fOzMfB_disdu.webp&quot; alt=&quot;A diagonally split comparison between Godot 4.5 (blue) and 4.6 (dark grey).&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1851&quot; height=&quot;1080&quot;&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h2&gt;&lt;a href=&quot;#whats-next&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What’s next?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;My main focus from a design perspective will be adding some gentle pressure to pursue the progression that I’m building out. Currently it’s pretty easy to find a balance where all your resources are increasing, and there’s not a whole lot of reasons to expand the map and build more if that’s where you are. So changes must be made!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;storage&quot;&gt;Storage&lt;/h3&gt;&lt;a href=&quot;#storage&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Storage”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Firstly I’m thinking of limiting storage based on buildings, which will mean players are incentivised to keep expanding the map so they can build capacity to hold enough resources for building higher-level buildings. There are already some buildings that I have implemented that could do with more purpose, like the lumber pile. Also, adding storage to buildings like houses gives me more parameters that I can use for upgrades, without scaling up resource production really fast.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;resident-happiness&quot;&gt;Resident Happiness&lt;/h3&gt;&lt;a href=&quot;#resident-happiness&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Resident Happiness”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;The other way I’m considering adding pressure is to add a happiness score, continuing the theme of focussing on residents as motivation. I’m thinking that each resident should have an individual happiness level, influenced by…well, I’m not sure yet. Here are some ideas I’ve had so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neighbouring buildings to their residence can provide happiness&lt;/li&gt;
&lt;li&gt;After a certain number of turns, happiness drops if their residence isn’t above specific level&lt;/li&gt;
&lt;li&gt;Residents make timed requests for the player to build specific buildings or produce a certain number of resources, and happiness drops if a request isn’t fulfilled&lt;/li&gt;
&lt;li&gt;If a resident is at minimum happiness, their housemates and colleagues’ happiness starts decreasing as well&lt;/li&gt;
&lt;li&gt;Depending on the number of total tiles unlocked (or some other similar measure of player progression), residents require specific buildings built to remain happy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m still not sure if I’ll implement any of these, but at the very least I’ll be thinking about it. Watch this space!&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;anything-else&quot;&gt;Anything else?&lt;/h3&gt;&lt;a href=&quot;#anything-else&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Anything else?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Improving progression some more! Continuing to add more buildings, particularly improved versions of existing buildings rather than just new buildings.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Making even more information visible: in particular, unlock requirements for future buildings need to be in the encyclopedia.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;section data-footnotes=&quot;&quot;&gt;&lt;div&gt;&lt;h2 id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;&lt;a href=&quot;#footnote-label&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Footnotes”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;em&gt;note: this only applies to buildings with workers! Other buildings’ non-adjacency-bonus production is just flat&lt;/em&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>godot</category><category>personal project</category></item><item><title>Hexbuilder: Devlog #1</title><link>https://bexedmondson.com/blog/2025-11-30-hexbuilder-devlog-1/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2025-11-30-hexbuilder-devlog-1/</guid><description>First proper update on Hexbuilder&apos;s progress!

</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;meta name=&quot;astro-view-transitions-enabled&quot; content=&quot;true&quot;&gt;&lt;meta name=&quot;astro-view-transitions-fallback&quot; content=&quot;animate&quot;&gt;
&lt;p&gt;If you haven’t read &lt;a href=&quot;../2025-11-09-hexbuilder-devlog-0&quot;&gt;devlog #0&lt;/a&gt;, I would strongly recommend reading that first so this post makes sense!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;screenshots&quot;&gt;Screenshots&lt;/h2&gt;&lt;a href=&quot;#screenshots&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Screenshots”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;section id=&quot;gallery-hexbuilder-log1&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot of the Godot game engine, showing a selection list on the left much like in devlog #0 - only this time, the selection list is alphabetised!&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/alphabetised-tree.eVvMObjH_1FXSLu.webp&quot; srcset=&quot;/_astro/alphabetised-tree.eVvMObjH_1FXSLu.webp 300w, /_astro/alphabetised-tree.eVvMObjH_Zsqhm8.webp 600w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left much like in devlog #0 - only this time, the selection list is alphabetised!&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;224&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.nzyFIb14_Z2n8KE7.webp&quot; srcset=&quot;/_astro/map.nzyFIb14_Z2n8KE7.webp 300w, /_astro/map.nzyFIb14_1w0Qjx.webp 600w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;2&quot; aria-label=&quot;View image 3: A gif of a hexagonal grid tile unlocking, with an animation of an hourglass shrinking and then a lock icon shaking and opening before disappearing.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/tile-unlock.DD07Rkd9_20XznV.webp&quot; srcset=&quot;/_astro/tile-unlock.DD07Rkd9_20XznV.webp 300w, /_astro/tile-unlock.DD07Rkd9_2qpb20.webp 600w&quot; alt=&quot;A gif of a hexagonal grid tile unlocking, with an animation of an hourglass shrinking and then a lock icon shaking and opening before disappearing.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;237&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/alphabetised-tree.eVvMObjH_10ujFP.webp&quot; srcset=&quot;/_astro/alphabetised-tree.eVvMObjH_Z1ua157.webp 640w, /_astro/alphabetised-tree.eVvMObjH_o7si6.webp 750w, /_astro/alphabetised-tree.eVvMObjH_ZbU9wn.webp 828w, /_astro/alphabetised-tree.eVvMObjH_Z1dsV4C.webp 1080w, /_astro/alphabetised-tree.eVvMObjH_760IY.webp 1280w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left much like in devlog #0 - only this time, the selection list is alphabetised!&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1516&quot; height=&quot;1133&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Tile Tool - now with an alphabetised selection list! &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/map.nzyFIb14_Z20JxHC.webp&quot; srcset=&quot;/_astro/map.nzyFIb14_ZE8Ieu.webp 640w, /_astro/map.nzyFIb14_Z1VcIq7.webp 750w, /_astro/map.nzyFIb14_mPJXs.webp 828w, /_astro/map.nzyFIb14_1h1Dbm.webp 1080w, /_astro/map.nzyFIb14_20NQmw.webp 1280w, /_astro/map.nzyFIb14_Z1Rc5qX.webp 1668w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt, with some buildings and UI over the top. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1894&quot; height=&quot;1065&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. Hex Map as of dev log #1 &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-2&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/tile-unlock.DD07Rkd9_ZN2Eqz.webp&quot; srcset=&quot;/_astro/tile-unlock.DD07Rkd9_ZSCyWR.webp 640w, /_astro/tile-unlock.DD07Rkd9_Zk7bOR.webp 750w, /_astro/tile-unlock.DD07Rkd9_w7miC.webp 828w&quot; alt=&quot;A gif of a hexagonal grid tile unlocking, with an animation of an hourglass shrinking and then a lock icon shaking and opening before disappearing.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1024&quot; height=&quot;808&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 3. Tile Unlock animation &lt;/p&gt; &lt;p&gt;Unlocking a tile on a new turn was a bit anticlimactic and quite hard to notice, so I thought I&amp;#39;d add a little bit of pizzazz to it :)&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;finding-some-direction&quot;&gt;Finding some direction&lt;/h2&gt;&lt;a href=&quot;#finding-some-direction&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Finding some direction”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Since my last post, I have made some pretty big steps forward in determining player motivations in Hexbuilder. I had a very helpful conversation with my sister (thanks Beth, if you’re reading!), and as a result decided that since cities are made of people, my city builder needed more emphasis on its residents. Before, I had people set up as one of the resources/currencies that could be produced and spent by buildings, and since then I’ve completely removed that, and now individual residents are individual entities.&lt;/p&gt;
&lt;p&gt;Now, I have a set of manager classes that handle residents and what they each do. Residents are created and assigned to housing and workplaces by the relevant manager class, and the presence of residents directly determines workplace production at the end of a turn. Currently, workplaces must have at least one worker assigned to them, though I’m considering revisiting that later - for now, if a workplace has no workers then I’ve added a little ”!” over the workplace’s info UI. Also, adding more workers doesn’t do anything yet, but that definitely will change in future.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;timed-jobs&quot;&gt;Timed Jobs&lt;/h4&gt;&lt;a href=&quot;#timed-jobs&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Timed Jobs”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I also created a manager class for what I’m calling “timed jobs” - something that means a resident can’t be working anywhere else that turn, but only exists for a set number of turns and frees up the residents afterwards automatically. Currently, the only timed job is the process of unlocking a new tile, but I could see this expanding to cover things like foraging on a tile or repairing a building. One thing I don’t love is how similar a lot of the code is between the WorkplaceManager and TimedJobManager, but I’m not going to refactor thise to make a shared base class unless I work on something else that needs it - there’s enough to do elsewhere!&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;names&quot;&gt;Names&lt;/h4&gt;&lt;a href=&quot;#names&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Names”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;One last thing I did was to give the residents silly names, randomly picked from a list that I wrote one evening. This was partly so each resident was easier to keep track of when debugging, partly because I was procrastinating, and partly because making up silly names is extremely fun and surprisingly hard to stop once you’ve started!&lt;/p&gt;
&lt;img src=&quot;https://bexedmondson.com/_astro/resident-names.Cood_NcF_1XYiaW.webp&quot; alt=&quot;A list of silly names in JSON format, alphabetised. Visible is only names between G and J - line numbers are in the 60s!&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;520&quot; height=&quot;373&quot;&gt;
&lt;div&gt;&lt;h2 id=&quot;ui-improvements&quot;&gt;UI improvements&lt;/h2&gt;&lt;a href=&quot;#ui-improvements&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “UI improvements”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;One part of building out all of the above was adding in a few info screens. These did double duty first as helpful debug tools for myself, but also will serve as a way to get information to the player.&lt;/p&gt;
&lt;p&gt;The UI is still very much placeholder-filled, but I brought in a free theme to make everything a bit easier to read and navigate: &lt;a href=&quot;https://github.com/wadlo/Themey&quot;&gt;Clashy, by Wadlo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also set up a way to add UI to show about cells over the top of those cells, and added those for about workplaces and timed jobs.&lt;/p&gt;
&lt;section id=&quot;gallery-hexbuilder-log1_popups&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot showing a popup with a list of names, all of which are silly portmanteaus of other names like Kimothy and Ronathan&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-residents.CgEfF47Y_N0YAj.webp&quot; srcset=&quot;/_astro/popup-residents.CgEfF47Y_N0YAj.webp 300w, /_astro/popup-residents.CgEfF47Y_1ToMYX.webp 600w&quot; alt=&quot;A screenshot showing a popup with a list of names, all of which are silly portmanteaus of other names like Kimothy and Ronathan&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A screenshot showing a popup with a list of coordinates, with names by each house and an indication of how many can fit in each. There&apos;s also an arrow to the right of each house&apos;s display.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-housing.fUDRqraK_Z20UmG9.webp&quot; srcset=&quot;/_astro/popup-housing.fUDRqraK_Z20UmG9.webp 300w, /_astro/popup-housing.fUDRqraK_BUR1R.webp 600w&quot; alt=&quot;A screenshot showing a popup with a list of coordinates, with names by each house and an indication of how many can fit in each. There&apos;s also an arrow to the right of each house&apos;s display.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;2&quot; aria-label=&quot;View image 3: A screenshot showing a popup with a list of workplaces like woodcutter and market, with names by each workplace and a plus and minus button that allows assigning and unassiging workers&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-workplaces.CUkogsTc_1Bd7rB.webp&quot; srcset=&quot;/_astro/popup-workplaces.CUkogsTc_1Bd7rB.webp 300w, /_astro/popup-workplaces.CUkogsTc_2iMN0E.webp 600w&quot; alt=&quot;A screenshot showing a popup with a list of workplaces like woodcutter and market, with names by each workplace and a plus and minus button that allows assigning and unassiging workers&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;169&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-residents.CgEfF47Y_1DAGfU.webp&quot; srcset=&quot;/_astro/popup-residents.CgEfF47Y_JICyx.webp 640w, /_astro/popup-residents.CgEfF47Y_1uIG02.webp 750w, /_astro/popup-residents.CgEfF47Y_RaGBd.webp 828w, /_astro/popup-residents.CgEfF47Y_Z1bk8c3.webp 1080w, /_astro/popup-residents.CgEfF47Y_1ScdEb.webp 1280w, /_astro/popup-residents.CgEfF47Y_2begud.webp 1668w&quot; alt=&quot;A screenshot showing a popup with a list of names, all of which are silly portmanteaus of other names like Kimothy and Ronathan&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1894&quot; height=&quot;1065&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Residents Popup &lt;/p&gt; &lt;p&gt;I spent way too long making up all those names. I have like 140 of them now, I just couldn&amp;#39;t stop myself from coming up with more haha&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-housing.fUDRqraK_Z1suKLI.webp&quot; srcset=&quot;/_astro/popup-housing.fUDRqraK_qJXuL.webp 640w, /_astro/popup-housing.fUDRqraK_1f6XDk.webp 750w, /_astro/popup-housing.fUDRqraK_Z26iG6S.webp 828w, /_astro/popup-housing.fUDRqraK_ZcUxpG.webp 1080w, /_astro/popup-housing.fUDRqraK_ZESGCQ.webp 1280w, /_astro/popup-housing.fUDRqraK_Z1hwwqr.webp 1668w&quot; alt=&quot;A screenshot showing a popup with a list of coordinates, with names by each house and an indication of how many can fit in each. There&apos;s also an arrow to the right of each house&apos;s display.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1894&quot; height=&quot;1065&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. Housing Popup &lt;/p&gt; &lt;p&gt;The arrow on the right closes the popup and centers the camera on the residence in question. House icons are TBD&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-2&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/popup-workplaces.CUkogsTc_Zv4wkG.webp&quot; srcset=&quot;/_astro/popup-workplaces.CUkogsTc_Z24Q6uk.webp 640w, /_astro/popup-workplaces.CUkogsTc_Z14q1QH.webp 750w, /_astro/popup-workplaces.CUkogsTc_1roSyN.webp 828w, /_astro/popup-workplaces.CUkogsTc_2drLyG.webp 1080w, /_astro/popup-workplaces.CUkogsTc_Z17qe94.webp 1280w, /_astro/popup-workplaces.CUkogsTc_FmCXc.webp 1668w&quot; alt=&quot;A screenshot showing a popup with a list of workplaces like woodcutter and market, with names by each workplace and a plus and minus button that allows assigning and unassiging workers&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1894&quot; height=&quot;1065&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 3. Workplaces Popup &lt;/p&gt;  &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;what-else-did-i-do&quot;&gt;What else did I do?&lt;/h2&gt;&lt;a href=&quot;#what-else-did-i-do&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What else did I do?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Set up better resource production values for the buildings that I’ve been testing with.&lt;/li&gt;
&lt;li&gt;Added an event dispatcher class that I had from another project, allowing me to dispatch and listen to events.&lt;/li&gt;
&lt;li&gt;Set up keyboard shortcuts for accepting options in specific popups and closing popups.&lt;/li&gt;
&lt;li&gt;Added a common base class to popup classes to handle the above in one common location.&lt;/li&gt;
&lt;li&gt;Refactoring lots of things, notably the cell unlocking to allow cell unlocking via timed jobs (as mentioned above).&lt;/li&gt;
&lt;li&gt;Made some usability improvements to the tiles tool, mainly alphabetising building names in the places they’re listed.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside aria-label=&quot;Note&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Note&lt;/p&gt;&lt;div&gt;&lt;p&gt;I encountered a fun little quirk of some of Godot’s APIs in the process: you seemingly can’t reorder the TreeItems in a Tree after they’re made, nor the items in an OptionButton dropdown! Which was irritating but not impossible to work around, thankfully.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;ul&gt;
&lt;li&gt;Had a couple of tries at fixing a confusing null ref that’s appearing in the tiles tool code after Godot rebuilds the project. I haven’t yet gotten rid of it fully because I’m not sure what’s happening, it’s something to do with the data associated with a graph node being re-assigned after build. But I think I did at least reduce the frequency that it appears, so depending on how annoying the effects of this error are, I might leave it as-is.&lt;/li&gt;
&lt;li&gt;And as a little bonus to myself, I tweaked the locked tile overlay to remove the irritating gaps that showed when the texture was tiled.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;next-steps&quot;&gt;Next steps&lt;/h2&gt;&lt;a href=&quot;#next-steps&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Next steps”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;I need to upgrade the unlocked cell popup to be more specialised and show more info for workplaces and buildings. I also want to show the effects that a building has on surrounding tiles, and to do that I’ll need to center the building in the screen while this popup is open.&lt;/li&gt;
&lt;li&gt;I want to separate out the concept of terrain from the concept of buildings. I partly set it up like this for simplicity, because all the models have terrain embedded in them, but I think I need to separate them in order to have more flexibility when it comes to building out the building tech trees.&lt;/li&gt;
&lt;li&gt;Talking of which, I want to add more to the upgrade paths for some of the buildings, and visualise that somewhere in the UI. I think I might also need to add a tool for this, though I may be able to deal with things as they are.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>godot</category><category>personal project</category></item><item><title>Hexbuilder: Devlog #0</title><link>https://bexedmondson.com/blog/2025-11-09-hexbuilder-devlog-0/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2025-11-09-hexbuilder-devlog-0/</guid><description>An introduction to my ongoing project Hexbuilder, laying the groundwork for future devlogs.

</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;meta name=&quot;astro-view-transitions-enabled&quot; content=&quot;true&quot;&gt;&lt;meta name=&quot;astro-view-transitions-fallback&quot; content=&quot;animate&quot;&gt;
&lt;p&gt;Hexbuilder is the name&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref=&quot;&quot; aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; of a personal project I’ve been working on here-and-there for the last few months. As such, I’ve already done a decent chunk of work on it, but I decided that for accountability, I should start posting about it publicly! So this post is to summarise what I’ve done so far and give context for future updates.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-is&quot;&gt;What it is&lt;/h2&gt;&lt;a href=&quot;#what-it-is&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What it is”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Hexbuilder is a turn-based, city builder, resource management game. As the name implies, the map is laid out on a hex grid. The central mechanic is based on building specific things on tiles: each tile you can build will consume and provide different resources when each turn ends. Tiles also have effects on neighbouring tiles, e.g. a smelter next to a mine will produce one more stone per turn.&lt;/p&gt;
&lt;p&gt;Unlocking new terrain tiles to build on also costs resources. Additionally, there are restrictions on which types of tiles can be built on which types of terrains, e.g. a mine can only be built on a hill or a mountain.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-ive-done-so-far&quot;&gt;What I’ve done so far&lt;/h2&gt;&lt;a href=&quot;#what-ive-done-so-far&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What I’ve done so far”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Created the first draft of the hex map&lt;/li&gt;
&lt;li&gt;Implemented tile purchasing and tile placement&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../2025-09-10-batch-rendering-blender&quot;&gt;Processed the 3D tile assets I’m currently using into 2D assets&lt;/a&gt; for use in Godot’s tile map system&lt;/li&gt;
&lt;li&gt;Set up a data structure to hold game config data for each tile&lt;/li&gt;
&lt;li&gt;Implemented turns, including resource quantity changes based on existing tiles&lt;/li&gt;
&lt;li&gt;Implemented tile adjacency effects&lt;/li&gt;
&lt;li&gt;Built a tool to more easily view and change which tiles can be placed on which terrain types&lt;/li&gt;
&lt;li&gt;Expanded the tiles tool to view and change tiles’ adjacency effects on other tiles&lt;/li&gt;
&lt;li&gt;Quick and dirty first pass at camera movement&lt;/li&gt;
&lt;li&gt;Ultra basic placeholder HUD with a next turn button and currency display&lt;/li&gt;
&lt;li&gt;Even more basic placeholder popups for unlocking a tile and building a tile&lt;/li&gt;
&lt;li&gt;Noise-based terrain generation&lt;/li&gt;
&lt;li&gt;First draft at river detection in generated terrain&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;screenshots&quot;&gt;Screenshots&lt;/h2&gt;&lt;a href=&quot;#screenshots&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Screenshots”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;section id=&quot;gallery-hexbuilder-log0&quot;&gt; &lt;button data-index=&quot;0&quot; aria-label=&quot;View image 1: A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to two boxes for dirt and rock tiles. The tab is called placement, indicating that the smelter is restricted to just these tiles.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/placementtool.CNl8UFp4_2119iY.webp&quot; srcset=&quot;/_astro/placementtool.CNl8UFp4_2119iY.webp 300w, /_astro/placementtool.CNl8UFp4_ZU9FMT.webp 600w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to two boxes for dirt and rock tiles. The tab is called placement, indicating that the smelter is restricted to just these tiles.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;150&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;1&quot; aria-label=&quot;View image 2: A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to a box for a mine tiles. The tab is called adjacency, and the info in the mine tile indicates that placing a mine by a smelter will increase the smelter&apos;s stone yield per turn by 1.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/adjacencytool.DRuIdkid_Z2be4sV.webp&quot; srcset=&quot;/_astro/adjacencytool.DRuIdkid_Z2be4sV.webp 300w, /_astro/adjacencytool.DRuIdkid_Z2cKKS.webp 600w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to a box for a mine tiles. The tab is called adjacency, and the info in the mine tile indicates that placing a mine by a smelter will increase the smelter&apos;s stone yield per turn by 1.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;150&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;2&quot; aria-label=&quot;View image 3: A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt. The outermost tiles are greyed out.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedtiles.D_6L8gR2_Z7kUGp.webp&quot; srcset=&quot;/_astro/unlockedtiles.D_6L8gR2_Z7kUGp.webp 300w, /_astro/unlockedtiles.D_6L8gR2_suK2.webp 600w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;168&quot;&gt; &lt;/button&gt;&lt;button data-index=&quot;3&quot; aria-label=&quot;View image 4: A screenshot of a Godot inspector, displaying several settings related to thresholds and noise values for terrain generation.&quot;&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/terrainnoisesettings.DD_ZBSym_1Dnniw.webp&quot; srcset=&quot;/_astro/terrainnoisesettings.DD_ZBSym_1Dnniw.webp 300w, /_astro/terrainnoisesettings.DD_ZBSym_SAtce.webp 600w&quot; alt=&quot;A screenshot of a Godot inspector, displaying several settings related to thresholds and noise values for terrain generation.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;(min-width: 300px) 300px, 100vw&quot; data-astro-image=&quot;constrained&quot; width=&quot;300&quot; height=&quot;259&quot;&gt; &lt;/button&gt; &lt;dialog data-option=&quot;fade&quot;&gt; &lt;div&gt; &lt;figure id=&quot;image-0&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/placementtool.CNl8UFp4_ZrDdQU.webp&quot; srcset=&quot;/_astro/placementtool.CNl8UFp4_Zm9Kil.webp 640w, /_astro/placementtool.CNl8UFp4_pDdzE.webp 750w, /_astro/placementtool.CNl8UFp4_JDn1W.webp 828w, /_astro/placementtool.CNl8UFp4_ZRddge.webp 1080w, /_astro/placementtool.CNl8UFp4_ZTM693.webp 1280w, /_astro/placementtool.CNl8UFp4_1etrVc.webp 1668w, /_astro/placementtool.CNl8UFp4_ZObeg1.webp 2048w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to two boxes for dirt and rock tiles. The tab is called placement, indicating that the smelter is restricted to just these tiles.&quot; loading=&quot;eager&quot; data-index=&quot;0&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;2256&quot; height=&quot;1131&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 1. Tile Placement Tool &lt;/p&gt; &lt;p&gt;This tool allows me to more easily see and change which tiles can be placed on top of which other tiles, as well as change other tile data.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-1&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/adjacencytool.DRuIdkid_1wMw3U.webp&quot; srcset=&quot;/_astro/adjacencytool.DRuIdkid_vM9IF.webp 640w, /_astro/adjacencytool.DRuIdkid_1iA8BF.webp 750w, /_astro/adjacencytool.DRuIdkid_1CAi3X.webp 828w, /_astro/adjacencytool.DRuIdkid_17dwEB.webp 1080w, /_astro/adjacencytool.DRuIdkid_14DDLM.webp 1280w, /_astro/adjacencytool.DRuIdkid_Z1QgUVT.webp 1668w, /_astro/adjacencytool.DRuIdkid_1afvEO.webp 2048w&quot; alt=&quot;A screenshot of the Godot game engine, showing a selection list on the left and a diagram on the right linking a box with details about the smelter tile to a box for a mine tiles. The tab is called adjacency, and the info in the mine tile indicates that placing a mine by a smelter will increase the smelter&apos;s stone yield per turn by 1.&quot; loading=&quot;eager&quot; data-index=&quot;1&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;2256&quot; height=&quot;1131&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 2. Tile Adjacency Tool &lt;/p&gt; &lt;p&gt;This tool allows me to see and change which tiles have effects on others when placed next to them, and what those effects are.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-2&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/unlockedtiles.D_6L8gR2_Z22VtuG.webp&quot; srcset=&quot;/_astro/unlockedtiles.D_6L8gR2_Z20FThL.webp 640w, /_astro/unlockedtiles.D_6L8gR2_CDBmu.webp 750w, /_astro/unlockedtiles.D_6L8gR2_2mq5qd.webp 828w, /_astro/unlockedtiles.D_6L8gR2_15rHi8.webp 1080w, /_astro/unlockedtiles.D_6L8gR2_Z1P0Jan.webp 1280w, /_astro/unlockedtiles.D_6L8gR2_26a7NL.webp 1668w&quot; alt=&quot;A screenshot of a hexagonal grid on a grey background, with several types of tiles unlocked: forest, grass, river, sand, and dirt. The outermost tiles are greyed out.&quot; loading=&quot;eager&quot; data-index=&quot;2&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;1891&quot; height=&quot;1059&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 3. Hex Map &lt;/p&gt; &lt;p&gt;This is an example of the map as it currently looks. When a tile is unlocked, any adjacent invisible tiles will be revealed.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt;&lt;figure id=&quot;image-3&quot;&gt;  &lt;div&gt; &lt;img src=&quot;https://bexedmondson.com/_astro/terrainnoisesettings.DD_ZBSym_Z1Ot9UL.webp&quot; srcset=&quot;/_astro/terrainnoisesettings.DD_ZBSym_2jyhaG.webp 640w, /_astro/terrainnoisesettings.DD_ZBSym_Z1Hytvj.webp 750w, /_astro/terrainnoisesettings.DD_ZBSym_xR2mn.webp 828w&quot; alt=&quot;A screenshot of a Godot inspector, displaying several settings related to thresholds and noise values for terrain generation.&quot; loading=&quot;eager&quot; data-index=&quot;3&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; sizes=&quot;100vw&quot; data-astro-image=&quot;full-width&quot; width=&quot;906&quot; height=&quot;783&quot;&gt; &lt;/div&gt; &lt;figcaption&gt; &lt;p&gt; 4. Terrain Generation Settings &lt;/p&gt; &lt;p&gt;These settings contain references to each of the terrain tiles, threshold levels for easy adjustment, and noise textures for different values.&lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt; &lt;/div&gt;&lt;!-- Navigation buttons --&gt; &lt;button aria-label=&quot;previous slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m8 15H11.85l5.58 5.573L16 24l-8-8l8-8l1.43 1.393L11.85 15H24Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l1.43 1.393L11.85 15H24v2H11.85l5.58 5.573L16 24l-8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;next slide&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M2 16A14 14 0 1 0 16 2A14 14 0 0 0 2 16m6-1h12.15l-5.58-5.607L16 8l8 8l-8 8l-1.43-1.427L20.15 17H8Z&quot;&gt;&lt;/path&gt; &lt;path fill=&quot;none&quot; d=&quot;m16 8l-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;button aria-label=&quot;close button&quot;&gt; &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot;&gt; &lt;path fill=&quot;currentColor&quot; d=&quot;M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14s14-6.2 14-14S23.8 2 16 2m5.4 21L16 17.6L10.6 23L9 21.4l5.4-5.4L9 10.6L10.6 9l5.4 5.4L21.4 9l1.6 1.6l-5.4 5.4l5.4 5.4z&quot;&gt;&lt;/path&gt; &lt;/svg&gt; &lt;/button&gt; &lt;/dialog&gt; &lt;/section&gt;  
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;&lt;a href=&quot;#whats-next&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What’s next”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;The main problem I need to solve is…well, fundamentally that I’m not a game designer! Developing games - well within my skill set. But designing is a whole other matter. I’ve got the mechanics, and I have them functioning to a reasonable degree, but there are bigger questions that I need to figure out. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what is success?&lt;/li&gt;
&lt;li&gt;what is failure?&lt;/li&gt;
&lt;li&gt;is there even a fail state at all?&lt;/li&gt;
&lt;li&gt;what is progress?&lt;/li&gt;
&lt;li&gt;what is the goal?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…and so on.&lt;/p&gt;
&lt;p&gt;Such nebulous, open-ended questions! Ordinarily, my job is based on having the end goal provided to me, and then figuring out the best way to make that happen, balancing considerations like performance, architecture, speed, and whatever else. Finding the answers to questions like the above is such a different way of thinking - I need to decide the end goal, and how players should reach it. This seems so difficult! I have no idea how game designers do this every day!&lt;/p&gt;
&lt;p&gt;To be honest, I’ve gotten to this point with previous projects quite a lot, where I have the central mechanic but none of the bigger progression questions answered, and in the past I’ve just moved on to something else. But I’m more determined this time around, so I hope to post devlog #1 with at least some of the above questions answered.&lt;/p&gt;
&lt;p&gt;My very early thoughts are around including some kind of tech-tree style system to answer some questions around progression. I think I could use the tile placement settings to make buildings upgradable, and then separately I would need to determine how buildings get unlocked.&lt;/p&gt;
&lt;p&gt;I also would like to modify the tiles to be more flexible. Currently, the building tiles contain the terrain, which means that I’m quite limited in determining which buildings can go on which terrain, and I think this might end up limiting me a fair amount when it comes to the tech tree concept as well. What I have currently is workable, but I may start looking into this so that I have some room to play around and see what feels fun.&lt;/p&gt;
&lt;p&gt;I have some other things that I know I’ll need to do eventually (like making buildings destroyable), but I don’t want to get bogged down in minor improvements when such big questions are looming over me. I’m doing my best to stay focussed on solving the big questions first - stay tuned to see if I manage that!&lt;/p&gt;
&lt;hr&gt;
&lt;section data-footnotes=&quot;&quot;&gt;&lt;div&gt;&lt;h2 id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;&lt;a href=&quot;#footnote-label&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Footnotes”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;em&gt;or at least, the placeholder name until I think of something better&lt;/em&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>godot</category><category>personal project</category></item><item><title>Batch Rendering Models with Blender</title><link>https://bexedmondson.com/blog/2025-09-10-batch-rendering-blender/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2025-09-10-batch-rendering-blender/</guid><description>How to create renders of multiple models using scripting in Blender 4.0.

</description><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this article, I’ll explain how to use Blender 4.0 to create renders of multiple models using Blender scripting. I’ll be using &lt;a href=&quot;https://kenney.nl/assets/hexagon-kit&quot;&gt;Kenney’s Hexagon Kit&lt;/a&gt; as the 3D assets, and rendering one image per model with one click of a button.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;setting-up&quot;&gt;Setting up&lt;/h2&gt;&lt;a href=&quot;#setting-up&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Setting up”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ol role=&quot;list&quot;&gt;
&lt;li&gt;
&lt;p&gt;Start by opening a new Blender project and importing all the models you want to render. In my case, I imported the GLB models.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Move all the models into one Collection so we can find them easily later. Rename the collection to something memorable - you will use this name later.&lt;/p&gt;
&lt;aside aria-label=&quot;Note&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Note&lt;/p&gt;&lt;div&gt;&lt;p&gt;If you’re using GLB models as I am in the example, your models might appear grey at this point. Don’t worry - they will still appear correctly in the final render!&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At this point, you can set up any lighting and reposition any of the models.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up the camera.&lt;/p&gt;
&lt;ol role=&quot;list&quot;&gt;
&lt;li&gt;
&lt;p&gt;Position: The easiest way I found was to position the editor view in the correct position, and then use View &gt; Align View &gt; Align Active Camera To View.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Render Size: Select any object in the scene. Select the Output tab in the Properties area, and change the dimensions of the output images here.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bexedmondson.com/_astro/batch-rendering-blender-output-size.DUXL6Mte_Z1JfUwi.webp&quot; alt=&quot;A screenshot of Blender&amp;#x27;s Properties panel with the Output tab selected. Under &amp;#x27;Format&amp;#x27;, the tab contains options labelled &amp;#x27;Resolution X&amp;#x27; and &amp;#x27;Y&amp;#x27;, which are the relevant options to change here&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;815&quot; height=&quot;309&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;By default, Blender won’t render images with transparent backgrounds. To set the background as transparent, select the Render tab in the Properties area, expand the ‘Film’ dropdown and check the ‘Transparent’ box.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check the results of the render using this menu option:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bexedmondson.com/_astro/batch-rendering-blender-render-menu-option.dxZa2Lh3_1b5X2H.webp&quot; alt=&quot;A screenshot of Blender&amp;#x27;s Render menu, with the Render Image option highlighted&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;717&quot; height=&quot;148&quot;&gt;&lt;/p&gt;
&lt;p&gt;You may want to only render one object when testing the final result. Toggle off other objects using the camera icon in the Outliner area:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bexedmondson.com/_astro/batch-rendering-blender-toggle-render.DVO2cfIe_2kBTGg.webp&quot; alt=&quot;A screenshot of Blender&amp;#x27;s Outliner area, showing two objects. There is a camera icon to the right of each object&amp;#x27;s name, highlighted with a pink outline to indicate that these icons are relevant to this step: one icon is active, and one is inactive - shown with a cross in the camera icon.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;815&quot; height=&quot;138&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once you’re happy with the render result, switch to the Scripting layout using the top bar and make a new script. Make sure you save your new file with the .py extension: Blender will not do this for you automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;scripting&quot;&gt;Scripting&lt;/h2&gt;&lt;a href=&quot;#scripting&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Scripting”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;aside aria-label=&quot;Read before you run this script&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Read before you run this script&lt;/p&gt;&lt;div&gt;&lt;ul&gt;
&lt;li&gt;This script runs synchronously, which means that Blender’s UI will freeze until the render is done. If your models are large or the renders are particularly intensive, this may take some time, and is not exitable without closing Blender.&lt;/li&gt;
&lt;li&gt;If you run this code multiple times, you may overwrite previous renders. There is no undo function for this, so be aware of any possible data loss.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h3 id=&quot;full-script&quot;&gt;Full Script&lt;/h3&gt;&lt;a href=&quot;#full-script&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Full Script”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;batch_render_models.py&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; bpy, os&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;show&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.hide_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; show&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; child &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.children:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;child.hide_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; show&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;dir_path &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/full/path/to/desired/output/directory&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;objects_to_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bpy.data.collections&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;YourCollectionNameHere&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.all_objects&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; other &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; other:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.context.scene.render.filepath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; os.path.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.name &lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.ops.render.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;animation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write_still&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;breakdown&quot;&gt;Breakdown&lt;/h3&gt;&lt;a href=&quot;#breakdown&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Breakdown”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; bpy, os&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Importing Blender’s Python module, as well as the &lt;code dir=&quot;auto&quot;&gt;os&lt;/code&gt; module to use in file path manipulation&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;show&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.hide_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; show&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; child &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.children:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;child.hide_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; show&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Defining a function with two parameters: an object in Blender, and a bool value indicating whether it should be rendered&lt;/li&gt;
&lt;li&gt;Setting the object’s &lt;code dir=&quot;auto&quot;&gt;hide_render&lt;/code&gt; value to the inverse of the &lt;code dir=&quot;auto&quot;&gt;show&lt;/code&gt; argument&lt;/li&gt;
&lt;li&gt;Iterating through the object’s immediate children and setting their &lt;code dir=&quot;auto&quot;&gt;hide_render&lt;/code&gt; values to the same; if your models have more complex structures, you may want to modify this to iterate through more than one level of children&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;objects_to_render &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bpy.data.collections&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;YourCollectionNameHere&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.all_objects&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Using Blender’s API to access all objects in the collection created during the setup, and putting those objects into a list&lt;/li&gt;
&lt;li&gt;Iterating over the list; then, for each object in the list:&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; other &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; other:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.context.scene.render.filepath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; os.path.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.name &lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.ops.render.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;animation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write_still&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;First, enabling rendering for this object using the function defined above&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; other &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; other:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.context.scene.render.filepath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; os.path.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.name &lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.ops.render.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;animation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write_still&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Turning off rendering for all objects except the one we’re currently looking at. In detail:
&lt;ul&gt;
&lt;li&gt;Iterating over the list of objects again&lt;/li&gt;
&lt;li&gt;Doing nothing if we find the object we’re looking at in the outer loop&lt;/li&gt;
&lt;li&gt;Otherwise, preventing the object from rendering with the function defined above&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; other &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; other:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.context.scene.render.filepath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; os.path.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.name &lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.ops.render.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;animation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write_still&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Setting Blender’s internal render filepath value to the target directory plus the model’s name and PNG extension&lt;/li&gt;
&lt;li&gt;Note that (unintuitively) you don’t pass an output path into the render function, the output path is set here completely separately&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; other &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; objects_to_render:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; other:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set_render_self_and_children&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.context.scene.render.filepath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; os.path.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir_path&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.name &lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div aria-hidden=&quot;true&quot;&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bpy.ops.render.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;animation&lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;write_still&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Calling Blender’s render function&lt;/li&gt;
&lt;li&gt;Ensuring that the rendering process doesn’t pull data from the animation ranges in the scene by setting &lt;code dir=&quot;auto&quot;&gt;animation&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;False&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Setting &lt;code dir=&quot;auto&quot;&gt;write_still&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;True&lt;/code&gt; so that the render writes to an external file at the path set earlier&lt;/li&gt;
&lt;/ul&gt;
&lt;aside aria-label=&quot;Caution&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Caution&lt;/p&gt;&lt;div&gt;&lt;p&gt;The first parameter in this function is actually a positional parameter, which accepts a string value. &lt;strong&gt;Do not set this value unless you know what you’re doing!&lt;/strong&gt; It will cause the render to run asynchronously, which means that Blender’s internal file path value will be overwritten when we set it in later iterations. For me, this caused only the last image to actually be output at the end. I believe that each render did happen, but just wrote it to the same path.&lt;/p&gt;&lt;p&gt;Running this code synchronously is the reason that Blender’s UI will freeze until the render is done. As mentioned above, if your models are large or the renders are particularly intensive, this may take some time, and is not exitable without closing Blender.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;example-results&quot;&gt;Example Results&lt;/h2&gt;&lt;a href=&quot;#example-results&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Example Results”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt; &lt;/p&gt;
&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://bexedmondson.com/_astro/batch-rendering-blender-models-before.DY4BhzX__dvaip.webp&quot; alt=&quot;A screenshot from Blender&apos;s 3D viewport showing a lot of grey models all in the same place and merged together&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;706&quot; height=&quot;699&quot;&gt;&lt;/p&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Before&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bexedmondson.com/_astro/batch-rendering-blender-renders-after.BESiG6oY_ZLQeyw.webp&quot; alt=&quot;A screenshot of a grid of files with thumbnails, each thumbnail showing a different render of a textured model, and each file named according to the name of the model&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1893&quot; height=&quot;1143&quot;&gt;
&lt;p&gt;&lt;em&gt;After&lt;/em&gt;&lt;/p&gt;&lt;/p&gt;</content:encoded><category>blender</category></item><item><title>Making, Breaking, and Uncomplicating Analytic Events</title><link>https://bexedmondson.com/blog/2025-08-10-analytics-validator/</link><guid isPermaLink="true">https://bexedmondson.com/blog/2025-08-10-analytics-validator/</guid><description>Or: how I built a system that prevented almost all our analytics bugs, how I came up with the idea in the first place, and the key things I learned along the way.

</description><pubDate>Sun, 10 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Or: how I built a system that prevented almost all our analytics bugs, how I came up with the idea in the first place, and the key things I learned along the way.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;context&quot;&gt;Context&lt;/h2&gt;&lt;a href=&quot;#context&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Context”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Let me set the stage for the context of this story. When I began working at Trailmix, the game that would become Love and Pies was still over a year from release. As such, the team was small and our playtests even smaller, which meant that our player data gathering at the time consisted of watching over another teammate’s shoulder.&lt;/p&gt;
&lt;p&gt;A few months after I joined, Trailmix hired their first data team member, who was given the gargantuan task of building out the whole data pipeline from scratch. They began setting up a system where the game build would send analytics to a &lt;a href=&quot;https://playfab.com/analytics/&quot;&gt;Playfab&lt;/a&gt; server, and then that data would later be injested into &lt;a href=&quot;https://cloud.google.com/bigquery&quot;&gt;Google BigQuery&lt;/a&gt; for analysis.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;What are analytics and why are they important?&lt;/summary&gt;
&lt;p&gt;In games, analytic events consist of pieces of data about a player and what they&apos;re doing. Analytic events are important in free-to=play games because it&apos;s almost impossible to make a game sustainable without at least knowing where your players are spending money, which players are spending money, and where they&apos;re dropping out of the game completely.&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;Due to the obvious capacity constraints on the data team side and a similar lack of time on the dev team side, devs began setting up analytics events for our features in a pretty ad-hoc manner, usually without much forethought, consistency or testing. This is a pretty normal situation for small teams who are working fast, but in turned out that Love and Pies had longevity! So these hacky analytic event implementations made it into production. And then stayed there. For quite a long time.&lt;/p&gt;
&lt;p&gt;And as is often the case with hacky implementations, it was only a matter of time before something went wrong.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-went-wrong&quot;&gt;What went wrong&lt;/h2&gt;&lt;a href=&quot;#what-went-wrong&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What went wrong”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;One Monday morning about a year after Love and Pies’ global release, a marketing team member came over to the devs in quite a frantic state. They told us that the game hadn’t been sending any analytics events about monetisation or attribution for at least three days. This was quite terrifying to hear, because four days ago we had released an update, and during those four days we had been featured by the Google Play store. This meant two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We were missing crucial data for a huge number of new players.&lt;/li&gt;
&lt;li&gt;We had broken something in analytics, even though we didn’t think we had changed anything to do with it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We began digging straight away, and before long a teammate discovered that they’d caused the type of bug that is every dev’s nightmare: they had refactored a system that they thought was totally unrelated to analytics, and through a series of non-obvious dependencies that routed through some historical hacks, they had actually completely broken the monetisation and attribution analytics system.&lt;/p&gt;
&lt;p&gt;They of course immediately began working on a fix, our QA team dropped what they were doing to start testing, and before the end of the day we had submitted a hotfix that solved the issue. But the end result was that we permanently lost the earliest, most important monetisation data for all the new players that came in from the Play store featuring, and we had to delay the next release by more than a week.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;problem-solved&quot;&gt;Problem solved?&lt;/h2&gt;&lt;a href=&quot;#problem-solved&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Problem solved?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;After we had verified that the fix had worked, we now needed to stop it from happening again. Devs, marketing, data, and production all crowded into a small meeting room and began to figure out how this could’ve happened to begin with, and what we could do about it in future.&lt;/p&gt;
&lt;p&gt;It’s at this point I must admit that I had very little to do with the whole scenario up until this point. At the time, I was relatively junior in comparison to everyone else in the meeting, and I think I’d been invited with an expectation that I’d learn something rather than contribute.&lt;/p&gt;
&lt;p&gt;Listening to the conversation that ensued was definitely interesting. The developer who had made the breaking change was clearly frustrated with themself and put the blame on entirely on their own shoulders. The QA lead, who at this point was the entirety of the QA department (!), blamed themself for not catching it as well, and said that going forwards, they would just manually check those specific analytics every release.&lt;/p&gt;
&lt;p&gt;I was fully aware of how overworked our singular QA tester was, and it seemed to me that adding more to their workload was not only unfair, but also unrealistic! From where I was standing, nobody here was really at fault exactly: it wasn’t the dev’s fault that they didn’t notice the breakage in what should’ve been a totally unrelated system, and I felt that it was really just chance that it didn’t happen to me instead. And I couldn’t blame QA either, as they were already so overworked and definitely had enough new things to be testing for each release. But the main thing that stood out to me was how this really didn’t solve the underlying problem. Sure, if these specific analytics broke in future, that MIGHT get caught. But there was still only one line of defence there, and it was one that relied on overworked people working even more. And it didn’t fix the true problem that analytics could break due to changes that seemed unrelated, and nobody would know unless they specifically went to check.&lt;/p&gt;
&lt;p&gt;I wasn’t the only one thinking this way: the head of the data team became increasingly frustrated as no systematic changes were suggested, and the meeting concluded with QA agreeing to the extra workload. I left the meeting knowing that there must be a better solution out there.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;more-than-just-one-problem&quot;&gt;More than just one problem&lt;/h2&gt;&lt;a href=&quot;#more-than-just-one-problem&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “More than just one problem”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I thought about our analytics system in general as I sat on the tube home that day. Its fragility was not the only problem; I had several grievances with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Our events were sent in JSON format. Every key and every string value were sent as abbreviations in an attempt to minimise the amount of data we were sending. That in itself was a problem, because it was almost impossible to know what each abbreviation meant without digging into the code - not something that a dev should have to do, let alone someone on the data or marketing team!&lt;/li&gt;
&lt;li&gt;Even worse, in order to avoid defining a million strings solely for use in analytics, we had been reusing the same abbreviations to mean different things in different circumstances.&lt;/li&gt;
&lt;li&gt;Due to the ad-hoc and uncentralised implementation of many events, there was almost no consistency between the events. There were some shared fields, but frequently the same value would be sent from two locations formatted in a different way. For example, a cake that had been merged to level 3 was sometimes sent as “cake-3”, sometimes “mergePath-cake-3”, and sometimes “path: cakes, level: 3”.&lt;/li&gt;
&lt;li&gt;The whole team often forgot about analytics entirely during planning, which meant either we had no data for key new features, or we’d only remember at the last minute and had to rush and add ill-thought-through, minimally-tested code right up against the deadline.&lt;/li&gt;
&lt;li&gt;When we did get specifications for new events from the data team, the specs were varied in location and format. I had received specs as everything from open-to-interpretation descriptions buried in wiki pages to bizarrely-structured tables in spreadsheets trying to account for JSON’s multidimensional nature. This caused three problems:
&lt;ul&gt;
&lt;li&gt;None of the specifications I’d received easily mapped to the actual JSON output that my code would send. Devs usually didn’t get it right first try, and spent a lot of time going back-and-forth redoing work.&lt;/li&gt;
&lt;li&gt;Few of these specifications were easy to find, and none of them had been updated since first being written. Figuring out what existing code was trying to do was extremely difficult.&lt;/li&gt;
&lt;li&gt;None of the formats the specs came in were versioned. If I needed to look at historical data, it was really difficult to figure out whether any documentation I did find was relevant for the data I was looking at.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;And I had a strong suspicion that we’d broken other, less obvious events in the past, and our data team hadn’t discovered the breakage yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On top of this, I could remember several other occasions when one of the devs had broken something just like the inciting incident. This one was only different in that it broke something important - but the fact was that our breakages were making it to live pretty often, and it just was not feasible for QA to test all our data manually for every release.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-another-way&quot;&gt;Finding another way&lt;/h2&gt;&lt;a href=&quot;#finding-another-way&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Finding another way”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Once I got home, I opened up my laptop and simply googled “json validation”. Lo and behold, &lt;a href=&quot;https://json-schema.org/&quot;&gt;JSON Schema&lt;/a&gt; came up as the first result: a way to describe and define JSON data, using the JSON format! JSON Schema is widely used in several industries, and libraries existed that could be used to validate data against schemas. I dug a bit further, and discovered the Python library &lt;a href=&quot;https://coveooss.github.io/json-schema-for-humans/&quot;&gt;json-schema-for-humans&lt;/a&gt;, which generates nicely-formatted Markdown from JSON schemas.&lt;/p&gt;
&lt;p&gt;I knew I was on to something good - I could solve the automation, inconsistency, and comprehension problems all at once with the tools I’d found. So I set up an experiment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First, I put together a quick example schema in a git repository for an existing event.&lt;/li&gt;
&lt;li&gt;I broke out some of the data that was frequently shared by other events into a subschema and brought it in with an include, to prove that we could reuse parts of schemas for new events which meant that making new schemas would get more efficient over time.&lt;/li&gt;
&lt;li&gt;I copied out a few examples from our real analytics as test cases.&lt;/li&gt;
&lt;li&gt;I wrote a script that brought in the test cases and used the event name to pick the right schema. Then it ran the event data through the validator, feeding the result into &lt;a href=&quot;https://docs.pytest.org/en/stable/&quot;&gt;Pytest&lt;/a&gt; to generate a report at the end.&lt;/li&gt;
&lt;li&gt;Finally, I added a Github action that automatically ran on push to generate Markdown pages, automatically rendered by Github to be a lot more readable than the raw JSON Schema file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It worked great as a proof of concept. I made a few tweaks like bringing in a custom schema template so I could get inter-schema links working in default Github markdown rendering, and then I brought it to my boss. Happily, he saw the potential immediately, and set aside time for me to start implementation!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;goals&quot;&gt;Goals&lt;/h2&gt;&lt;a href=&quot;#goals&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Goals”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;After talking to my boss, I talked to the data team, QA and some of the other devs to narrow down my key goals for when this tool would roll out to the wider team.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;goal-1-a-clean-slate&quot;&gt;Goal 1: A clean slate&lt;/h3&gt;&lt;a href=&quot;#goal-1-a-clean-slate&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Goal 1: A clean slate”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I wanted to make sure that I had no test failures. Allowing failures as the default state would mean that the team would become used to failures being the default state, increasing the likelihood that “true” failures - that is, test failures indicating that something is actually wrong - would be disregarded along with the baseline failures that ran than a state that required an action. It was also important to build a system with those who would maintain it day-to-day so that we could return to that clean baseline as soon as possible. Having a clean slate to fall back on avoids the risk of diluting the response to all failures, which would compromise the fundamental purpose of having a validation system.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;goal-2-minimal-effort&quot;&gt;Goal 2: Minimal effort&lt;/h3&gt;&lt;a href=&quot;#goal-2-minimal-effort&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Goal 2: Minimal effort”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I wanted to hand the content of the system over to the data team, and the monitoring of the system over to QA. QA were happy to take on logging issues if they were flagged up automatically, so automatic notification went into the specification as well. The data team were happy to take ownership of the documentation as JSON Schema would allow them to keep all the information they needed about the events in one central place. They were especially looking forward to easily outlining events in a consistent and easy-to-understand format, so devs could implement those events with minimal reworking.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;goal-3-automation&quot;&gt;Goal 3: Automation&lt;/h3&gt;&lt;a href=&quot;#goal-3-automation&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Goal 3: Automation”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I wanted the system to run validation automatically, requiring zero effort from anyone if all the tests were passing. It’s hard to argue that people should do something if they don’t get any new information most of the time! And it’s natural to become complacent and prioritise other tasks if that’s the case as well.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;&lt;a href=&quot;#implementation&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Implementation”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;I began by copying my rough prototype into a new work repository, and set up authentication with Playfab so that I could pull data from storage. I set up my validation script in a job on Teamcity on our build machines, so that I could automatically pull the most recent 24 hours of data. Importantly, I didn’t pull data from our live build: I pulled the data from the store that our test builds sent to, so that we could catch issues &lt;em&gt;before&lt;/em&gt; they went live, while QA was running other tests.&lt;/p&gt;
&lt;p&gt;Teamcity has a &lt;a href=&quot;https://www.jetbrains.com/teamcity/tutorials/tests/python-build-configure-test/&quot;&gt;Pytest integration&lt;/a&gt; already, so I hooked that up to the validation report for nicely-readable results. Then I wrote up some draft schemas for all the events and ran the job.&lt;/p&gt;
&lt;p&gt;And…there were thousands of failures. I was expecting this to a degree, because I knew my draft schemas weren’t perfect and I would need to modify them to actually capture the intended structure of each event, but at least I had a starting point!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;running-out-of-time&quot;&gt;Running out of time&lt;/h2&gt;&lt;a href=&quot;#running-out-of-time&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Running out of time”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;So I started going through each schema and correcting errors. Unfortunately, as I dug into the code for each event, I discovered some major flaws in the data we were sending. There were even more inconsistencies than I expected: many things worked but were convoluted to read or oddly structured for no apparent reason; several things were broken and sent nothing; and worst of all, a few things sent data that was outright wrong. Fortunately, the fact that it hadn’t been flagged meant that the wrong data likely wasn’t being used, but then &lt;em&gt;that&lt;/em&gt; meant we were spending money storing and processing data that we just weren’t using!&lt;/p&gt;
&lt;p&gt;So suddenly my task had expanded to cover fixing or changing several events, many of which came from code in fundamental systems. But I knew it was necessary in order to have a consistent baseline to begin testing from. Ultimately, I came to the end of the allotted time without solving all of these issues. Everyone I spoke to could see the potential, but there were still many important events that weren’t covered by a thorough schema, and there were some major bugs that either I didn’t have time to fix, or the QA team didn’t have time to test thoroughly enough, or the data team didn’t have capacity to send across an exact specification for. I wasn’t happy to start running the validation while there were still so many failures because I knew that we’d become accustomed to seeing those failures and not notice when things were actually broken. So I put the whole thing on the backburner for the time being.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;rollout&quot;&gt;Rollout&lt;/h2&gt;&lt;a href=&quot;#rollout&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Rollout”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;However, the story doesn’t end there. I continued to push for more time while working on other things, and while the automated testing wasn’t running properly yet, the documentation aspect was absolutely present. I booked a meeting with each new data team member that joined to show them how it worked and explain the potential. The data team took on the job of writing schemas for new events as they requested implementation from the dev team, which meant that the documentation stayed up to date. During the wait for more dedicated time, I also had the ability to manually run the validation process, with the results only visible to me. As I had spent so much time digging into each of the events, I could see through the noise and tell when there was something truly wrong, so I flagged up relevant issues when things changed, and sometimes got the opportunity to fix other issues as side effects. Obviously this wasn’t sustainable though!&lt;/p&gt;
&lt;p&gt;Eventually I did get more time - two years later! I threw myself into ironing out the final issues, and over the next couple of weeks, with the help of some dedicated data team members (if you’re reading this, Sonya and Ankit, thank you so much - you were both invaluable!), I got to a point where we had no failures. I set up nightly automation and handed monitoring over to the QA team.&lt;/p&gt;
&lt;p&gt;The system continued to evolve after the initial hand-off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added the ability to run validation against events from only specific builds, or only from builds that were made from specific branches&lt;/li&gt;
&lt;li&gt;Allowed the user to specify a time range to gather events for validation&lt;/li&gt;
&lt;li&gt;Filtered daily testing to just test events against main and release builds to reduce noise from in-development builds&lt;/li&gt;
&lt;li&gt;Integrated Slack notifications when tests completed&lt;/li&gt;
&lt;li&gt;Organised a system to make sure the team wouldn’t become accustomed to the validator reporting failures, where the QA department would monitor the test results and assign any issues to a designated “on-call” developer, and then follow up on those issues until the tests passed again&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;what-couldve-been-better&quot;&gt;What could’ve been better&lt;/h2&gt;&lt;a href=&quot;#what-couldve-been-better&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “What could’ve been better”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;While I did achieve most of what I wanted, there are lots of things I didn’t get to. Here are some things I wish I’d had time for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Publishing the readable generated markdown documentation to a password-protected website, to make accessing the documentation more straightforward for CS agents and others without Github access - the current solution was to manually download the markdown files and send a zip file to those who needed it, who then opened the files in a program that rendered Github-flavoured markdown&lt;/li&gt;
&lt;li&gt;Making the schemas more thorough, adding better validation for field values with regexes or enums for example, or checking combinations of values in different fields using JSON Schema features like &lt;a href=&quot;https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse&quot;&gt;if/then/else&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Periodic validation against live events - currently the validator only runs against data sent from test builds due to the cost of pulling the large amount of data that is sent from players, but this was never really analysed to see if the cost would be worthwhile&lt;/li&gt;
&lt;li&gt;Creating a test plan that intentionally went through and sent instances of each event for filtering through the validator&lt;/li&gt;
&lt;li&gt;Creating a set of test data that would be sent through with each round of tests to make sure the validation itself hadn’t broken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And most important thing I felt the system was lacking was…well, unfortunately I actually don’t think that this system fully solved the original problem, where a change caused an event to not send at all. I did implement a set of tests to monitor whether an event was present in each round of testing, but I couldn’t flag that as a failure without compromising goal 1: not all periods of 24 hours would contain every type of event just because not all systems were tested every 24 hours.&lt;/p&gt;
&lt;p&gt;Compounding this issue was the fact that the Slack integration for Teamcity test reports was lacking, to say the least. The message sent through was not customisable at all, meaning that I couldn’t add any information about how long it had been since each event had been tested: so even if I had time I implement some sort of persistent data store to track the most recent test for each event, I wouldn’t have been able to send that to Slack with the other test results.&lt;/p&gt;
&lt;p&gt;Ultimately, the best I could do in the time I had was to mark a test as “skipped” if there was no instance of an event in that run’s data. This meant that the presence of an event would be at least tracked somewhere, without polluting results with false failures. This is not ideal at all: people need to proactively seek out that information instead of having it be passively monitored, they would need to seek out that information for each event one by one, and would have to manually go back and check the last time when that test had last reported a status other than ‘skipped’. My options were to compromise goal 1, a clean slate, over goal 3, automation, and ultimately I settled on keeping goal 1 as my gold standard. But I never came up with a solution that felt right to me. If you have ideas or if you’ve solved this problem before, please feel free to reach out on social media - I’d be very interested to hear your perspective!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;but-did-it-work&quot;&gt;But did it work?&lt;/h2&gt;&lt;a href=&quot;#but-did-it-work&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “But did it work?”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;That said, I think overall this system did solve a lot of problems! I can recall many situations where the validation system flagged a problem before changes went live, and at the time I left the team, the QA department had been successfully and consistently managing the validation system for over a year. The changes I implemented on the road to the first clean slate also cleaned up a lot of code and made it clearer and easier to use.&lt;/p&gt;
&lt;p&gt;Surprisingly though, I think the biggest benefit of this system wasn’t actually the validation at all, but the documentation. Members of the data team had to frequently interrupt their work to ask the dev team for clarifications on our obscure and cryptic analytics. But thorough and up-to-date documentation solved that problem completely! And as the documentation also functioned as test specifications, it avoided the most common documentation pitfall and was kept up-to-date.&lt;/p&gt;
&lt;p&gt;And it was also really useful documentation! I often see documentation from devs that’s mired in technical language and is more confusing than useful for end-users.But as this was written and maintained by the same people that were using it, it was focussed only on what they needed to know! Other departments benefitted from the improved clarity as well: customer support agents were more able to understand the context of players’ messages as they could understand the data sent by each player without having to wait for devs to interpret it; the product team were able to use the raw data if they needed to as well; and analytics were more present in everyone’s minds, which meant they were much less likely to be skipped over when planning features.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;key-take-aways&quot;&gt;Key take-aways&lt;/h2&gt;&lt;a href=&quot;#key-take-aways&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;currentcolor&quot; d=&quot;m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span&gt;Section titled “Key take-aways”&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Solve problems at the source, not at the symptom&lt;/li&gt;
&lt;li&gt;You can’t solve problems by adding work to already-overworked people&lt;/li&gt;
&lt;li&gt;“Do the exact same thing again, just better” is not a good solution&lt;/li&gt;
&lt;li&gt;Automate testing wherever possible&lt;/li&gt;
&lt;li&gt;Make tests that alert relevant people when something goes wrong&lt;/li&gt;
&lt;li&gt;False failures lead to a boy-who-cried-wolf situation: avoid them at all costs or real failures won’t trigger an appropriate response&lt;/li&gt;
&lt;li&gt;It’s always better when documentation users are responsible for accuracy and upkeep, because they’re invested in its reliability and they’re engaging with it regularly&lt;/li&gt;
&lt;li&gt;Prototypes will help people understand what you’re trying to solve and why you think this solution is a good one, and it will keep your solution in their minds when they’re involved in planning new tasks and allocating time for them&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>trailmix</category><category>love and pies</category><category>tooling</category></item></channel></rss>