589 lines
28 KiB
Plaintext
589 lines
28 KiB
Plaintext
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
|
|
scripts = [
|
|
"components/literate-programming.js",
|
|
"tairu/cardinal-directions.js",
|
|
"tairu/framework.js",
|
|
"tairu/tairu.js",
|
|
"tairu/tilemap-registry.js",
|
|
"tairu/tilemap.js",
|
|
"vendor/codejar.js",
|
|
]
|
|
styles = ["tairu.css"]
|
|
|
|
% id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
|
|
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.
|
|
|
|
% id = "01HPD4XQPWPDBH6QQAZER7A05G"
|
|
- in Terraria, you can fully modify the terrain however you want, and the tiles will connect to each other seamlessly.
|
|
|
|
% id = "01HPD4XQPW8HE7681P7H686X4N"
|
|
- TODO: short videos demoing this here
|
|
|
|
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
|
|
- once upon a time I stumbled upon a technique called...\
|
|
**bitwise autotiling**
|
|
|
|
% id = "01HPD4XQPW6VK3FDW5QRCE6HSS"
|
|
+ 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
|
|
|
|
% id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
|
|
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
|
|
|
|
% id = "01HPJ8GHDET8ZGNN0AH3FWA8HX"
|
|
- let's begin with a tilemap. say we have the following grid of tiles: (the examples are interactive, try editing it!)
|
|
|
|
<canvas
|
|
is="tairu-editor"
|
|
data-tilemap-id="bitwiseAutotiling"
|
|
data-tile-size="40">
|
|
Your browser does not support <canvas>.
|
|
</canvas>
|
|
|
|
% id = "01HPJ8GHDEC0Z334M04MTNADV9"
|
|
- for each tile we can assign a bitset of cardinal directions, based on which tiles it should connect to - like so:
|
|
|
|
<canvas
|
|
is="tairu-editor-cardinal-directions"
|
|
data-tilemap-id="bitwiseAutotiling"
|
|
data-tile-size="40">
|
|
Your browser does not support <canvas>.
|
|
</canvas>
|
|
|
|
% template = true
|
|
id = "01HPJ8GHDE9QKQ4QFZK1Z1KQD4"
|
|
classes.branch = "tileset-cardinal-directions-demo"
|
|
+ now given a tileset, such as the one below that I drew a while ago, we can assign each tile to a set of cardinal directions.
|
|
I'll indicate where there's a connection between individual tiles with the letters **N**, **E**, **S**, **W**, standing for the cardinal directions **N**orth, **E**ast, **S**outh, and **W**est.
|
|
|
|
<ul class="tileset-demo">
|
|
<li class="full-image">
|
|
<img alt="a 16-tile tileset of 8x8 pixel metal" src="{% pic 01HPHVDRV0F0251MD0A2EG66C4 %}">
|
|
</li>
|
|
<li class="tileset-pieces">
|
|
<span class="metal x-0 y-0"><span class="east">E</span><span class="south">S</span></span>
|
|
<span class="metal x-1 y-0"><span class="east">E</span><span class="south">S</span><span class="west">W</span></span>
|
|
<span class="metal x-2 y-0"><span class="south">S</span><span class="west">W</span></span>
|
|
<span class="metal x-3 y-0"><span class="south">S</span></span>
|
|
<span class="metal x-0 y-1"><span class="east">E</span><span class="south">S</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>
|
|
<span class="metal x-2 y-1"><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
|
|
<span class="metal x-3 y-1"><span class="south">S</span><span class="north">N</span></span>
|
|
<span class="metal x-0 y-2"><span class="east">E</span><span class="north">N</span></span>
|
|
<span class="metal x-1 y-2"><span class="east">E</span><span class="west">W</span><span class="north">N</span></span>
|
|
<span class="metal x-2 y-2"><span class="west">W</span><span class="north">N</span></span>
|
|
<span class="metal x-3 y-2"><span class="north">N</span></span>
|
|
<span class="metal x-0 y-3"><span class="east">E</span></span>
|
|
<span class="metal x-1 y-3"><span class="east">E</span><span class="west">W</span></span>
|
|
<span class="metal x-2 y-3"><span class="west">W</span></span>
|
|
<span class="metal x-3 y-3"></span>
|
|
</li>
|
|
</ul>
|
|
|
|
% id = "01HPMVT9BM65YD5AXWPT4Z67H5"
|
|
- (it's frustratingly hard to center individual letters like this in CSS. please forgive me for how crooked these are!)
|
|
|
|
% id = "01HPMVT9BM5V4BP8K80X0C1HJZ"
|
|
- note that the state of connection for a given cardinal direction can be represented using two values: **connected**, and **not connected**.
|
|
two values make one bit, so we can pack these four connection states into four bits, and use that as an array index!
|
|
|
|
% classes.branch = "tileset-cardinal-directions-demo"
|
|
id = "01HPMVT9BM4AXG2Z1D2QBH828G"
|
|
+ for that to work though, we need to rearrange our tilemap somewhat such that we can index into it easily using our integer.
|
|
assuming we pack our bits as `NWSE` (bit 0 is east, each next bit we go clockwise),
|
|
therefore the final arrangement is this:
|
|
|
|
<div class="horizontal-tile-strip">
|
|
<span class="metal x-3 y-3"></span>
|
|
<span class="metal x-0 y-3"><span class="east">E</span></span>
|
|
<span class="metal x-3 y-0"><span class="south">S</span></span>
|
|
<span class="metal x-0 y-0"><span class="east">E</span><span class="south">S</span></span>
|
|
<span class="metal x-2 y-3"><span class="west">W</span></span>
|
|
<span class="metal x-1 y-3"><span class="east">E</span><span class="west">W</span></span>
|
|
<span class="metal x-2 y-0"><span class="south">S</span><span class="west">W</span></span>
|
|
<span class="metal x-1 y-0"><span class="east">E</span><span class="south">S</span><span class="west">W</span></span>
|
|
<span class="metal x-3 y-2"><span class="north">N</span></span>
|
|
<span class="metal x-0 y-2"><span class="east">E</span><span class="north">N</span></span>
|
|
<span class="metal x-3 y-1"><span class="south">S</span><span class="north">N</span></span>
|
|
<span class="metal x-0 y-1"><span class="east">E</span><span class="south">S</span><span class="north">N</span></span>
|
|
<span class="metal x-2 y-2"><span class="west">W</span><span class="north">N</span></span>
|
|
<span class="metal x-1 y-2"><span class="east">E</span><span class="west">W</span><span class="north">N</span></span>
|
|
<span class="metal x-2 y-1"><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>
|
|
|
|
packing that into a single tilesheet, or rather tile *strip*, we get this image:
|
|
|
|
![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5]
|
|
|
|
% id = "01HPQCCV4RB65D5Q4RANJKGC0D"
|
|
- **hint:** you can actually just use the original image, but use a lookup table from these indices to (x, y) coordinates.
|
|
this makes creating the assets a lot easier! (at the expense of some CPU time, though it is totally possible to offload tilemap rendering to the GPU - in that case it barely even matters.)
|
|
|
|
% id = "01HPMVT9BMMEM4HT4ANZ40992P"
|
|
- in JavaScript, drawing on a `<canvas>` using bitwise autotiling would look like this:
|
|
```javascript
|
|
for (let y = 0; y < tilemap.height; ++y) {
|
|
for (let x = 0; x < tilemap.width; ++x) {
|
|
// Assume `tilemap.at` is a function which returns the type of tile
|
|
// stored at coordinates (x, y).
|
|
let tile = tilemap.at(x, y);
|
|
|
|
// We need to treat *some* tile as an empty (fully transparent) tile.
|
|
// In our case that'll be 0.
|
|
if (tile != 0) {
|
|
let tileset = tilesets[tile];
|
|
|
|
// Now it's time to represent the tile connections as bits.
|
|
// For each cardinal direction we produce a different bit value, or 0 if there is
|
|
// no connection:
|
|
let connectedWithEast = shouldConnect(tile, tilemap.at(x + 1, y)) ? 0b0001 : 0;
|
|
let connectedWithSouth = shouldConnect(tile, tilemap.at(x, y + 1)) ? 0b0010 : 0;
|
|
let connectedWithWest = shouldConnect(tile, tilemap.at(x - 1, y)) ? 0b0100 : 0;
|
|
let connectedWithNorth = shouldConnect(tile, tilemap.at(x, y - 1)) ? 0b1000 : 0;
|
|
// Then we OR them together into one integer.
|
|
let tileIndex = connectedWithNorth
|
|
| connectedWithWest
|
|
| connectedWithSouth
|
|
| connectedWithEast;
|
|
|
|
// With that, we can draw the correct tile.
|
|
// Our strip is a single horizontal line, so we can assume
|
|
let tilesetTileSize = tileset.height;
|
|
let tilesetX = tileIndex * tilesetTileSize;
|
|
let tilesetY = 0;
|
|
ctx.drawImage(
|
|
tilesets[tile],
|
|
tilesetX, tilesetY, tilesetTileSize, tilesetTileSize,
|
|
x * tileSize, y * tileSize, tileSize, tileSize,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
TODO this should be literate code
|
|
|
|
% template = true
|
|
id = "01HPMVT9BM9CS9375MX4H9WKW8"
|
|
- and that gives us this result:
|
|
|
|
<canvas
|
|
is="tairu-editor"
|
|
data-tilemap-id="bitwiseAutotiling"
|
|
data-tile-size="40"
|
|
>
|
|
Your browser does not support <canvas>.
|
|
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
|
|
</canvas>
|
|
|
|
% id = "01HPMVT9BM3WR0BNZFHP2BPZ8A"
|
|
- but if you play around with it (or have *already* played around with it, and are therefore left with a non-default tilemap)
|
|
|
|
...something seems awful about it doesn't it?
|
|
|
|
% template = true
|
|
id = "01HPMVT9BMPA89037VPWPPWX8V"
|
|
- something's off about the corners. let me give you a fresh example to illustrate what I mean:
|
|
|
|
<canvas
|
|
is="tairu-editor"
|
|
data-tilemap-id="bitwiseAutotilingChapter2"
|
|
data-tile-size="40"
|
|
>
|
|
Your browser does not support <canvas>.
|
|
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
|
|
</canvas>
|
|
|
|
% id = "01HPMVT9BM16EF3TV5J1K19JAM"
|
|
+ see that tile in the bottom left corner of the `L` shape? it's missing a corner.
|
|
the top-right corner, to be exact, which makes it visually disjoint from the tiles to the north and the east.
|
|
|
|
% id = "01HPMVT9BM5VWJSMDNPK2SRNZV"
|
|
- (I'm totally not trying to say this implementation is an L so far)
|
|
|
|
% id = "01HPMVT9BMWG6QHQ125Z884W8Z"
|
|
+ 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"
|
|
- see what I did there?
|
|
|
|
% id = "01HPMVT9BMJTG3KD3K5EJ3BC93"
|
|
- the solution to that is to introduce more tiles to handle these edge cases.
|
|
|
|
% classes.branch = "tileset-four-to-eight-demo"
|
|
id = "01HPQCCV4R5N97FJ1GS36HZJZ7"
|
|
- to represent the corners, we'll turn our four cardinal directions...
|
|
|
|
<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>
|
|
|
|
into eight *ordinal* directions:
|
|
|
|
<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>
|
|
|
|
% id = "01HPQCCV4R3GNEWZQFWGWH4Z6R"
|
|
- you might think that at this point we'll need 8 bits to represent our tiles, and that would make...
|
|
|
|
***256 tiles!?***
|
|
|
|
nobody in their right mind would actually draw 256 separate tiles, right? ***RIGHT???***
|
|
|
|
% template = true
|
|
id = "01HPQCCV4RX13VR4DJAP2F9PFA"
|
|
- ...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:
|
|
|
|
<canvas
|
|
is="tairu-editor"
|
|
data-tilemap-id="bitwiseAutotilingCorners"
|
|
data-tile-size="40"
|
|
>
|
|
Your browser does not support <canvas>.
|
|
<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).
|
|
|
|
% classes.branch = "tileset-four-to-eight-demo"
|
|
id = "01HPQCCV4RHZ8A7VMT2KM7T27P"
|
|
- what we can do about this is to ignore corners whenever zero or one of the tiles at their cardinal directions is connected -
|
|
for example, in the case of `E | SE | S`:
|
|
|
|
<ul class="directions-square e-s">
|
|
<li class="east">E</li>
|
|
<li class="south-east">SE</li>
|
|
<li class="south">S</li>
|
|
</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 = "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 defining a bunch of variables to represent our ordinal directions:
|
|
|
|
```javascript ordinal-directions
|
|
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;
|
|
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"
|
|
- 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:
|
|
|
|
```javascript ordinal-directions
|
|
export function isSet(integer, bit) {
|
|
return (integer & bit) == bit;
|
|
}
|
|
```
|
|
|
|
% 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.
|
|
|
|
```javascript ordinal-directions
|
|
// t is a tile index; variable name is short for brevity
|
|
export function removeRedundancies(t) {
|
|
if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) {
|
|
t &= ~SE;
|
|
}
|
|
if (isSet(t, SW) && (!isSet(t, S) || !isSet(t, W))) {
|
|
t &= ~SW;
|
|
}
|
|
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;
|
|
}
|
|
```
|
|
|
|
% id = "01HPSY4Y19HWQQ9XBW1DDGW68T"
|
|
- with that, we can find a set of all unique non-redundant combinations:
|
|
|
|
```javascript ordinal-directions
|
|
export function ordinalDirections() {
|
|
let unique = new Set();
|
|
for (let i = 0; i <= ALL; ++i) {
|
|
unique.add(removeRedundancies(i));
|
|
}
|
|
return Array.from(unique).sort((a, b) => a - b);
|
|
}
|
|
```
|
|
|
|
% id = "01HPSY4Y19KG8DC4SYXR1DJJ5F"
|
|
- by the way, I find it quite funny how JavaScript's [`Array.prototype.sort`] defaults to ASCII ordering *for all types.*
|
|
even numbers! ain't that silly?
|
|
|
|
[`Array.prototype.sort`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
|
|
|
% id = "01HPSY4Y19V62YKTGK3TTKEB38"
|
|
- and now it's time to _Let It Cook™_:
|
|
|
|
```javascript ordinal-directions
|
|
let dirs = ordinalDirections();
|
|
console.log(dirs.length);
|
|
```
|
|
|
|
```output ordinal-directions
|
|
47
|
|
```
|
|
|
|
% id = "01HPSY4Y194DYYDGSAT83MPQFR"
|
|
- forty seven! that's how many unique tiles we actually need.
|
|
|
|
% id = "01HPSY4Y19C303Z595KNVXYYVS"
|
|
- you may find pixel art tutorials saying you need forty *eight* and not forty *seven*, but that is not quite correct -
|
|
the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO.
|
|
|
|
% id = "01HPSY4Y19TM2K2WN06HHEM3D0"
|
|
- phew... the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling!
|
|
|
|
% 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!
|
|
|
|
% 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.
|
|
|
|
% 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.
|
|
|
|
% id = "01HPWJB4Y0QX6YR6TQKZ7T1C2E"
|
|
- we *could* use a similar approach to the 16 tile version, but that would leave us with lots of wasted space!
|
|
|
|
% 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.
|
|
|
|
% id = "01HPWJB4Y0705RWPFB89V23M1P"
|
|
- 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!
|
|
|
|
% 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.
|
|
|
|
% id = "01HPWJB4Y0HTV32T4WMKCKWTVA"
|
|
- we'll start by obtaining our ordinal directions array again:
|
|
|
|
```javascript ordinal-directions
|
|
export let xToConnectionBitSet = ordinalDirections();
|
|
```
|
|
|
|
% 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.
|
|
|
|
remember that our array has only 256 values, so it should be pretty cheap to represent using a `Uint8Array`:
|
|
|
|
```javascript ordinal-directions
|
|
export let connectionBitSetToX = new Uint8Array(256);
|
|
for (let i = 0; i < xToConnectionBitSet.length; ++i) {
|
|
connectionBitSetToX[xToConnectionBitSet[i]] = i;
|
|
}
|
|
```
|
|
|
|
% 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!
|
|
|
|
```javascript ordinal-directions
|
|
console.log(connectionBitSetToX[E | SE | S]);
|
|
```
|
|
```output ordinal-directions
|
|
4
|
|
```
|
|
|
|
% id = "01HPWJB4Y09P9Q3NGN59XWX2X9"
|
|
+ for my own (and your) convenience, here's a complete list of *all* the possible combinations in order.
|
|
|
|
% id = "01HPWJB4Y01VJFMHYEC1WZ353W"
|
|
- ```javascript ordinal-directions
|
|
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])}`);
|
|
}
|
|
```
|
|
```output ordinal-directions
|
|
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
|
|
```
|
|
|
|
% 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:
|
|
|
|
anyways I spent like 20 minutes doing that by hand, and now we have a neat tile strip just like before, except way longer:
|
|
|
|
![horizontal tile strip of 47 8x8 pixel metal tiles][pic:01HPW47SHMSVAH7C0JR9HWXWCM]
|
|
|
|
% id = "01HPWJB4Y0J3DHQV5F9GD3VNQ8"
|
|
- now let's hook it up to our tileset renderer! TODO literate program.
|
|
|
|
% template = true
|
|
id = "01HPWJB4Y00ARHBGDF2HTQQ4SD"
|
|
- with the capability to render with 47-tile tilesets, our examples suddenly look a whole lot better!
|
|
|
|
<canvas
|
|
is="tairu-editor"
|
|
data-tilemap-id="bitwiseAutotiling47"
|
|
data-tile-size="40"
|
|
>
|
|
Your browser does not support <canvas>.
|
|
<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"
|
|
+ but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit)
|
|
|
|
% id = "01HPD4XQPW11EQTBDQSGXW3S52"
|
|
+ (of course it's really cool, after all [rxi](https://github.com/rxi) made it)
|
|
|
|
% 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
|
|
|
|
% 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
|
|
|
|
% id = "01HPD4XQPWB11TZSX5VAAJ6TCD"
|
|
- I stopped using it because VS Code was just more feature packed and usable; no need to reinvent the wheel, rust-analyzer *just works.*
|
|
|
|
% id = "01HPD4XQPW3G7BXTBBTD05MB8V"
|
|
- the LSP plugin for Lite XL had some issues around autocompletions not filling in properly :pensive:
|
|
|
|
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.
|
|
|
|
% id = "01HPD4XQPWV1BAPA27SNDFR93B"
|
|
- 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
|
|
|
|
% id = "01HPD4XQPWM1JSAPXVT6NBHKYY"
|
|
classes.branch_children = "branch-quote"
|
|
- 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
|
|
|
|
% id = "01HPD4XQPWE7ZVR0SS67DHTGHQ"
|
|
- autotile using this base tileset
|
|
|
|
% id = "01HPD4XQPW2BFZYQQ920SYHM9M"
|
|
- if the tile above is empty AND with a 50% chance
|
|
|
|
% id = "01HPD4XQPWJB7V67TS1M3HFCYE"
|
|
- then grass
|
|
|
|
% id = "01HPD4XQPWF7K85Z0CEK4WDDBZ"
|
|
- if the tile above is solid AND with a 10% chance
|
|
|
|
% id = "01HPD4XQPW5J3N6MVT9Z2W00S9"
|
|
- then vines
|
|
|
|
% id = "01HPD4XQPWGCMCEAR5Z9EETSGP"
|
|
- if the tile above is vines AND with a 50% chance
|
|
|
|
% id = "01HPD4XQPWP847T0EAM0FJ88T4"
|
|
- then vines
|
|
|
|
% id = "01HPSY4Y19FA2HGYE4F3Y9NJ57"
|
|
- well... it's even simpler than that in terms of graphical presentation, but we'll get to that.
|
|
|
|
% 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?
|
|
|
|
% id = "01HPJ8GHDFRA2SPNHKJYD0SYPP"
|
|
- of course Tilekit's solution is a lot more simple, streamlined, and user-friendly, but you get the gist.
|