conclusion of tairu

This commit is contained in:
liquidex 2024-02-19 22:32:26 +01:00
parent b506f5a219
commit 6712f51778
11 changed files with 539 additions and 330 deletions

View file

@ -1,3 +1,5 @@
%% title = "Graphical Indicators of Emotion, more commonly known as \"emoticons\" or under the brand name \"Emoji\""
% id = "01H8W7VEVNQ0XR4RDYRGMKS0E9" % id = "01H8W7VEVNQ0XR4RDYRGMKS0E9"
- the emojis here are grouped by game, topic, or other thing for easier navigation - the emojis here are grouped by game, topic, or other thing for easier navigation
@ -67,13 +69,36 @@
% id = "01H8W7VEVPSHDWDH58HFKBMGD6" % id = "01H8W7VEVPSHDWDH58HFKBMGD6"
- but I don't wanna replace it because it's just too good - but I don't wanna replace it because it's just too good
% id = "emoji/nap"
- :nap: - <sub>z</sub>zZ
% id = "01HQ162WWF60BD1F4K26E7ZZEV"
- ### random places - ### random places
% id = "emoji/ahyes" % id = "emoji/ahyes"
- :ahyes: - ah, yes - :ahyes: - ah, yes
% id = "01HQ162WWFYWW92J0DKRWNZYDY"
- smuggest expression for the smuggest of moments, and some tea with it. - smuggest expression for the smuggest of moments, and some tea with it.
% id = "emoji/kamien"
- :kamien: - stone cold
% id = "01HQ1K39AYJ2KEFFDAM6A8M8ET"
- Polish for _rock_ (stone, not the music genre)
% id = "01HQ1K39AY808NPR08YPBAR1ZF"
+ an expression of magnificence in primitivity
% id = "01HQ1K39AYKA86GRF12PFDN96N"
+ or magnificence in stupidity
% id = "01HQ1K39AYPE9FCD1FR93H60RR"
+ or being dead fucking serious about not liking something that's either of the two above (non-exclusive OR)
% id = "01HQ1K39AYM2FS3R4Y9175V7XS"
- all credit goes to Apple for designing this beauty
% id = "01HA4HJKQ7FKV8JJ70Q2CY9R86" % id = "01HA4HJKQ7FKV8JJ70Q2CY9R86"
- ### [Noto Color Emoji](https://github.com/googlefonts/noto-emoji/) - ### [Noto Color Emoji](https://github.com/googlefonts/noto-emoji/)

View file

