Package github.com/matjam/bunyip/grid/autotile
grid/autotile
Package autotile picks tile frames from a terrain grid, so a map of plain terrain ids draws with matching edges, corners and transitions. The game keeps one small int per cell; a Mapper turns that into frame indices for a tile sheet, whole maps at a time with Apply, one changed cell at a time with Cell, or one changed rectangle at a time with Region. The package is pure logic: frames are ints for gfx.Tilemap or anything else, and -1 means no tile.
Five rule kinds cover the usual tilesets. Edge16 matches the four edge neighbours and needs 16 tiles: walls, pipes, fences. Edge64 is its hexagonal counterpart, matching the six neighbours of a hexagon with 64 tiles. Blob47 matches all eight neighbours, reduced to the 47 distinct cases: the standard blob terrain set. Corner16 is the dual grid: each tile sits on a corner between four cells and needs 16 tiles. Wang matches terrain colours on tile edges, corners or both, for any number of terrains meeting with proper transitions; tilesets authored in the Tiled editor's terrain tool convert to it through the tiled package. ExpandBlob composes the 47 blob tiles from a six-tile template so an artist draws six tiles instead of 47.
Directions follow Tiled's clockwise order from north: N, NE, E, SE, S, SW, W, NW. A Mapper's Layout says where each direction's neighbour lies: Square, hexagons in staggered rows or columns, hexagons in axial coordinates, or an isometric diamond grid whose directions are the tile's directions on screen. A hexagonal layout has six neighbours and no diagonals, so it takes Edge64 or Wang rules; Blob47 and Corner16 need the eight neighbours of a square or isometric grid.
All zero values are usable defaults: a Mapper with only Rules set works on a square grid, treats the map border as continuing each cell's terrain and varies tile variants with seed zero.
Index
- Constants
func BlobIndex(mask uint8) intfunc BlobMasks() [47]uint8func ExpandBlob(template image.Image, tile int) (*image.RGBA, [47]int)- type Layout
- type Mapper
- type Rules
func Blob47(terrain int, frames [47]int) *Rulesfunc Corner16(terrain int, frames [16]int) *Rulesfunc Edge16(terrain int, frames [16]int) *Rulesfunc Edge64(terrain int, frames [64]int) *Rulesfunc Wang(t WangType, tiles []WangTile) *Rulesfunc (r *Rules) Connect(terrains ...int) *Rulesfunc (r *Rules) Variant(mask, frame int, weight float64) *Rules
- type WangTile
- type WangType
Constants
const (
DirN = iota
DirNE
DirE
DirSE
DirS
DirSW
DirW
DirNW
)
Directions, clockwise from north. They index WangTile.Colors and number the bits of a blob mask.
Functions
BlobIndex source
func BlobIndex(mask uint8) int
BlobIndex maps a raw 8-bit neighbour mask (bit d set when the neighbour in direction d connects) to the canonical 0..46 index that Blob47 frames use.
BlobMasks source
func BlobMasks() [47]uint8
BlobMasks returns the 47 canonical masks in frame order, for building or checking a sheet layout.
ExpandBlob source
func ExpandBlob(template image.Image, tile int) (*image.RGBA, [47]int)
ExpandBlob composes the 47 blob tiles from a six-tile template, so an artist draws six tiles instead of 47. The template is two tiles wide and three tall, each tile square with the given side:
inner preview TL TR BL BR
TL, TR, BL and BR are the four corner tiles of a filled two-by-two block, which supply the outer corners, the edges and the interior. The inner tile holds the four inside-corner pieces, drawn as if a hole sat at each diagonal. The preview tile is not read; RPG Maker style autotile blocks put a display tile there.
Every output tile is built from four quarter-tiles chosen by the neighbour mask. The result is a sheet eight tiles wide holding the 47 tiles in canonical Blob47 order, and the matching frames array: upload it, cut it with a sheet of the same tile size, and pass the frames to Blob47. tile must be a positive even pixel size; template must be non-nil and contain at least two tiles across and three down from Bounds().Min.
Types
type Layout source
type Layout int
Layout is the shape of the grid a Mapper walks: which of the eight directions a cell has a neighbour in, and which cell that is. The zero value, Square, is a square grid with four edge neighbours and four diagonals.
A hexagonal layout has six neighbours and no diagonals. The rows layouts hold pointy-top hexagons in staggered rows, which is Tiled's stagger axis Y: every cell has an east and a west neighbour, and the four remaining sides run to the rows above and below. The columns layouts hold flat-top hexagons in staggered columns, Tiled's stagger axis X: every cell has a north and a south neighbour. Odd and even say which rows or columns are shifted, matching Tiled's stagger index.
IsoDiamond is a square grid projected as isometric diamonds, so the direction names are the tile's directions on screen: the cell north of (x, y) is (x-1, y-1), and the cell that shares the diamond's upper-right edge is (x, y-1).
const (
Square Layout = iota // a square grid: four edges and four diagonals
HexRowsOdd // hexagons in staggered rows, the odd rows shifted right
HexRowsEven // hexagons in staggered rows, the even rows shifted right
HexColsOdd // hexagons in staggered columns, the odd columns shifted down
HexColsEven // hexagons in staggered columns, the even columns shifted down
// HexAxial holds hexagons in axial coordinates, where x runs east and
// y runs south-east, so the offsets are the same for every cell. It
// has the same six directions as the rows layouts.
HexAxial
IsoDiamond // a square grid projected as diamonds, with screen-relative directions
)
Dirs source
func (l Layout) Dirs() []int
Dirs lists the directions the layout has neighbours in, clockwise. A square or isometric layout lists all eight; a hexagonal one lists its six. The result is a fresh slice the caller may keep.
Hex source
func (l Layout) Hex() bool
Hex reports whether the layout is hexagonal, so a cell has six neighbours and no diagonals.
type Mapper source
type Mapper struct {
Rules *Rules
// Layout is the grid's shape, which says where each direction's
// neighbour lies. The zero value is Square.
Layout Layout
// Seed varies which variant each cell picks; the choice is a pure
// function of Seed and the cell position, so reapplying is stable.
Seed uint64
// Outside is the terrain assumed beyond the map edge when
// OutsideFixed is set. Unset, the outside continues whatever
// terrain is being matched, so borders connect.
Outside int
OutsideFixed bool
// contains filtered or unexported fields
}
Mapper applies one set of rules to a terrain grid. Rules is the only required field. The zero values of the rest mean: the border continues each cell's own terrain, and variants are seeded with zero. A Mapper reuses scratch storage and is not safe for concurrent or recursive use. Finish modifying its Rules before applying them.
Apply source
func (m *Mapper) Apply(w, h int, terrain func(x, y int) int, set func(x, y, frame int))
Apply computes a frame for every cell and hands each to set, with -1 for cells the rules leave empty. terrain returns the id at a cell and is only called inside the map. Corner16 rules emit one extra row and column: x and y run to w and h inclusive. A nil Rules makes no calls.
Cell source
func (m *Mapper) Cell(x, y, w, h int, terrain func(x, y int) int, set func(x, y, frame int))
Cell recomputes the cells a change at (x, y) can affect: the cell and the neighbours the layout gives it, or the four surrounding corners for Corner16 rules. Call it after editing one cell instead of reapplying the map. To update after editing a block of cells, call Region instead, which computes each affected cell once.
Region source
func (m *Mapper) Region(x, y, rw, rh, w, h int, terrain func(x, y int) int, set func(x, y, frame int))
Region recomputes the cells a change anywhere in a rectangle can affect, and hands each to set once. To re-autotile after a brush stroke or a pasted block, call it with the rectangle of edited cells: rw by rh cells with its top-left cell at (x, y), on a w by h map. The frames it sets are the ones calling Cell for every cell of the rectangle would set, but a cell next to several edited cells is computed once instead of once per edited neighbour. Cells go to set in row-major order. Parts of the rectangle outside the map are allowed; a nil Rules or an empty rectangle makes no calls.
type Rules source
type Rules struct {
// contains filtered or unexported fields
}
Rules maps a cell's neighbourhood to tile frames. Build one with Edge16, Edge64, Corner16, Blob47 or Wang, then hand it to a Mapper. The zero value has no frames; use a constructor before applying rules.
Blob47 source
func Blob47(terrain int, frames [47]int) *Rules
Blob47 builds rules that match all eight neighbours of cells with the given terrain id, reduced to the 47 distinct cases: a diagonal only matters when both edges beside it connect. frames is in canonical order, ascending by normalised mask; BlobIndex maps any raw mask to its place, and ExpandBlob emits tiles in this order.
Corner16 source
func Corner16(terrain int, frames [16]int) *Rules
Corner16 builds dual-grid rules for cells with the given terrain id. Each output tile sits on a corner between four cells, so Apply and Cell emit a grid one wider and one taller than the map; draw that tilemap offset up and left by half a tile. frames is indexed by a 4-bit mask of matching cells around the corner: north-west 1, north-east 2, south-west 4, south-east 8.
Edge16 source
func Edge16(terrain int, frames [16]int) *Rules
Edge16 builds rules that match the four edge neighbours of cells with the given terrain id. frames is indexed by a 4-bit mask of connected neighbours: north 1, east 2, south 4, west 8. Frame -1 leaves a case empty.
Edge64 source
func Edge64(terrain int, frames [64]int) *Rules
Edge64 builds rules that match the six neighbours of cells with the given terrain id on a hexagonal layout. frames is indexed by a 6-bit mask of connected neighbours, bit i being the i-th direction the layout uses, clockwise: for the rows layouts north-east, east, south-east, south-west, west and north-west, and for the columns layouts north, north-east, south-east, south, south-west and north-west. Frame -1 leaves a case empty.
Wang source
func Wang(t WangType, tiles []WangTile) *Rules
Wang builds rules from a Wang tile set. Cells hold terrain colours, zero for empty. For each cell the mapper works out the colour every relevant position should show, then places the tile matching the most positions; a complete set always matches exactly, an incomplete one falls back to the closest tile. Where two terrains meet at a corner or an edge, the higher colour wins, so higher colours overlap lower ones. A cell whose own and surrounding colours are all zero gets -1.
A hexagonal layout has six sides and no diagonals, so every type matches those six positions, in the direction slots the layout uses. t must be WangCorners, WangEdges or WangFull. The tiles slice is retained, not copied; edits affect later mapping. Use finite weights; nonpositive weights count as 1. An empty set maps every cell to -1.
A Mapper indexes the set by the colours each tile shows, so a cell that some tile matches at every position costs one lookup, however large the set. A cell no tile matches exactly scores every tile. Each call to Apply, Cell or Region first compares the set with the copy the index was built from and rebuilds the index if a tile changed, which costs time in proportion to the number of tiles.
Connect source
func (r *Rules) Connect(terrains ...int) *Rules
Connect makes neighbours of other terrains count as connected, so grass rules join up against road cells without drawing their tiles. It returns the rules for chaining.
Variant source
func (r *Rules) Variant(mask, frame int, weight float64) *Rules
Variant adds an alternative frame for one neighbourhood, chosen at random by cell position with the given weight; the frame passed to the constructor keeps weight 1. mask is the scheme's mask: 4 bits for Edge16 and Corner16, 6 bits for Edge64, the raw 8-bit mask for Blob47. A nonpositive weight counts as 1. An out-of-range mask is ignored. Wang rules take variants as extra tiles instead.
type WangTile source
type WangTile struct {
Frame int
Colors [8]int
Weight float64
}
WangTile is one tile of a Wang set: its frame and the terrain colour at each of the eight positions, in direction order (DirN through DirNW). Colour zero means the position is empty. Tiles with equal colours are variants; Weight biases the choice and zero counts as 1.
type WangType source
type WangType int
WangType says which positions of a Wang tile carry terrain colours.
const (
// WangCorners matches colours on the four diagonal positions; the
// usual form for overlapping terrains.
WangCorners WangType = iota
// WangEdges matches colours on the four edge positions; the usual
// form for paths and pipes.
WangEdges
// WangFull matches all eight positions.
WangFull
)
Source files
autotile.go autotile_test.go bench_test.go expand.go index_test.go layout.go layout_test.go wang.go