treehouse/content/programming/blog/tairu.tree

1062 lines
42 KiB
Plaintext
Raw Normal View History

2024-02-11 23:05:08 +01:00
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
2024-02-14 23:31:39 +01:00
scripts = [
"treehouse/components/literate-programming.js",
"treehouse/vendor/codejar.js",
2024-02-14 23:31:39 +01:00
]
2024-03-07 21:21:02 +01:00
styles = ["page/tairu.css"]
2024-02-11 23:05:08 +01:00
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.
2024-02-11 23:05:08 +01:00
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPWPDBH6QQAZER7A05G"
2024-02-11 23:05:08 +01:00
- in Terraria, you can fully modify the terrain however you want, and the tiles will connect to each other seamlessly.
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPW8HE7681P7H686X4N"
2024-02-11 23:05:08 +01:00
- TODO: short videos demoing this here
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
2024-02-19 22:32:26 +01:00
- once upon a time I stumbled upon a technique called…
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA1KXZPBDWJXSCQA1D"
2024-02-18 23:37:31 +01:00
- ### bitwise autotiling
2024-02-11 23:05:08 +01:00
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPW6VK3FDW5QRCE6HSS"
2024-02-16 22:01:19 +01:00
+ I learned about it way back when I was just a kid building 2D Minecraft clones using [Construct 2](https://www.construct.net/en/construct-2/manuals/construct-2), and I wanted my terrain to look nice as it does in Terraria
2024-02-12 19:56:06 +01:00
% id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAMCPC5M88QAXHX4BT"
2024-02-18 23:37:31 +01:00
- so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look:
2024-02-14 23:31:39 +01:00
2024-07-22 20:34:42 +02:00
{% this could probably be written with some clever generator magic, but I'm too lazy for that %}
`<noscript>`{=html}
(…though you will need to enable JavaScript to try it out.
2024-02-20 23:30:36 +01:00
seriously, pinky promise I won't ever track you!
inspect the source code if you wanna.
if not, you will have to deal with static pictures.
2024-07-22 20:34:42 +02:00
but just keep in mind this was supposed to be an _*interactive*_ exploration of autotiling techniques.
cheers!)
`</noscript>`{=html}
2024-02-20 23:30:36 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
import { Tilemap } from "tairu/tilemap.js";
import { TileEditor } from "tairu/editor.js";
2024-02-14 23:31:39 +01:00
2024-02-18 23:37:31 +01:00
export const tilemapSquare = Tilemap.parse(" x", [
" ",
" xxx ",
" xxx ",
" xxx ",
" ",
]);
2024-02-14 23:31:39 +01:00
2024-02-18 23:37:31 +01:00
new TileEditor({
tilemap: tilemapSquare,
tileSize: 40,
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ47ZX7520PJNPJ75M793R5G}
```output
2024-02-18 23:37:31 +01:00
```
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAC3FN565QE3JAB87D"
2024-07-22 20:34:42 +02:00
- `Tilemap` is a class wrapping a flat [`Uint8Array`][Uint8Array] with a `width` and a `height`, so that we can index it using (x, y) coordinates.
2024-02-18 23:37:31 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
console.log(tilemapSquare.at(0, 0));
console.log(tilemapSquare.at(3, 1));
```
2024-07-22 20:34:42 +02:00
{:program=tairu}
```output
2024-02-20 23:30:36 +01:00
0
1
2024-02-18 23:37:31 +01:00
```
2024-07-22 20:34:42 +02:00
[Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA090YW5BR1XW68XJN"
2024-02-18 23:37:31 +01:00
- `at` has a `setAt` counterpart which sets tiles instead of getting them.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAMD68SY56P7TVT2DJ"
2024-02-18 23:37:31 +01:00
- `TileEditor` provides a graphical editor for a `Tilemap` based on a `<canvas>`.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWABTFQ0J83C4VZYZB5"
2024-02-18 23:37:31 +01:00
- 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.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA8Y1AD22MSN71V2E4"
2024-02-18 23:37:31 +01:00
- 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]
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAZV559ABQD1NVXPMA"
2024-02-18 23:37:31 +01:00
- let's break this down into smaller steps. drawing a border around the rectangle will involve:
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWATV30HXGBQVWERP2M"
2024-07-22 20:34:42 +02:00
- determining _on which tiles_ to draw it,
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAA0V0SS0D1Y38BDS1"
2024-07-22 20:34:42 +02:00
- determining _where in these tiles_ to draw it,
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAGBCBDYF4VH26MX1B"
2024-02-18 23:37:31 +01:00
- and actually drawing it!
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA2PNGVV075HR3WMER"
2024-07-22 20:34:42 +02:00
- so let's zoom in a bit and look at the tiles one by one. in particular, let's focus on _these_ two tiles:
2024-02-18 23:37:31 +01:00
![the same red rectangle, now with a focus on the northern tile at its center][pic:01HPYWPJB1P0GK53BSJFJFRAGR]
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAYDS6CSD3T102NA9X"
2024-07-22 20:34:42 +02:00
- notice how the two highlighted tiles are _different._ therefore, we can infer we should probably connect together any tiles that are _the same_.
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWATDD86D4GY7RMT0BZ"
2024-02-18 23:37:31 +01:00
- knowing that, we can extract the logic to a function:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
export function shouldConnect(a, b) {
return a == b;
}
```
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA9M6801Q0RNRSF09H"
2024-07-22 20:34:42 +02:00
+ 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`!).
2024-02-18 23:37:31 +01:00
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.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAM5YYQCEXH791T0E9"
2024-02-18 23:37:31 +01:00
- 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]?
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWABJ696HCJ09WDC0NX"
2024-02-18 23:37:31 +01:00
- 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.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWABNXV4N2AHZBQC5B7"
2024-02-18 23:37:31 +01:00
- 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:\
as `X` is first alphabetically, so checking `Y` first would feel wrong.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA5W8NXSXVZY3BBQ0H"
2024-02-18 23:37:31 +01:00
- to do that, I'm gonna override the tile editor's `drawTilemap` function - as this is where the actual tilemap rendering happens!
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
import { TileEditor } from "tairu/editor.js";
export class TileEditorWithBorders extends TileEditor {
constructor({ borderWidth, ...options }) {
super(options);
this.borderWidth = borderWidth;
this.colorScheme.borderColor = "#000000";
}
drawTilemap() {
// Let the base class render out the infill, we'll just handle the borders.
super.drawTilemap();
this.ctx.fillStyle = this.colorScheme.borderColor;
for (let y = 0; y < this.tilemap.height; ++y) {
for (let x = 0; x < this.tilemap.width; ++x) {
let tile = this.tilemap.at(x, y);
// We only want to draw non-empty tiles, so skip tile 0.
if (tile == 0) {
continue;
}
2024-07-22 20:34:42 +02:00
// Check which of this tile's neighbors should _not_ connect to it.
2024-02-18 23:37:31 +01:00
let disjointWithEast = !shouldConnect(tile, this.tilemap.at(x + 1, y));
let disjointWithSouth = !shouldConnect(tile, this.tilemap.at(x, y + 1));
let disjointWithWest = !shouldConnect(tile, this.tilemap.at(x - 1, y));
let disjointWithNorth = !shouldConnect(tile, this.tilemap.at(x, y - 1));
let { borderWidth, tileSize } = this;
let tx = x * tileSize;
let ty = y * tileSize;
// For each disjoint neighbor, we want to draw a border between us and them.
if (disjointWithEast) {
this.ctx.fillRect(tx + tileSize - borderWidth, ty, borderWidth, tileSize);
}
if (disjointWithSouth) {
this.ctx.fillRect(tx, ty + tileSize - borderWidth, tileSize, borderWidth);
}
if (disjointWithWest) {
this.ctx.fillRect(tx, ty, borderWidth, tileSize);
}
if (disjointWithNorth) {
this.ctx.fillRect(tx, ty, tileSize, borderWidth);
}
}
}
}
}
```
and here's the result:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
new TileEditorWithBorders({
tilemap: tilemapSquare,
tileSize: 40,
borderWidth: 4,
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ49TJZFMK719KSE16SG3F7B}
```output
2024-02-18 23:37:31 +01:00
```
2024-02-19 22:32:26 +01:00
% 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!
2024-02-18 23:37:31 +01:00
instead, the more common way is to use a tileset - a big texture with a bunch of sprites to use for rendering each tile.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWACD5CD7GCZE53ZPD7"
2024-02-18 23:37:31 +01:00
- 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.
2024-02-14 23:31:39 +01:00
% template = true
classes.branch = "tileset-cardinal-directions-demo"
2024-02-19 22:32:26 +01:00
id = "01HQ162WWAADKPDQE69W3QZG0M"
2024-02-18 23:37:31 +01:00
- 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]
[Planet Overgamma]: https://github.com/liquidev/planet-overgamma
% classes.branch = "tileset-cardinal-directions-demo"
2024-02-19 22:32:26 +01:00
id = "01HQ162WWAS502000K8QZWVBDW"
2024-02-18 23:37:31 +01:00
- 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.
2024-07-22 20:34:42 +02:00
::: horizontal-tile-strip
[]{.metal .x-0 .y-0}
[]{.metal .x-1 .y-0}
[]{.metal .x-2 .y-0}
[]{.metal .x-3 .y-0}
[]{.metal .x-0 .y-1}
[]{.metal .x-1 .y-1}
[]{.metal .x-2 .y-1}
[]{.metal .x-3 .y-1}
[]{.metal .x-0 .y-2}
[]{.metal .x-1 .y-2}
[]{.metal .x-2 .y-2}
[]{.metal .x-3 .y-2}
[]{.metal .x-0 .y-3}
[]{.metal .x-1 .y-3}
[]{.metal .x-2 .y-3}
[]{.metal .x-3 .y-3}
:::
2024-02-18 23:37:31 +01:00
% classes.branch = "tileset-cardinal-directions-demo"
2024-02-19 22:32:26 +01:00
id = "01HQ162WWANBTYH1JJWCTZYYVN"
2024-02-18 23:37:31 +01:00
- the keen eyed among you have probably noticed that this is very similar to the case we had before with drawing procedural borders -
2024-07-22 20:34:42 +02:00
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!
::: horizontal-tile-strip
[[E]{.east} [S]{.south}]{.metal .x-0 .y-0}
[[E]{.east} [S]{.south} [W]{.west}]{.metal .x-1 .y-0}
[[S]{.south} [W]{.west}]{.metal .x-2 .y-0}
[[S]{.south}]{.metal .x-3 .y-0}
[[E]{.east} [S]{.south} [N]{.north}]{.metal .x-0 .y-1}
[[E]{.east} [S]{.south} [W]{.west} [N]{.north}]{.metal .x-1 .y-1}
[[S]{.south} [W]{.west} [N]{.north}]{.metal .x-2 .y-1}
[[S]{.south} [N]{.north}]{.metal .x-3 .y-1}
[[E]{.east} [N]{.north}]{.metal .x-0 .y-2}
[[E]{.east} [W]{.west} [N]{.north}]{.metal .x-1 .y-2}
[[W]{.west} [N]{.north}]{.metal .x-2 .y-2}
[[N]{.north}]{.metal .x-3 .y-2}
[[E]{.east}]{.metal .x-0 .y-3}
[[E]{.east} [W]{.west}]{.metal .x-1 .y-3}
[[W]{.west}]{.metal .x-2 .y-3}
[]{.metal .x-3 .y-3}
:::
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA4Z6KKWFV59BR4WD3"
2024-02-18 23:37:31 +01:00
- previously we represented which single border to draw with a single boolean.
2024-07-22 20:34:42 +02:00
now we will represent which single tile to draw with _four_ booleans, because each tile can connect to four different directions.
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAQ9GZ6JD8KESW4N53"
2024-07-22 20:34:42 +02:00
- 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!
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAMBM8RXKQTN3D0XR2"
2024-07-22 20:34:42 +02:00
- 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?
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA0ZGZ97JZZBFS41TF"
2024-02-18 23:37:31 +01:00
- 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.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAQQ99TRBDY5DCSW3Z"
2024-02-18 23:37:31 +01:00
- say we arrange our bits like this:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
export const E = 0b0001;
export const S = 0b0010;
export const W = 0b0100;
export const N = 0b1000;
```
2024-02-14 23:31:39 +01:00
% classes.branch = "tileset-cardinal-directions-demo"
2024-02-19 22:32:26 +01:00
id = "01HQ162WWABANND0WGT933TBMV"
2024-02-18 23:37:31 +01:00
- 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`):
2024-02-14 23:31:39 +01:00
2024-07-22 20:34:42 +02:00
::: horizontal-tile-strip
[]{.metal .x-3 .y-3}
[[E]{.east}]{.metal .x-0 .y-3}
[[S]{.south}]{.metal .x-3 .y-0}
[[E]{.east} [S]{.south}]{.metal .x-0 .y-0}
[[W]{.west}]{.metal .x-2 .y-3}
[[W]{.west} [E]{.east}]{.metal .x-1 .y-3}
[[W]{.west} [S]{.south}]{.metal .x-2 .y-0}
[[W]{.west} [E]{.east} [S]{.south}]{.metal .x-1 .y-0}
[[N]{.north}]{.metal .x-3 .y-2}
[[E]{.east} [N]{.north}]{.metal .x-0 .y-2}
[[S]{.south} [N]{.north}]{.metal .x-3 .y-1}
[[E]{.east} [S]{.south} [N]{.north}]{.metal .x-0 .y-1}
[[W]{.west} [N]{.north}]{.metal .x-2 .y-2}
[[E]{.east} [W]{.west} [N]{.north}]{.metal .x-1 .y-2}
[[S]{.south} [W]{.west} [N]{.north}]{.metal .x-2 .y-1}
[[E]{.east} [S]{.south} [W]{.west} [N]{.north}]{.metal .x-1 .y-1}
:::
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAJPW00XA25N0K6KS7"
2024-07-22 20:34:42 +02:00
- packing that into a single tileset, or rather this time, a _tile strip_, we get this image:
2024-02-14 23:31:39 +01:00
![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5]
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAT2ZC7T2P9ATD6WG2"
2024-07-22 20:34:42 +02:00
- 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.
2024-02-18 23:37:31 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA0NRHBB6HP2RERNBK"
2024-02-18 23:37:31 +01:00
- 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.
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA9PGSHH5E97RVE1PB"
2024-02-18 23:37:31 +01:00
- just imagine some game where glass connects to metal, but metal doesn't connect to glass - I bet that would look pretty great!
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAYJ4JCG3Z24SJR8S9"
2024-02-18 23:37:31 +01:00
- …but anyways, here's the basic bitwise magic function:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
export function tileIndexInBitwiseTileset(tilemap, x, y) {
let tile = tilemap.at(x, y);
let tileIndex = 0;
tileIndex |= shouldConnect(tile, tilemap.at(x + 1, y)) ? E : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x, y + 1)) ? S : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x - 1, y)) ? W : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x, y - 1)) ? N : 0;
return tileIndex;
}
```
% template = true
2024-02-19 22:32:26 +01:00
id = "01HQ162WWAS813ANMBG1PWDZHC"
2024-02-18 23:37:31 +01:00
- 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:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
// You'll probably want to host the assets on your own website rather than
// hotlinking to others. It helps longevity!
2024-02-19 22:32:26 +01:00
let tilesetImage = new Image();
2024-02-18 23:37:31 +01:00
tilesetImage.src = "{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}";
export const heavyMetalTileset = {
image: tilesetImage,
tileSize: 8,
tileIndex: tileIndexInBitwiseTileset,
};
```
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA0SC2GA7Y3KJE0W5F"
2024-02-18 23:37:31 +01:00
- with all that, we should now be able to write a tile renderer which can handle textures! so let's try it:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
import { TileEditor } from "tairu/editor.js";
export class TilesetTileEditor extends TileEditor {
constructor({ tilesets, ...options }) {
super(options);
this.tilesets = tilesets;
// The image may not be loaded once the editor is first drawn, so we need to request a
// redraw for each image that gets loaded in.
for (let tileset of this.tilesets) {
tileset.image.addEventListener("load", () => this.draw());
}
}
drawTilemap() {
// We're dealing with pixel tiles so we want our images to be pixelated,
// not interpolated.
this.ctx.imageSmoothingEnabled = false;
for (let y = 0; y < this.tilemap.height; ++y) {
for (let x = 0; x < this.tilemap.width; ++x) {
let tile = this.tilemap.at(x, y);
if (tile == 0) {
continue;
}
// Subtract one from the tile because tile 0 is always empty.
// Having to specify a null entry at array index 0 would be pretty annoying.
let tileset = this.tilesets[tile - 1];
if (tileset != null) {
let { tileSize } = this;
let tileIndex = tileset.tileIndex(this.tilemap, x, y);
this.ctx.drawImage(
tileset.image,
tileIndex * tileset.tileSize, 0, tileset.tileSize, tileset.tileSize,
x * tileSize, y * tileSize, tileSize, tileSize,
);
}
}
2024-02-14 23:31:39 +01:00
}
}
}
```
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAS2HYF41MZNJ18BXC"
- drum roll please…
2024-02-18 23:37:31 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
new TilesetTileEditor({
tilemap: tilemapSquare,
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ49X8Z57FNMN3E79FYF8CMG}
```output
2024-02-18 23:37:31 +01:00
```
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA03JAGJYCT0DRZP24"
2024-02-18 23:37:31 +01:00
- it works! buuuut if you play around with it you'll quickly start noticing some problems:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-18 23:37:31 +01:00
import { Tilemap } from "tairu/tilemap.js";
export const tilemapEdgeCase = Tilemap.parse(" x", [
" ",
" xxx ",
" x x ",
" xxx ",
" ",
]);
new TilesetTileEditor({
tilemap: tilemapEdgeCase,
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ49YDPQXYSAT5N6P241DG3C}
```output
2024-02-18 23:37:31 +01:00
```
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWAB0AYSPGB4AEVT03Z"
2024-02-18 23:37:31 +01:00
- where did our nice seamless connections go!?
2024-02-17 21:03:45 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWA3Q095ZGXDFZ1V2Q1"
- ### thing is, it was never good in the first place
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ162WWARSVDRNHZE13ZF6W6"
2024-07-22 20:34:42 +02:00
- I'll be blunt: we don't have enough tiles to represent _corners_! like in this case:
2024-02-11 23:05:08 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
import { Tilemap } from "tairu/tilemap.js";
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" xx ",
" x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ49Z8JWR75D85DGHCB34K8E}
```output
2024-02-19 22:32:26 +01:00
```
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39AS4VDW7DVTAGQ03WFM"
- have a closer look at the top-left tile:
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
![the above example, showing an L shape rotated 180°, with the top left corner highlighted][pic:01HQ167GJEPTKHAKAVNW3WN1SZ]
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39AS6Y9XMJTMMQYTWRMC"
- it should kind of _"bend"_ to fit in with the tiles to the north and the south, but it doesn't :kamien:
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASQQNF7B881SYJWRC7"
2024-07-22 20:34:42 +02:00
- so what if we made the tile look like _this_ instead:
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
![mockup showing that previous L-shape but with a real corner][pic:01HQ17GYEZSZCVRBFHP4HXAJV8]
2024-02-14 23:31:39 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASMKRMTXFV93FRHZTG"
- that sure as heck looks a lot nicer! but there's a problem: that tile, let's zoom in on it…
2024-02-14 23:31:39 +01:00
2024-07-22 20:34:42 +02:00
![that bent tile, and just _it_ alone][pic:01HQ183RANGH4S7VZSG1ZGH0S5]
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASR81NWMW8Q0MF8QMP"
- enhance!
2024-07-22 20:34:42 +02:00
{% NOTE djot: I don't there's a way to achieve this in Djot alone %}
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square bend">
<li class="east">E</li>
<li class="south">S</li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASC5WTR2A2AJN85JK2"
- huh. interesting. it connects to the east and the south. so what about this tile -
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south">S</li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASXYBH9QJH5Q0C45JZ"
2024-07-22 20:34:42 +02:00
- because it _also_ connects to the east and the south :thinking:
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASW5PWS52NGA2X3M0P"
2024-07-22 20:34:42 +02:00
- seems like we'll need something to disambiguate the two cases - and what better thing to disambiguate with than _more bits_!
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% classes.branch = "tileset-four-to-eight-demo"
id = "01HPQCCV4R5N97FJ1GS36HZJZ7"
- to represent the corners, we'll turn our four cardinal directions…
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square">
<li class="east">E</li>
<li class="south">S</li>
<li class="west">W</li>
<li class="north">N</li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
into eight _ordinal_ directions:
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square">
<li class="east">E</li>
<li class="south-east">SE</li>
<li class="south">S</li>
<li class="south-west">SW</li>
<li class="west">W</li>
<li class="north-west">NW</li>
<li class="north">N</li>
<li class="north-east"><a href="https://github.com/NoiseStudio/NoiseEngine/" title="NoiseEngine????">NE</a></li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASFN94YDY1RWQYS12K"
- at this point with the four extra corners we'll need 8 bits to represent our tiles, and that would make…
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
_*256 tiles!?*_
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
nobody in their right mind would actually draw 256 separate tiles, right? _*RIGHT???*_
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% template = true
id = "01HQ1K39AS11M1M4GQQ60NXTY6"
- …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!
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" x x ",
" x ",
" x x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ4A01MPE6JT5ZZFEN9S635W}
```output
2024-02-19 22:32:26 +01:00
```
% id = "01HQ1K39AS7CRBZ67N1VVHCVME"
2024-07-22 20:34:42 +02:00
- therefore there must be _some_ bit combinations that are redundant to others. let's find them!
2024-02-19 22:32:26 +01:00
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASZPJ4E23EZ1XJ5J7K"
- let's pick one corner first, then generalize to all the other ones. I pick southeast!
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south-east">SE</li>
<li class="south">S</li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASQTR054W0VWEAV2FS"
- in this case, if we remove the tile to the southeast, we get that bent tile from before:
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<ul class="directions-square bend">
2024-02-16 22:01:19 +01:00
<li class="east">E</li>
<li class="south">S</li>
</ul>
2024-07-22 20:34:42 +02:00
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39AS6RGE6Z83T8MH1R0M"
- what we can learn from this is that for `E | S`, `ES` affects the result!
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASVSAQ6F8ANEZE1WQ4"
- but if we add any other corner, nothing changes. heck, let's add all of them:
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
``` =html
2024-02-19 22:32:26 +01:00
<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>
2024-07-22 20:34:42 +02:00
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% 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"
- 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"
+ 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.
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
export const E = 0b0000_0001;
export const SE = 0b0000_0010;
export const S = 0b0000_0100;
export const SW = 0b0000_1000;
export const W = 0b0001_0000;
export const NW = 0b0010_0000;
export const N = 0b0100_0000;
export const NE = 0b1000_0000;
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19HPNXC54VP6TFFHXN"
- 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:
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
export function isSet(integer, bit) {
return (integer & bit) == bit;
}
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y1984H2FX6QY6K2KHKF"
- 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.
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
// t is an existing tile index; variable name is short for brevity
export function removeRedundancies(t) {
if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) {
t &= ~SE;
2024-02-16 22:01:19 +01:00
}
2024-02-19 22:32:26 +01:00
if (isSet(t, SW) && (!isSet(t, S) || !isSet(t, W))) {
t &= ~SW;
2024-02-16 22:01:19 +01:00
}
2024-02-19 22:32:26 +01:00
if (isSet(t, NW) && (!isSet(t, N) || !isSet(t, W))) {
t &= ~NW;
}
if (isSet(t, NE) && (!isSet(t, N) || !isSet(t, E))) {
t &= ~NE;
}
return t;
}
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19HWQQ9XBW1DDGW68T"
- with that, we can find a set of all unique non-redundant combinations:
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
export function ordinalDirections() {
let unique = new Set();
for (let i = 0; i <= 0b1111_1111; ++i) {
unique.add(removeRedundancies(i));
2024-02-16 22:01:19 +01:00
}
2024-02-19 22:32:26 +01:00
return Array.from(unique).sort((a, b) => a - b);
}
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19KG8DC4SYXR1DJJ5F"
2024-07-22 20:34:42 +02:00
- by the way, I find it quite funny how JavaScript's [`Array.prototype.sort`][sort] defaults to ASCII ordering _for all types._
2024-02-19 22:32:26 +01:00
even numbers! ain't that silly?
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
[sort]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19V62YKTGK3TTKEB38"
- and with all the ingredients in the pot, we now _Let It Cook™_:
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
let dirs = ordinalDirections();
console.log(dirs.length);
```
2024-02-16 22:01:19 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```output
2024-02-19 22:32:26 +01:00
47
```
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y194DYYDGSAT83MPQFR"
- forty seven! that's how many unique tiles we actually need.
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19C303Z595KNVXYYVS"
2024-07-22 20:34:42 +02:00
- you may find pixel art tutorials saying you need forty _eight_ and not forty _seven_, but that is not quite correct -
2024-02-19 22:32:26 +01:00
the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO.
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19TM2K2WN06HHEM3D0"
- phew… the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling!
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% 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!
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19HEBWBTNMDMM0AZSC"
- and it's even possible to autogenerate most of them given just a few smaller 4x4 pieces - but for now, let's not go down that path.\
maybe another time.
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y047YGYAP6XQXJ3576"
- so we only need to draw 47 tiles, but to actually display them in a game we still need to pack them into an image.
2024-02-17 14:56:17 +01:00
2024-02-18 00:29:58 +01:00
% id = "01HPWJB4Y0QX6YR6TQKZ7T1C2E"
2024-07-22 20:34:42 +02:00
- we _could_ use a similar approach to the 16 tile version, but that would leave us with lots of wasted space!
2024-02-17 14:56:17 +01:00
2024-02-18 00:29:58 +01:00
% id = "01HPWJB4Y0HKGSDABB56CNFP9H"
2024-02-17 14:56:17 +01:00
- 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.
2024-02-19 22:32:26 +01:00
% id = "01HQ1K39ASM53P1E74HKRZ1T24"
2024-07-22 20:34:42 +02:00
- 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.
2024-02-17 14:56:17 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y0F9JGXQDAAVC3ERG1"
- I don't want to write the lookup table by hand, so let's generate it!
2024-02-17 14:56:17 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y0HTV32T4WMKCKWTVA"
- we'll start by obtaining our ordinal directions array again:
2024-02-17 14:56:17 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
export let xToConnectionBitSet = ordinalDirections();
```
2024-02-17 14:56:17 +01:00
2024-02-19 22:32:26 +01:00
% 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.
2024-02-17 14:56:17 +01:00
2024-02-19 22:32:26 +01:00
remember that our array has only 256 values, so it should be pretty cheap to represent using a [`Uint8Array`]:
2024-02-17 14:56:17 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
export let connectionBitSetToX = new Uint8Array(256);
for (let i = 0; i < xToConnectionBitSet.length; ++i) {
connectionBitSetToX[xToConnectionBitSet[i]] = i;
}
```
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
% 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!
2024-02-17 14:56:17 +01:00
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
console.log(connectionBitSetToX[E | SE | S]);
```
2024-07-22 20:34:42 +02:00
{:program=tairu}
```output
2024-02-19 22:32:26 +01:00
4
```
2024-02-17 14:56:17 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y09P9Q3NGN59XWX2X9"
2024-07-22 20:34:42 +02:00
+ for my own (and your) convenience, here's a complete list of _all_ the possible combinations in order.
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y01VJFMHYEC1WZ353W"
2024-07-22 20:34:42 +02:00
- {:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
function toString(bitset) {
if (bitset == 0) return "0";
let directions = [];
if (isSet(bitset, E)) directions.push("E");
if (isSet(bitset, SE)) directions.push("SE");
if (isSet(bitset, S)) directions.push("S");
if (isSet(bitset, SW)) directions.push("SW");
if (isSet(bitset, W)) directions.push("W");
if (isSet(bitset, NW)) directions.push("NW");
if (isSet(bitset, N)) directions.push("N");
if (isSet(bitset, NE)) directions.push("NE");
return directions.join(" | ");
}
for (let x in xToConnectionBitSet) {
console.log(`${x} => ${toString(xToConnectionBitSet[x])}`);
}
2024-02-17 14:56:17 +01:00
```
2024-07-22 20:34:42 +02:00
{:program=tairu}
```output
2024-02-19 22:32:26 +01:00
0 => 0
1 => E
2 => S
3 => E | S
4 => E | SE | S
5 => W
6 => E | W
7 => S | W
8 => E | S | W
9 => E | SE | S | W
10 => S | SW | W
11 => E | S | SW | W
12 => E | SE | S | SW | W
13 => N
14 => E | N
15 => S | N
16 => E | S | N
17 => E | SE | S | N
18 => W | N
19 => E | W | N
20 => S | W | N
21 => E | S | W | N
22 => E | SE | S | W | N
23 => S | SW | W | N
24 => E | S | SW | W | N
25 => E | SE | S | SW | W | N
26 => W | NW | N
27 => E | W | NW | N
28 => S | W | NW | N
29 => E | S | W | NW | N
30 => E | SE | S | W | NW | N
31 => S | SW | W | NW | N
32 => E | S | SW | W | NW | N
33 => E | SE | S | SW | W | NW | N
34 => E | N | NE
35 => E | S | N | NE
36 => E | SE | S | N | NE
37 => E | W | N | NE
38 => E | S | W | N | NE
39 => E | SE | S | W | N | NE
40 => E | S | SW | W | N | NE
41 => E | SE | S | SW | W | N | NE
42 => E | W | NW | N | NE
43 => E | S | W | NW | N | NE
44 => E | SE | S | W | NW | N | NE
45 => E | S | SW | W | NW | N | NE
46 => E | SE | S | SW | W | NW | N | NE
2024-02-17 14:56:17 +01:00
```
2024-02-19 22:32:26 +01:00
% id = "01HPWJB4Y0NMP35M9138DV3P8W"
- with the lookup table generated, we are now able to prepare a tile strip like before - except now it's even more tedious work arranging the pieces together :ralsei_dead:
2024-02-17 21:03:45 +01:00
2024-02-19 22:32:26 +01:00
anyways I spent like 20 minutes doing that by hand, and now we have a neat tile strip just like before, except way longer:
2024-02-17 21:03:45 +01:00
2024-02-19 22:32:26 +01:00
![horizontal tile strip of 47 8x8 pixel metal tiles][pic:01HPW47SHMSVAH7C0JR9HWXWCM]
% id = "01HPWJB4Y0J3DHQV5F9GD3VNQ8"
- 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:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
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
id = "01HQ1M84GS4C99VQZC4150CMDS"
- now we can write a new tileset descriptor that uses this indexing function and the larger tile strip:
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
// 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…
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" x ",
" x x ",
" xxx ",
" xx ",
" x ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ4A11RRXEQ850598GFBJN0B}
```output
2024-02-19 22:32:26 +01:00
```
% id = "01HQ1M84GSCXTPGVPXY840WCQ6"
- it works perfectly!
% id = "01HQ1M84GSVBG9T94ZN9XTXX58"
2024-07-22 20:34:42 +02:00
- but honestly, this is a bit _boring_ if we're gonna build a game with procedural worlds.
2024-02-19 22:32:26 +01:00
% 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"
2024-02-20 22:36:47 +01:00
- and a better way… there is! but I'll get to that once my nap is over.
2024-02-19 22:32:26 +01:00
% id = "01HQ1M84GS0KJ9NA6GPS62RC95"
- for now, have a big editor to play around with. it's a lot of fun arranging the tiles in various shapes!
2024-07-22 20:34:42 +02:00
{:program=tairu}
```javascript
2024-02-19 22:32:26 +01:00
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: new Tilemap(25, 16),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
2024-07-22 20:34:42 +02:00
{:program=tairu :placeholder=01HQ4A45WNAEJGCT2WDMQJHK14}
```output
2024-02-19 22:32:26 +01:00
```
2024-02-20 22:36:47 +01:00
:nap: <!--
2024-02-19 22:32:26 +01:00
2024-02-20 22:36:47 +01:00
% stage = "Draft"
2024-02-19 22:32:26 +01:00
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.
2024-02-17 21:03:45 +01:00
2024-02-11 23:05:08 +01:00
2024-02-12 19:56:06 +01:00
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWPN6HNA6M6EH507C6"
+ but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit)
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPW11EQTBDQSGXW3S52"
+ (of course it's really cool, after all [rxi](https://github.com/rxi) made it)
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWYHS327BV586SB085"
- for context rxi is the genius behind the Lua-powered, simple, and modular text editor [lite](https://github.com/rxi/lite) that I was using for quite a while
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWJ9QAQ5MF2J5JBB8M"
- after a while I switched to a fork - [Lite XL](https://github.com/lite-xl/lite-xl), which had better font rendering and more features
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWB11TZSX5VAAJ6TCD"
2024-07-22 20:34:42 +02:00
- I stopped using it because VS Code was just more feature packed and usable; no need to reinvent the wheel, rust-analyzer _just works._
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPW3G7BXTBBTD05MB8V"
- the LSP plugin for Lite XL had some issues around autocompletions not filling in properly :pensive:
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
it's likely a lot better now, but back then I decided this is too much for my nerves.
while tinkering with your editor is something really cool, in my experience it's only cool up to a point.
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWV1BAPA27SNDFR93B"
2024-07-22 20:34:42 +02:00
- the cool thing with Tilekit is that it's _more_ than just your average bitwise autotiling - of course it _can_ do basic autotiling, but it can also do so much more
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWM1JSAPXVT6NBHKYY"
classes.branch_children = "branch-quote"
2024-07-22 20:34:42 +02:00
- if I had to describe it, it's basically something of a _shader langauge for tilesets._ this makes it really powerful, as you can do little programs like
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWE7ZVR0SS67DHTGHQ"
- autotile using this base tileset
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPW2BFZYQQ920SYHM9M"
- if the tile above is empty AND with a 50% chance
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWJB7V67TS1M3HFCYE"
- then grass
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWF7K85Z0CEK4WDDBZ"
- if the tile above is solid AND with a 10% chance
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPW5J3N6MVT9Z2W00S9"
- then vines
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWGCMCEAR5Z9EETSGP"
- if the tile above is vines AND with a 50% chance
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPD4XQPWP847T0EAM0FJ88T4"
- then vines
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPSY4Y19FA2HGYE4F3Y9NJ57"
- well… it's even simpler than that in terms of graphical presentation, but we'll get to that.
2024-02-16 22:01:19 +01:00
2024-02-19 22:32:26 +01:00
% 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?
2024-02-11 23:05:08 +01:00
2024-02-19 22:32:26 +01:00
% id = "01HPJ8GHDFRA2SPNHKJYD0SYPP"
- of course Tilekit's solution is a lot more simple, streamlined, and user-friendly, but you get the gist.