@ -15,8 +15,9 @@ styles = ["tairu.css"]
- TODO: short videos demoing this here - TODO: short videos demoing this here
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9" % id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
- once upon a time I stumbled upon a technique called... - once upon a time I stumbled upon a technique called
% id = "01HQ162WWA1KXZPBDWJXSCQA1D"
- ### bitwise autotiling - ### bitwise autotiling
% id = "01HPD4XQPW6VK3FDW5QRCE6HSS" % id = "01HPD4XQPW6VK3FDW5QRCE6HSS"
@ -25,6 +26,7 @@ styles = ["tairu.css"]
% id = "01HPD4XQPWJ1CE9ZVRW98X7HE6" % id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile: - Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
% id = "01HQ162WWAMCPC5M88QAXHX4BT"
- so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look: - so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look:
```javascript tairu ```javascript tairu
@ -48,6 +50,7 @@ styles = ["tairu.css"]
```output tairu ```output tairu
``` ```
% id = "01HQ162WWAC3FN565QE3JAB87D"
- `Tilemap` is a class wrapping a flat [`Uint8Array`] with a `width` and a `height`, so that we can index it using (x, y) coordinates. - `Tilemap` is a class wrapping a flat [`Uint8Array`] with a `width` and a `height`, so that we can index it using (x, y) coordinates.
```javascript tairu ```javascript tairu
@ -59,31 +62,42 @@ styles = ["tairu.css"]
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array [`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
% id = "01HQ162WWA090YW5BR1XW68XJN"
- `at` has a `setAt` counterpart which sets tiles instead of getting them. - `at` has a `setAt` counterpart which sets tiles instead of getting them.
% id = "01HQ162WWAMD68SY56P7TVT2DJ"
- `TileEditor` provides a graphical editor for a `Tilemap` based on a `<canvas>`. - `TileEditor` provides a graphical editor for a `Tilemap` based on a `<canvas>`.
% id = "01HQ162WWABTFQ0J83C4VZYZB5"
- this editor is _Certified Battery Efficient™_, so it won't redraw unless it needs to!\ - this editor is _Certified Battery Efficient™_, so it won't redraw unless it needs to!\
we'll need to keep this in mind for later when we try to draw images, which may not be loaded during the initial draw. we'll need to keep this in mind for later when we try to draw images, which may not be loaded during the initial draw.
% id = "01HQ162WWA8Y1AD22MSN71V2E4"
- to kick this off, let's set off a goal. I would like the tiles in our little renderer to connect together, like this: - to kick this off, let's set off a goal. I would like the tiles in our little renderer to connect together, like this:
![red rectangle with a black outline, made out of 3x3 tiles][pic:01HPYW5SNTY0Z0ENDE5K3XWMTH] ![red rectangle with a black outline, made out of 3x3 tiles][pic:01HPYW5SNTY0Z0ENDE5K3XWMTH]
% id = "01HQ162WWAZV559ABQD1NVXPMA"
- let's break this down into smaller steps. drawing a border around the rectangle will involve: - let's break this down into smaller steps. drawing a border around the rectangle will involve:
% id = "01HQ162WWATV30HXGBQVWERP2M"
- determining *on which tiles* to draw it, - determining *on which tiles* to draw it,
% id = "01HQ162WWAA0V0SS0D1Y38BDS1"
- determining *where in these tiles* to draw it, - determining *where in these tiles* to draw it,
% id = "01HQ162WWAGBCBDYF4VH26MX1B"
- and actually drawing it! - and actually drawing it!
% id = "01HQ162WWA2PNGVV075HR3WMER"
- so let's zoom in a bit and look at the tiles one by one. in particular, let's focus on *these* two tiles: - so let's zoom in a bit and look at the tiles one by one. in particular, let's focus on *these* two tiles:
![the same red rectangle, now with a focus on the northern tile at its center][pic:01HPYWPJB1P0GK53BSJFJFRAGR] ![the same red rectangle, now with a focus on the northern tile at its center][pic:01HPYWPJB1P0GK53BSJFJFRAGR]
% id = "01HQ162WWAYDS6CSD3T102NA9X"
- notice how the two highlighted tiles are *different.* therefore, we can infer we should probably connect together any tiles that are *the same*. - notice how the two highlighted tiles are *different.* therefore, we can infer we should probably connect together any tiles that are *the same*.
% id = "01HQ162WWATDD86D4GY7RMT0BZ"
- knowing that, we can extract the logic to a function: - knowing that, we can extract the logic to a function:
```javascript tairu ```javascript tairu
@ -92,19 +106,24 @@ styles = ["tairu.css"]
} }
``` ```
% id = "01HQ162WWA9M6801Q0RNRSF09H"
+ now, also note that the border around this particular tile is only drawn on its *northern* edge - + now, also note that the border around this particular tile is only drawn on its *northern* edge -
therefore we can infer that borders should only be drawn on edges for whom `shouldConnect(thisTile, adjacentTile)` is **`false`** (not `true`!). therefore we can infer that borders should only be drawn on edges for whom `shouldConnect(thisTile, adjacentTile)` is **`false`** (not `true`!).
a tile generally has four edges - east, south, west, north - so we need to perform this check for all of them, and draw our border accordingly. a tile generally has four edges - east, south, west, north - so we need to perform this check for all of them, and draw our border accordingly.
% id = "01HQ162WWAM5YYQCEXH791T0E9"
- you might be wondering why I'm using this particular order for cardinal directions - why not [north, south, east, west]? or [north, east, south, west]? - you might be wondering why I'm using this particular order for cardinal directions - why not [north, south, east, west]? or [north, east, south, west]?
% id = "01HQ162WWABJ696HCJ09WDC0NX"
- the reason comes from math - `[cos(0) sin(0)]` is a vector pointing rightwards, not upwards! - the reason comes from math - `[cos(0) sin(0)]` is a vector pointing rightwards, not upwards!
and I chose clockwise order, because that's how the vector rotates as we increase the angle, in a coordinate space where +Y points downward - such as the `<canvas>` coordinate space. and I chose clockwise order, because that's how the vector rotates as we increase the angle, in a coordinate space where +Y points downward - such as the `<canvas>` coordinate space.
% id = "01HQ162WWABNXV4N2AHZBQC5B7"
- this choice yields some nice orderliness in the code that handles fetching tiles for connections - first you check `+X`, then `+Y`, then `-X`, and then `-Y` - - this choice yields some nice orderliness in the code that handles fetching tiles for connections - first you check `+X`, then `+Y`, then `-X`, and then `-Y` -
which my pedantic mind really appreciates :ahyes:\ which my pedantic mind really appreciates :ahyes:\
as `X` is first alphabetically, so checking `Y` first would feel wrong. as `X` is first alphabetically, so checking `Y` first would feel wrong.
% id = "01HQ162WWA5W8NXSXVZY3BBQ0H"
- to do that, I'm gonna override the tile editor's `drawTilemap` function - as this is where the actual tilemap rendering happens! - to do that, I'm gonna override the tile editor's `drawTilemap` function - as this is where the actual tilemap rendering happens!
```javascript tairu ```javascript tairu
@ -173,13 +192,16 @@ styles = ["tairu.css"]
```output tairu ```output tairu
``` ```
- this looks pretty perfect - maybe sans corners, which I'll conveniently skip for now - because most games don't actually render graphics in a vectorial way like this! % id = "01HQ162WWAAEKW1ECV5G3ZEY47"
- this looks pretty perfect - maybe sans corners, which I'll conveniently skip for now, because most games don't actually render graphics in a vectorial way like this!
instead, the more common way is to use a tileset - a big texture with a bunch of sprites to use for rendering each tile. instead, the more common way is to use a tileset - a big texture with a bunch of sprites to use for rendering each tile.
% id = "01HQ162WWACD5CD7GCZE53ZPD7"
- not only does this have the advantage of allowing for richer graphics, but it is also a lot easier to modify by artists, because you no longer need knowledge of graphics APIs to draw tiles. - not only does this have the advantage of allowing for richer graphics, but it is also a lot easier to modify by artists, because you no longer need knowledge of graphics APIs to draw tiles.
% template = true % template = true
classes.branch = "tileset-cardinal-directions-demo" classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWAADKPDQE69W3QZG0M"
- for example, here's a tileset I drew for the 3rd iteration of my game [Planet Overgamma] - though tweaked a bit because I had never used it before writing this post :hueh: - for example, here's a tileset I drew for the 3rd iteration of my game [Planet Overgamma] - though tweaked a bit because I had never used it before writing this post :hueh:
![heavy metal sheet tileset from Planet Overgamma, made out of 16 tiles. it looks like heavy embossed sheets of metal, resembling steel in its heavyness][pic:01HPHVDRV0F0251MD0A2EG66C4] ![heavy metal sheet tileset from Planet Overgamma, made out of 16 tiles. it looks like heavy embossed sheets of metal, resembling steel in its heavyness][pic:01HPHVDRV0F0251MD0A2EG66C4]
@ -187,6 +209,7 @@ styles = ["tairu.css"]
[Planet Overgamma]: https://github.com/liquidev/planet-overgamma [Planet Overgamma]: https://github.com/liquidev/planet-overgamma
% classes.branch = "tileset-cardinal-directions-demo" % classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWAS502000K8QZWVBDW"
- we can split this tileset up into 16 individual tiles, each one 8 × 8 pixels; people choose various resolutions, I chose a fairly low one to hide my lack of artistic skill. - we can split this tileset up into 16 individual tiles, each one 8 × 8 pixels; people choose various resolutions, I chose a fairly low one to hide my lack of artistic skill.
<div class="horizontal-tile-strip"> <div class="horizontal-tile-strip">
@ -209,6 +232,7 @@ styles = ["tairu.css"]
</div> </div>
% classes.branch = "tileset-cardinal-directions-demo" % classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWANBTYH1JJWCTZYYVN"
- the keen eyed among you have probably noticed that this is very similar to the case we had before with drawing procedural borders - - the keen eyed among you have probably noticed that this is very similar to the case we had before with drawing procedural borders -
except that instead of determining which borders to draw based on a tile's neighbors, this time we'll determine which *whole tile* to draw based on its neighbors! except that instead of determining which borders to draw based on a tile's neighbors, this time we'll determine which *whole tile* to draw based on its neighbors!
@ -231,15 +255,20 @@ styles = ["tairu.css"]
<span class="metal x-3 y-3"></span> <span class="metal x-3 y-3"></span>
</div> </div>
% id = "01HQ162WWA4Z6KKWFV59BR4WD3"
- previously we represented which single border to draw with a single boolean. - previously we represented which single border to draw with a single boolean.
now we will represent which single tile to draw with *four* booleans, because each tile can connect to four different directions. now we will represent which single tile to draw with *four* booleans, because each tile can connect to four different directions.
% id = "01HQ162WWAQ9GZ6JD8KESW4N53"
- four booleans like this can easily be packed into a single integer using some bitwise operations, hence we get ***bitwise autotiling*** - autotiling using bitwise operations! - four booleans like this can easily be packed into a single integer using some bitwise operations, hence we get ***bitwise autotiling*** - autotiling using bitwise operations!
% id = "01HQ162WWAMBM8RXKQTN3D0XR2"
- now the clever part of bitwise autotiling is that we can use this packed integer *as an array index* - therefore selecting which tile to draw can be determined using just a single lookup table! neat, huh? - now the clever part of bitwise autotiling is that we can use this packed integer *as an array index* - therefore selecting which tile to draw can be determined using just a single lookup table! neat, huh?
% id = "01HQ162WWA0ZGZ97JZZBFS41TF"
- but because I'm lazy, and CPU time is valuable, instead of using an array I'll just rearrange the tileset texture a bit to be able to slice it in place using this index. - but because I'm lazy, and CPU time is valuable, instead of using an array I'll just rearrange the tileset texture a bit to be able to slice it in place using this index.
% id = "01HQ162WWAQQ99TRBDY5DCSW3Z"
- say we arrange our bits like this: - say we arrange our bits like this:
```javascript tairu ```javascript tairu
@ -250,6 +279,7 @@ styles = ["tairu.css"]
``` ```
% classes.branch = "tileset-cardinal-directions-demo" % classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWABANND0WGT933TBMV"
- that means we'll need to arrange our tiles like so, where the leftmost tile is at index 0 (`0b0000`) and the rightmost tile is at index 15 (`0b1111`): - that means we'll need to arrange our tiles like so, where the leftmost tile is at index 0 (`0b0000`) and the rightmost tile is at index 15 (`0b1111`):
<div class="horizontal-tile-strip"> <div class="horizontal-tile-strip">
@ -271,16 +301,21 @@ styles = ["tairu.css"]
<span class="metal x-1 y-1"><span class="east">E</span><span class="south">S</span><span class="west">W</span><span class="north">N</span></span> <span class="metal x-1 y-1"><span class="east">E</span><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
</div> </div>
% id = "01HQ162WWAJPW00XA25N0K6KS7"
- packing that into a single tileset, or rather this time, a *tile strip*, we get this image: - packing that into a single tileset, or rather this time, a *tile strip*, we get this image:
![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5] ![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5]
% id = "01HQ162WWAT2ZC7T2P9ATD6WG2"
- now it's time to actually implement it as code! I'll start by defining a *tile index* function as a general way of looking up tiles in a tileset. - now it's time to actually implement it as code! I'll start by defining a *tile index* function as a general way of looking up tiles in a tileset.
% id = "01HQ162WWA0NRHBB6HP2RERNBK"
- I want to make the tile renderer a bit more general, so being able to attach a different tile lookup function to each tileset sounds like a great feature. - I want to make the tile renderer a bit more general, so being able to attach a different tile lookup function to each tileset sounds like a great feature.
% id = "01HQ162WWA9PGSHH5E97RVE1PB"
- just imagine some game where glass connects to metal, but metal doesn't connect to glass - I bet that would look pretty great! - just imagine some game where glass connects to metal, but metal doesn't connect to glass - I bet that would look pretty great!
% id = "01HQ162WWAYJ4JCG3Z24SJR8S9"
- …but anyways, here's the basic bitwise magic function: - …but anyways, here's the basic bitwise magic function:
```javascript tairu ```javascript tairu
@ -298,12 +333,13 @@ styles = ["tairu.css"]
``` ```
% template = true % template = true
id = "01HQ162WWAS813ANMBG1PWDZHC"
- we'll define our tilesets by their texture, tile size, and a tile indexing function. so let's create an object that will hold our tileset data: - we'll define our tilesets by their texture, tile size, and a tile indexing function. so let's create an object that will hold our tileset data:
```javascript tairu ```javascript tairu
// You'll probably want to host the assets on your own website rather than // You'll probably want to host the assets on your own website rather than
// hotlinking to others. It helps longevity! // hotlinking to others. It helps longevity!
const tilesetImage = new Image(); let tilesetImage = new Image();
tilesetImage.src = "{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}"; tilesetImage.src = "{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}";
export const heavyMetalTileset = { export const heavyMetalTileset = {
@ -313,6 +349,7 @@ styles = ["tairu.css"]
}; };
``` ```
% id = "01HQ162WWA0SC2GA7Y3KJE0W5F"
- with all that, we should now be able to write a tile renderer which can handle textures! so let's try it: - with all that, we should now be able to write a tile renderer which can handle textures! so let's try it:
```javascript tairu ```javascript tairu
@ -360,7 +397,8 @@ styles = ["tairu.css"]
} }
``` ```
- drum roll please... % id = "01HQ162WWAS2HYF41MZNJ18BXC"
- drum roll please…
```javascript tairu ```javascript tairu
new TilesetTileEditor({ new TilesetTileEditor({
@ -372,6 +410,7 @@ styles = ["tairu.css"]
```output tairu ```output tairu
``` ```
% id = "01HQ162WWA03JAGJYCT0DRZP24"
- it works! buuuut if you play around with it you'll quickly start noticing some problems: - it works! buuuut if you play around with it you'll quickly start noticing some problems:
```javascript tairu ```javascript tairu
@ -393,58 +432,75 @@ styles = ["tairu.css"]
```output tairu ```output tairu
``` ```
% id = "01HQ162WWAB0AYSPGB4AEVT03Z"
- where did our nice seamless connections go!? - where did our nice seamless connections go!?
% template = true % id = "01HQ162WWA3Q095ZGXDFZ1V2Q1"
id = "01HPMVT9BM9CS9375MX4H9WKW8" - ### thing is, it was never good in the first place
- and that gives us this result:
<canvas % id = "01HQ162WWARSVDRNHZE13ZF6W6"
is="tairu-editor" - I'll be blunt: we don't have enough tiles to represent *corners*! like in this case:
data-tilemap-id="bitwiseAutotiling"
data-tile-size="40"
>
Your browser does not support &lt;canvas&gt;.
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
</canvas>
% id = "01HPMVT9BM3WR0BNZFHP2BPZ8A" ```javacript tairu
- but if you play around with it (or have *already* played around with it, and are therefore left with a non-default tilemap) import { Tilemap } from "tairu/tilemap.js";
...something seems awful about it doesn't it? new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" xx ",
" x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
```output tairu
```
% template = true % id = "01HQ1K39AS4VDW7DVTAGQ03WFM"
id = "01HPMVT9BMPA89037VPWPPWX8V" - have a closer look at the top-left tile:
- something's off about the corners. let me give you a fresh example to illustrate what I mean:
<canvas ![the above example, showing an L shape rotated 180°, with the top left corner highlighted][pic:01HQ167GJEPTKHAKAVNW3WN1SZ]
is="tairu-editor"
data-tilemap-id="bitwiseAutotilingChapter2"
data-tile-size="40"
>
Your browser does not support &lt;canvas&gt;.
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
</canvas>
% id = "01HPMVT9BM16EF3TV5J1K19JAM" % id = "01HQ1K39AS6Y9XMJTMMQYTWRMC"
+ see that tile in the bottom left corner of the `L` shape? it's missing a corner. - it should kind of _"bend"_ to fit in with the tiles to the north and the south, but it doesn't :kamien:
the top-right corner, to be exact, which makes it visually disjoint from the tiles to the north and the east.
% id = "01HPMVT9BM5VWJSMDNPK2SRNZV" % id = "01HQ1K39ASQQNF7B881SYJWRC7"
- (I'm totally not trying to say this implementation is an L so far) - so what if we made the tile look like *this* instead:
% id = "01HPMVT9BMWG6QHQ125Z884W8Z" ![mockup showing that previous L-shape but with a real corner][pic:01HQ17GYEZSZCVRBFHP4HXAJV8]
+ i'll cut right to the chase here and say it outright - the issue is that we simply don't have enough tiles to represent *corner* cases like this!
% id = "01HPMVT9BMQK8N1H68YV3J4CFQ" % id = "01HQ1K39ASMKRMTXFV93FRHZTG"
- see what I did there? - that sure as heck looks a lot nicer! but there's a problem: that tile, let's zoom in on it…
% id = "01HPMVT9BMJTG3KD3K5EJ3BC93" ![that bent tile, and just *it* alone][pic:01HQ183RANGH4S7VZSG1ZGH0S5]
- the solution to that is to introduce more tiles to handle these edge cases.
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASR81NWMW8Q0MF8QMP"
- enhance!
<ul class="directions-square bend">
<li class="east">E</li>
<li class="south">S</li>
</ul>
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASC5WTR2A2AJN85JK2"
- huh. interesting. it connects to the east and the south. so what about this tile -
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south">S</li>
</ul>
% id = "01HQ1K39ASXYBH9QJH5Q0C45JZ"
- because it *also* connects to the east and the south :thinking:
% id = "01HQ1K39ASW5PWS52NGA2X3M0P"
- seems like we'll need something to disambiguate the two cases - and what better thing to disambiguate with than *more bits*!
% classes.branch = "tileset-four-to-eight-demo" % classes.branch = "tileset-four-to-eight-demo"
id = "01HPQCCV4R5N97FJ1GS36HZJZ7" id = "01HPQCCV4R5N97FJ1GS36HZJZ7"
- to represent the corners, we'll turn our four cardinal directions... - to represent the corners, we'll turn our four cardinal directions
<ul class="directions-square"> <ul class="directions-square">
<li class="east">E</li> <li class="east">E</li>
@ -466,32 +522,43 @@ styles = ["tairu.css"]
<li class="north-east"><a href="https://github.com/NoiseStudio/NoiseEngine/" title="NoiseEngine????">NE</a></li> <li class="north-east"><a href="https://github.com/NoiseStudio/NoiseEngine/" title="NoiseEngine????">NE</a></li>
</ul> </ul>
% id = "01HPQCCV4R3GNEWZQFWGWH4Z6R" % id = "01HQ1K39ASFN94YDY1RWQYS12K"
- you might think that at this point we'll need 8 bits to represent our tiles, and that would make... - at this point with the four extra corners we'll need 8 bits to represent our tiles, and that would make…
***256 tiles!?*** ***256 tiles!?***
nobody in their right mind would actually draw 256 separate tiles, right? ***RIGHT???*** nobody in their right mind would actually draw 256 separate tiles, right? ***RIGHT???***
% template = true % template = true
id = "01HPQCCV4RX13VR4DJAP2F9PFA" id = "01HQ1K39AS11M1M4GQQ60NXTY6"
- ...right! if you experiment with the bit combinations, you'll quickly find out that there is no difference if, relative to a single center tile, we have tiles on the corners: - …right! let's stick with the 16 tile version for a moment.
if we arrange the tiles in a diagnonal cross like this, notice how the tile in the center would have the bits `SE | SW | NW | NE` set, which upon first glance would suggest us needing a different tile -
but it looks correct!
<canvas ```javascript tairu
is="tairu-editor" import { Tilemap } from "tairu/tilemap.js";
data-tilemap-id="bitwiseAutotilingCorners"
data-tile-size="40"
>
Your browser does not support &lt;canvas&gt;.
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
</canvas>
these should all render the same way, despite technically having some [new neighbors](https://en.wikipedia.org/wiki/Moore_neighborhood). new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" x x ",
" x ",
" x x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
```output tairu
```
% id = "01HQ1K39AS7CRBZ67N1VVHCVME"
- therefore there must be *some* bit combinations that are redundant to others. let's find them!
% classes.branch = "tileset-four-to-eight-demo" % classes.branch = "tileset-four-to-eight-demo"
id = "01HPQCCV4RHZ8A7VMT2KM7T27P" id = "01HQ1K39ASZPJ4E23EZ1XJ5J7K"
- what we can do about this is to ignore corners whenever zero or one of the tiles at their cardinal directions is connected - - let's pick one corner first, then generalize to all the other ones. I pick southeast!
for example, in the case of `E | SE | S`:
<ul class="directions-square e-s"> <ul class="directions-square e-s">
<li class="east">E</li> <li class="east">E</li>
@ -499,15 +566,44 @@ styles = ["tairu.css"]
<li class="south">S</li> <li class="south">S</li>
</ul> </ul>
we can completely ignore what happens in the northeast, northwest, and southwest, because the tile's cardinal directions do not fully contain any of these direction pairs. % id = "01HQ1K39ASQTR054W0VWEAV2FS"
- in this case, if we remove the tile to the southeast, we get that bent tile from before:
<ul class="directions-square bend">
<li class="east">E</li>
<li class="south">S</li>
</ul>
% id = "01HQ1K39AS6RGE6Z83T8MH1R0M"
- what we can learn from this is that for `E | S`, `ES` affects the result!
% id = "01HQ1K39ASVSAQ6F8ANEZE1WQ4"
- but if we add any other corner, nothing changes. heck, let's add all of them:
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south-east">SE</li>
<li class="south">S</li>
<li class="south-west">SW</li>
<li class="north-west">NW</li>
<li class="north-east">NE</li>
</ul>
% id = "01HQ1K39AST8RQTVSCDV7FSH62"
- this combination is definitely redundant!
% id = "01HQ1K39AS8VHKHANJFKA4PQJ5"
- so it seems like for any two cardinal directions such as `E` and `S`, the ordinal direction that's a combination of the two -
in this case `ES` - only matters if both the cardinal direction bits are set!
% id = "01HPQCCV4R557T2SN7ES7Z4EJ7" % id = "01HPQCCV4R557T2SN7ES7Z4EJ7"
- we can verify this logic with a bit of code; with a bit of luck, we should be able to narrow down our tileset into something a lot more manageable. - we can verify this logic with a bit of code; with a bit of luck, we should be able to narrow down our tileset into something a lot more manageable.
% id = "01HPSY4Y19NQ6DZN10BP1KQEZN" % id = "01HPSY4Y19NQ6DZN10BP1KQEZN"
+ we'll start off by defining a bunch of variables to represent our ordinal directions: + we'll start off by redefining our bits to be ordinal directions instead. I still want to keep the [nice orderliness][branch:01HQ162WWAM5YYQCEXH791T0E9] that comes with
arranging the bits clockwise starting from east, so if we want that we can't just extend the indices with an extra four bits at the top.
```javascript ordinal-directions ```javascript tairu
export const E = 0b0000_0001; export const E = 0b0000_0001;
export const SE = 0b0000_0010; export const SE = 0b0000_0010;
export const S = 0b0000_0100; export const S = 0b0000_0100;
@ -516,18 +612,12 @@ styles = ["tairu.css"]
export const NW = 0b0010_0000; export const NW = 0b0010_0000;
export const N = 0b0100_0000; export const N = 0b0100_0000;
export const NE = 0b1000_0000; export const NE = 0b1000_0000;
export const ALL = E | SE | S | SW | W | NW | N | NE;
``` ```
as I've already said, we represent each direction using a single bit.
% id = "01HPSY4Y19AW70YX8PPA7AS4DH"
- I'm using JavaScript by the way, because it's the native programming language of your web browser. read on to see why.
% id = "01HPSY4Y19HPNXC54VP6TFFHXN" % id = "01HPSY4Y19HPNXC54VP6TFFHXN"
- now I don't know about you, but I find the usual C-style way of checking whether a bit is set extremely hard to read, so let's take care of that: - I don't know about you, but I find the usual C-style way of checking whether a bit is set extremely hard to read, so let's take care of that:
```javascript ordinal-directions ```javascript tairu
export function isSet(integer, bit) { export function isSet(integer, bit) {
return (integer & bit) == bit; return (integer & bit) == bit;
} }
@ -537,8 +627,8 @@ styles = ["tairu.css"]
- now we can write a function that will remove the aforementioned redundancies. - now we can write a function that will remove the aforementioned redundancies.
the logic is quite simple - for southeast, we only allow it to be set if both south and east are also set, and so on and so forth. the logic is quite simple - for southeast, we only allow it to be set if both south and east are also set, and so on and so forth.
```javascript ordinal-directions ```javascript tairu
// t is a tile index; variable name is short for brevity // t is an existing tile index; variable name is short for brevity
export function removeRedundancies(t) { export function removeRedundancies(t) {
if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) { if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) {
t &= ~SE; t &= ~SE;
@ -559,10 +649,10 @@ styles = ["tairu.css"]
% id = "01HPSY4Y19HWQQ9XBW1DDGW68T" % id = "01HPSY4Y19HWQQ9XBW1DDGW68T"
- with that, we can find a set of all unique non-redundant combinations: - with that, we can find a set of all unique non-redundant combinations:
```javascript ordinal-directions ```javascript tairu
export function ordinalDirections() { export function ordinalDirections() {
let unique = new Set(); let unique = new Set();
for (let i = 0; i <= ALL; ++i) { for (let i = 0; i <= 0b1111_1111; ++i) {
unique.add(removeRedundancies(i)); unique.add(removeRedundancies(i));
} }
return Array.from(unique).sort((a, b) => a - b); return Array.from(unique).sort((a, b) => a - b);
@ -576,14 +666,14 @@ styles = ["tairu.css"]
[`Array.prototype.sort`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort [`Array.prototype.sort`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
% id = "01HPSY4Y19V62YKTGK3TTKEB38" % id = "01HPSY4Y19V62YKTGK3TTKEB38"
- and now it's time to _Let It Cook™_: - and with all the ingredients in the pot, we now _Let It Cook™_:
```javascript ordinal-directions ```javascript tairu
let dirs = ordinalDirections(); let dirs = ordinalDirections();
console.log(dirs.length); console.log(dirs.length);
``` ```
```output ordinal-directions ```output tairu
47 47
``` ```
@ -595,7 +685,7 @@ styles = ["tairu.css"]
the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO. the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO.
% id = "01HPSY4Y19TM2K2WN06HHEM3D0" % id = "01HPSY4Y19TM2K2WN06HHEM3D0"
- phew... the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling! - phew… the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling!
% id = "01HPSY4Y192FZ37K3KXZM90K9J" % id = "01HPSY4Y192FZ37K3KXZM90K9J"
- so in reality we actually only need 47 tiles and not 256 - that's a whole lot less, that's 81.640625% less tiles we have to draw! - so in reality we actually only need 47 tiles and not 256 - that's a whole lot less, that's 81.640625% less tiles we have to draw!
@ -613,38 +703,40 @@ styles = ["tairu.css"]
% id = "01HPWJB4Y0HKGSDABB56CNFP9H" % id = "01HPWJB4Y0HKGSDABB56CNFP9H"
- think that with this redundancy elimination approach most of the tiles will never even be looked up by the renderer, because the bit combinations will be collapsed into a more canonical form before the lookup. - think that with this redundancy elimination approach most of the tiles will never even be looked up by the renderer, because the bit combinations will be collapsed into a more canonical form before the lookup.
% id = "01HPWJB4Y0705RWPFB89V23M1P" % id = "01HQ1K39ASM53P1E74HKRZ1T24"
- we could also use the approach I mentioned briefly [here][branch:01HPQCCV4RB65D5Q4RANJKGC0D], which involves introducing a lookup table - which sounds reasonable, so let's do it! - so instead of wasting space, we can compress the tiles into a compact strip, and use a lookup table from sparse tile indices to dense tile *positions* within the strip.
% id = "01HPWJB4Y0F9JGXQDAAVC3ERG1" % id = "01HPWJB4Y0F9JGXQDAAVC3ERG1"
- I don't want to write the lookup table by hand, so let's generate it! I'll reuse the redundancy elimination code from before to make this easier. - I don't want to write the lookup table by hand, so let's generate it!
% id = "01HPWJB4Y0HTV32T4WMKCKWTVA" % id = "01HPWJB4Y0HTV32T4WMKCKWTVA"
- we'll start by obtaining our ordinal directions array again: - we'll start by obtaining our ordinal directions array again:
```javascript ordinal-directions ```javascript tairu
export let xToConnectionBitSet = ordinalDirections(); export let xToConnectionBitSet = ordinalDirections();
``` ```
% id = "01HPWJB4Y03WYYZ3VTW27GP7Z3" % id = "01HPWJB4Y03WYYZ3VTW27GP7Z3"
- then we'll turn that array upside down... in other words, invert the index-value relationship, so that we can look up which X position in the tile strip to use for a specific connection combination. - then we'll turn that array upside down… in other words, invert the index-value relationship, so that we can look up which X position in the tile strip to use for a specific connection combination.
remember that our array has only 256 values, so it should be pretty cheap to represent using a `Uint8Array`: remember that our array has only 256 values, so it should be pretty cheap to represent using a [`Uint8Array`]:
```javascript ordinal-directions ```javascript tairu
export let connectionBitSetToX = new Uint8Array(256); export let connectionBitSetToX = new Uint8Array(256);
for (let i = 0; i < xToConnectionBitSet.length; ++i) { for (let i = 0; i < xToConnectionBitSet.length; ++i) {
connectionBitSetToX[xToConnectionBitSet[i]] = i; connectionBitSetToX[xToConnectionBitSet[i]] = i;
} }
``` ```
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
% id = "01HPWJB4Y0CWQB9EZG6C91A0H0" % id = "01HPWJB4Y0CWQB9EZG6C91A0H0"
- and there we go! we now have a mapping from our bitset to positions within the tile strip. try to play around with the code example to see which bitsets correspond to which position! - and there we go! we now have a mapping from our bitset to positions within the tile strip. try to play around with the code example to see which bitsets correspond to which position!
```javascript ordinal-directions ```javascript tairu
console.log(connectionBitSetToX[E | SE | S]); console.log(connectionBitSetToX[E | SE | S]);
``` ```
```output ordinal-directions ```output tairu
4 4
``` ```
@ -652,7 +744,7 @@ styles = ["tairu.css"]
+ for my own (and your) convenience, here's a complete list of *all* the possible combinations in order. + for my own (and your) convenience, here's a complete list of *all* the possible combinations in order.
% id = "01HPWJB4Y01VJFMHYEC1WZ353W" % id = "01HPWJB4Y01VJFMHYEC1WZ353W"
- ```javascript ordinal-directions - ```javascript tairu
function toString(bitset) { function toString(bitset) {
if (bitset == 0) return "0"; if (bitset == 0) return "0";
@ -672,7 +764,7 @@ styles = ["tairu.css"]
console.log(`${x} => ${toString(xToConnectionBitSet[x])}`); console.log(`${x} => ${toString(xToConnectionBitSet[x])}`);
} }
``` ```
```output ordinal-directions ```output tairu
0 => 0 0 => 0
1 => E 1 => E
2 => S 2 => S
@ -730,36 +822,108 @@ styles = ["tairu.css"]
![horizontal tile strip of 47 8x8 pixel metal tiles][pic:01HPW47SHMSVAH7C0JR9HWXWCM] ![horizontal tile strip of 47 8x8 pixel metal tiles][pic:01HPW47SHMSVAH7C0JR9HWXWCM]
% id = "01HPWJB4Y0J3DHQV5F9GD3VNQ8" % id = "01HPWJB4Y0J3DHQV5F9GD3VNQ8"
- now let's hook it up to our tileset renderer! TODO literate program. - now let's hook it up to our tileset renderer!
% id = "01HQ1M84GS09M7PMXFYHDPRTMT"
- since we already prepared the bulk of the framework before, it should be as simple as writing a new `tileIndex` function:
```javascript tairu
export function tileIndexInBitwiseTileset47(tilemap, x, y) {
let tile = tilemap.at(x, y);
let tileBitset = 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y)) ? E : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y + 1)) ? SE : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x, y + 1)) ? S : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y + 1)) ? SW : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y)) ? W : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y - 1)) ? NW : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x, y - 1)) ? N : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y - 1)) ? NE : 0;
return connectionBitSetToX[removeRedundancies(tileBitset)];
}
```
% template = true % template = true
id = "01HPWJB4Y00ARHBGDF2HTQQ4SD" id = "01HQ1M84GS4C99VQZC4150CMDS"
- with the capability to render with 47-tile tilesets, our examples suddenly look a whole lot better! - now we can write a new tileset descriptor that uses this indexing function and the larger tile strip:
```javascript tairu
// Once again, use your own link here!
let tilesetImage = new Image();
tilesetImage.src = "{% pic 01HPW47SHMSVAH7C0JR9HWXWCM %}";
export const heavyMetalTileset47 = {
image: tilesetImage,
tileSize: 8,
tileIndex: tileIndexInBitwiseTileset47,
};
```
% id = "01HQ1M84GS9CC8VR1BVDC15W50"
- and Drum Roll 2: Return of the Snare please…
```javascript tairu
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" x ",
" x x ",
" xxx ",
" xx ",
" x ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
```output tairu
```
% id = "01HQ1M84GSCXTPGVPXY840WCQ6"
- it works perfectly!
% id = "01HQ1M84GSVBG9T94ZN9XTXX58"
- but honestly, this is a bit *boring* if we're gonna build a game with procedural worlds.
% id = "01HQ1M84GSH0KTFFZET6GZZ4V2"
- heck, it's even boring for a level designer to have to lay out all the tiles manually -
introducing variations and what not, such that the world doesn't look too bland… there has to be a better way!
% id = "01HQ1M84GSE1N5WG88DGJZH0F8"
- and a better way… there is! but I'll get to that once my nap is over. <!--
% id = "01HQ1M84GS0KJ9NA6GPS62RC95"
- for now, have a big editor to play around with. it's a lot of fun arranging the tiles in various shapes!
```javascript tairu
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: new Tilemap(25, 16),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
```output tairu
```
:nap:
% disabled = true
id = "01HQ1M84GS3WKE2X6QV2SNQX46"
- TODO: next chapter! if you're reading this, you're in on it soon. that's quite sad, but YOU CAN STILL TURN BACK! I advise you to do that immediately.
You have been warned.
<canvas
is="tairu-editor"
data-tilemap-id="bitwiseAutotiling47"
data-tile-size="40"
>
Your browser does not support &lt;canvas&gt;.
<img class="resource" src="{% pic 01HPW47SHMSVAH7C0JR9HWXWCM %}" data-tairu-tileset-47="1">
</canvas>
% id = "01HPD4XQPWT9N8X9BD9GKWD78F"
- bitwise autotiling is a really cool technique that I've used in plenty of games in the past.
% id = "01HPD4XQPW5FQY8M04S6JEBDHQ"
- as I mentioned before, [I've known it since my Construct 2 days][branch:01HPD4XQPW6VK3FDW5QRCE6HSS], but when it comes to any released games [Planet Overgamma] would probably be the first to utilize it properly.
TODO video of some Planet Overgamma gameplay showing the autotiling in action
[Planet Overgamma]: https://liquidev.itch.io/planet-overgamma-classic
% id = "01HPJ8GHDEN4XRPT1AJ1BTNTFJ"
- this accursed game has been haunting me for years since; there have been many iterations.
the autotiling source code of the one in the video can be found [here][autotiling source code].
[autotiling source code]: https://github.com/liquidev/planet-overgamma/blob/classic/jam/map.lua#L209
% id = "01HPD4XQPWPN6HNA6M6EH507C6" % id = "01HPD4XQPWPN6HNA6M6EH507C6"
+ but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit) + but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit)
@ -811,7 +975,7 @@ styles = ["tairu.css"]
- then vines - then vines
% id = "01HPSY4Y19FA2HGYE4F3Y9NJ57" % id = "01HPSY4Y19FA2HGYE4F3Y9NJ57"
- well... it's even simpler than that in terms of graphical presentation, but we'll get to that. - well… it's even simpler than that in terms of graphical presentation, but we'll get to that.
% id = "01HPD4XQPWK58Z63X6962STADR" % id = "01HPD4XQPWK58Z63X6962STADR"
- I mean, after all - bitwise autotiling is basically a clever solution to an `if` complexity problem, so why not extend that with more logic and rules and stuff to let you build more complex maps? - I mean, after all - bitwise autotiling is basically a clever solution to an `if` complexity problem, so why not extend that with more logic and rules and stuff to let you build more complex maps?

View file

@ -303,6 +303,11 @@ img {
/* TODO: These could be autogenerated! */ /* TODO: These could be autogenerated! */
&[src*='+width72'] {
width: 72px;
height: auto;
}
&[src*='+width160'] { &[src*='+width160'] {
width: 160px; width: 160px;
height: auto; height: auto;
@ -556,6 +561,7 @@ th-literate-program[data-mode="output"] {
padding: 0; padding: 0;
background: none; background: none;
border: none; border: none;
border-radius: 0;
& iframe { & iframe {
border-style: none; border-style: none;
@ -635,7 +641,7 @@ th-literate-program[data-mode="output"] {
--syntax-comment: #aca8a4; --syntax-comment: #aca8a4;
--syntax-identifier: var(--text-color); --syntax-identifier: var(--text-color);
--syntax-keyword1: #ffb06a; --syntax-keyword1: #ffb06a;
--syntax-keyword2: #9acfe3; --syntax-keyword2: #8ad4f9;
--syntax-operator: #ec9f8d; --syntax-operator: #ec9f8d;
--syntax-function: #fbd283; --syntax-function: #fbd283;
--syntax-literal: #e9b9f0; --syntax-literal: #e9b9f0;

View file

@ -135,6 +135,20 @@
position: relative; position: relative;
&.bend {
background-image: url('../pic/01HQ183RANGH4S7VZSG1ZGH0S5-the-tile+width72+pixel.png');
background-size: 100%;
background-position: 0 0;
background-repeat: no-repeat;
}
&.notabend {
background-image: url('../pic/01HQ18E39K5F9Q5P41XAEVSEWK-the-also-tile.png');
background-size: 100%;
background-position: 0 0;
background-repeat: no-repeat;
}
li { li {
padding: 2px 4px; padding: 2px 4px;
position: absolute; position: absolute;

BIN
static/emoji/kamien.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/emoji/nap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B