ignifx
GitHubnpm · soon
All examples

Tilemap

2D2D

  • Keyboard
  • Gamepad
  • Touch

A village of forty by twenty cells, drawn from a hand-written Tiled export. `Tilemap` owns the document — the grid, the tileset and the per-tile colliders; `TilemapRenderer` draws it from one atlas a chunk at a time and drops the chunks the camera cannot see; `TilemapCollider2D` turns the solid layer into one static body, with adjacent cells merged into as few outlines as the tiles allow. The three tile layers each sit in their own sorting layer, and the top one — roofs and tree crowns — draws above the villager, which is what puts her behind a house when she walks into it.

A pixel-art village seen from above: a wide dirt lane crossing the frame with a spur running north, red and grey cottages with tiled roofs along it, autumn and pine trees, mushrooms and a fenced paddock, and a small villager in a red tunic standing on the lane.
Open standalone

` devtools

WebGPU: checking…

Try this

  • Turn off Cull chunks and watch the tile count double: the whole map is materialised instead of the part you can see.
  • Walk into the fence, the tree trunks and the house fronts; every one of them is a collider the map file described.
  • Follow a lane to the edge of the village: the camera stops at the map bounds rather than showing you the void.

Read the guide

Show source

Source

main.ts
import {  Camera2D,  Camera2DFollow,  CharacterController2D,  physics2d,  spawnTilemapObjects,  SpriteAnimator,  SpriteRenderer,  Tilemap,  TilemapCollider2D,  TilemapRenderer,  twoD,  Vec2,  VirtualJoystick,} from "ignifx";import { bootExample } from "../_kit/boot.ts";import { bind, readout, slider, toggle } from "../_kit/panel.ts";import { Villager, WALK_ACTIONS } from "./villager.ts";import type {  App,  AssetHandle,  Entity,  MutableVec2,  SpriteAnimationAsset,  SpriteAtlasAsset,  TileObjectContext,  TilemapAsset,} from "ignifx";/** * A hand-drawn Tiled map with three tile layers, chunk culling, and a villager who walks it with * the camera following. * * Three components share the work. `Tilemap` owns the document — the grid, the tilesets and the * per-tile colliders; `TilemapRenderer` draws it from one atlas, a chunk at a time, dropping the * chunks the camera cannot see; `TilemapCollider2D` turns the map's merged outlines into one static * Rapier body. `village.tmj.json` beside this file is the Tiled export the map came from, and * `tools/build-2d-assets.ts` is what ran it through `importTiledMap` at build time. * * The three layers are the lesson worth watching. `Ground` is grass and stone; `Solid` is the * fences, the walls and the tree roots, and it is the only layer with collision; `Canopy` is the * roofs and the crowns, and it draws in a sorting layer **above** the villager — which is why * walking into a house or under a tree hides her. *//** The map is forty by twenty cells of one metre, and the camera may not leave it. */const LEVEL_SIZE = { x: 40, y: 20 } as const;/** The design resolution the pixel-perfect camera fits a whole-number zoom to. */const REFERENCE_RESOLUTION = { x: 320, y: 180 } as const;/** * How many cells wide a chunk is. * * @remarks * Eight rather than the default thirty-two, because a 40x20 map is barely one chunk at the default * and the point here is to watch the sprite count fall when a chunk leaves the frame. */const CHUNK_SIZE = 8;/** Scratch for the panel's cell readout, so reading it four times a second allocates nothing. */const cellScratch: MutableVec2 = new Vec2();/** * How many sprites the sprite layers are holding, tiles included. * * @param app - The running app. * @returns The total. */function tilesDrawn(app: App): number {  let total = 0;  for (const layer of app.twoD.layers) {    total += layer.count;  }  return total;}/** * Loads the four documents the level is built from. * * @remarks * Awaited before `app.start()`, where a completed load settles at once; started after it, a load is * delivered in a later frame's `PreUpdate` and the first frames would draw an empty map. * * @param app - The app being set up. * @returns The map, the tile atlas, and the villager's atlas and clips. */async function loadLevel(app: App): Promise<{  readonly map: AssetHandle<TilemapAsset>;  readonly tiles: AssetHandle<SpriteAtlasAsset>;  readonly atlas: AssetHandle<SpriteAtlasAsset>;  readonly clips: AssetHandle<SpriteAnimationAsset>;}> {  const [map, tiles, atlas, clips] = await Promise.all([    app.assets.loadAsync<TilemapAsset>("2d/village.tilemap.json"),    app.assets.loadAsync<SpriteAtlasAsset>("2d/tiny-town.atlas.json"),    app.assets.loadAsync<SpriteAtlasAsset>("2d/villager.atlas.json"),    app.assets.loadAsync<SpriteAnimationAsset>("2d/villager.spriteanim.json"),  ]);  return { map, tiles, atlas, clips };}bootExample({  title: "Tilemap",  extensions: [    // One world metre is one 16-pixel tile. The default is 100, which would draw every sprite here    // at a sixth of its intended size.    twoD({ pixelsPerUnit: 16, ySort: { Default: true } }),    physics2d(),  ],  settings: {    rendering: {      // `twoD` in `"sprite"` mode clears the frame to black itself and does not read      // `rendering.clearColor` (`packages/2d/src/extension.ts`), so the sky here is the renderer's,      // not a setting. Multisampling is off because it would soften exactly the edges pixel art      // exists to keep sharp.      msaaSamples: 1,    },    time: { fixedDeltaTime: 1 / 60 },    // Back to front. `Canopy` is above `Default`, which is what puts a roof in front of a villager.    sortingLayers: { sortingLayers: ["Ground", "Terrain", "Default", "Canopy"] },    layers: { layers: ["Default", "Player", "Terrain"] },    // A top-down world has no gravity: the controller goes exactly where `move` says.    physics2d: { gravity: { x: 0, y: 0 }, defaultMaterial: { friction: 0, restitution: 0 } },  },  async setup({ app, panel }) {    app.registerComponents([Villager]);    app.input.loadActions(WALK_ACTIONS);    const assets = await loadLevel(app);    // The map's objects layer is a list of intentions — "someone starts here" — and only the game    // knows what each one becomes. Registered before the map is walked, or nothing spawns.    app.twoD.registerTileObjectFactory("spawn", (context: TileObjectContext): Entity => {      const entity = app.world.createEntity(context.name);      entity.layer = app.world.layers.requireIndex("Player");      // The object is a one-metre cell and its position is the cell's bottom-left corner; the      // sheet's frames pivot at `[0.5, 1]`, so the entity's origin is under her feet.      entity.transform.position2D = new Vec2(context.position.x + context.size.x / 2, context.position.y);      entity.addComponent(SpriteRenderer, { sprite: assets.atlas, sortingLayer: "Default" });      entity.addComponent(SpriteAnimator, {        animations: assets.clips,        defaultClip: "idle_down",        playOnAwake: true,      });      // A box, not the default capsule: a top-down character slides along a wall more predictably      // with square corners. The offset lifts the box off the origin, which is at her feet.      entity.addComponent(CharacterController2D, {        shape: "box",        radius: 0.3,        height: 0.5,        offset: { x: 0, y: 0.25 },        slopeLimit: 90,        snapToGround: 0,      });      entity.addComponent(Villager);      return entity;    });    const level = app.world.createEntity("Level");    level.layer = app.world.layers.requireIndex("Terrain");    const map = level.addComponent(Tilemap, { map: assets.map, chunkSize: CHUNK_SIZE });    const renderer = level.addComponent(TilemapRenderer, { atlas: assets.tiles, cullChunks: true });    // Adjacent solid cells are merged into as few polygons as the tiles allow, so the fence around    // the paddock is one shape rather than one per cell.    level.addComponent(TilemapCollider2D).collisionData = map.collisionData;    // The call answers with what it created, in document order, which is how the camera finds its    // subject without the factory having to publish it.    const villager = spawnTilemapObjects(app, app.twoD, map)[0];    if (villager === undefined) {      throw new Error('village.tilemap.json has no object of type "spawn".');    }    const eye = app.world.createEntity("Main Camera");    eye.transform.position2D = new Vec2(villager.transform.position2D.x, villager.transform.position2D.y);    eye.addComponent(Camera2D, {      // `orthographicSize` is ignored while `pixelPerfect` is on: the zoom is      // `viewportHeight / referenceResolution.y`, snapped to a whole number, so one texel always      // covers an exact square of screen pixels.      pixelPerfect: true,      referenceResolution: REFERENCE_RESOLUTION,      follow: villager,      followDamping: 0.12,      followOffset: { x: 0, y: 0.5 },      deadZone: { x: 1.5, y: 1 },      boundsMin: { x: 0, y: 0 },      boundsMax: LEVEL_SIZE,    });    eye.addComponent(Camera2DFollow);    // On-screen controls only where there is a touch screen: on a desktop they would cover the map.    if (navigator.maxTouchPoints > 0) {      const joystick = new VirtualJoystick(app, {        control: "joystick",        ariaLabel: "Walk",        style: { left: "1.5rem", bottom: "calc(1.5rem + var(--ignifx-safe-bottom, 0px))" },      });      window.addEventListener("pagehide", (): void => {        joystick.dispose();      });    }    panel({      title: "Tilemap",      groups: [        {          label: "Map",          controls: [            toggle("Cull chunks", bind(renderer, "cullChunks")),            // Every tile the renderer has materialised, summed over the sprite layers. Turn the            // toggle off and it climbs to the whole map; turn it on and it falls back to the            // chunks the camera can see. `app.twoD.spriteCount` is the other figure and counts            // only `SpriteRenderer` components — one, here, the villager.            readout("Tiles drawn", (): string => String(tilesDrawn(app))),            readout("Chunk size", (): string => `${String(map.chunkSize)} cells`),          ],        },        {          label: "Villager",          controls: [            slider(              "Speed",              { min: 1, max: 9, step: 0.5, format: (value: number): string => `${value.toFixed(1)} m/s` },              bind(villager.requireComponent(Villager), "speed"),            ),            readout("Cell", (): string => {              const cell = map.worldToCell(villager.transform.position2D, cellScratch);              return `${String(cell.x)}, ${String(cell.y)}`;            }),          ],        },      ],    });  },});
villager.ts
import { CharacterController2D, defineInputActions, f32, Script, SpriteAnimator, Vec2 } from "ignifx";import type { InputAction, MutableVec2, ScriptCallbacks } from "ignifx";/** * The villager: the walk, the facing and the clip, in one file, so `main.ts` is only the map she * walks on. *//** The four facings the sheet has, in the order a stick angle is bucketed into. */const FACINGS = ["right", "up", "left", "down"] as const;/** The actions the villager reads. Its own map, so the kit's camera actions are untouched. */export const WALK_ACTIONS = defineInputActions({  maps: [    {      name: "Village",      actions: [        {          name: "walk",          type: "vector2",          bindings: [            {              composite: "2DVector",              up: "<Keyboard>/w",              down: "<Keyboard>/s",              left: "<Keyboard>/a",              right: "<Keyboard>/d",            },            {              composite: "2DVector",              up: "<Keyboard>/arrowUp",              down: "<Keyboard>/arrowDown",              left: "<Keyboard>/arrowLeft",              right: "<Keyboard>/arrowRight",            },            { path: "<Gamepad>/leftStick", processors: ["deadzone(0.2)"] },            { path: "<Gamepad>/dpad" },            { path: "<Virtual>/joystick", processors: ["deadzone(0.15)"] },          ],        },      ],    },  ],});/** * Walks its entity on the tilemap's collision, and keeps the animator on the clip that matches * where it is heading. * * Input is sampled in `update` and spent in `fixedUpdate`, because those are two different clocks: * a frame carries zero, one or two fixed steps, so reading a stick inside the step would sample the * same frame twice or miss it entirely. */export class Villager extends Script.define({ speed: f32(4.5) }) implements ScriptCallbacks {  /** The namespaced registration id. */  static typeId = "tilemap/Villager";  #controller: CharacterController2D | null = null;  #animator: SpriteAnimator | null = null;  #walk: InputAction | null = null;  /** This frame's direction, already normalised so a diagonal is not 1.41 times faster. */  readonly #wish: MutableVec2 = new Vec2();  /** The displacement handed to the controller. Reused, so the fixed step allocates nothing. */  readonly #step: MutableVec2 = new Vec2();  /** Which way she faces, and the clip that is playing, so `play` is called only on a change. */  #facing = "down";  #clip = "";  awake(): void {    this.#controller = this.entity.requireComponent(CharacterController2D);    this.#animator = this.entity.getComponent(SpriteAnimator);    this.#walk = this.app.input.actions.find("walk");  }  update(): void {    const vector = this.#walk?.vector ?? null;    const length = vector === null ? 0 : Math.hypot(vector.x, vector.y);    if (vector === null || length < 0.01) {      this.#wish.set(0, 0);    } else {      const scale = length > 1 ? 1 / length : 1;      this.#wish.set(vector.x * scale, vector.y * scale);      // A stick points anywhere; the sheet has four directions, so the angle is bucketed into the      // nearest cardinal — which is what almost every top-down game with this kind of sheet does.      const quadrant = Math.round(Math.atan2(this.#wish.y, this.#wish.x) / (Math.PI / 2));      this.#facing = FACINGS[((quadrant % 4) + 4) % 4] ?? "down";    }    const clip = `${length < 0.01 ? "idle" : "walk"}_${this.#facing}`;    if (clip !== this.#clip) {      this.#clip = clip;      this.#animator?.play(clip);    }  }  fixedUpdate(dt: number): void {    this.#step.set(this.#wish.x * this.speed * dt, this.#wish.y * this.speed * dt);    this.#controller?.move(this.#step);  }}
village.tmj.json
{  "compressionlevel": -1,  "infinite": false,  "orientation": "orthogonal",  "renderorder": "right-down",  "tiledversion": "1.11.2",  "type": "map",  "version": "1.10",  "tilewidth": 16,  "tileheight": 16,  "width": 40,  "height": 20,  "nextlayerid": 5,  "nextobjectid": 2,  "properties": [    {      "name": "title",      "type": "string",      "value": "Willowbrook"    }  ],  "tilesets": [    {      "name": "town",      "firstgid": 1,      "image": "tiny-town.png",      "imagewidth": 203,      "imageheight": 135,      "tilewidth": 16,      "tileheight": 16,      "spacing": 1,      "margin": 0,      "columns": 12,      "tilecount": 96,      "tiles": [        {          "id": 27,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 28,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 44,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 45,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 46,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 56,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 57,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 58,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 68,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 69,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 70,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 73,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 77,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 80,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 81,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 82,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 84,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 86,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 88,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        },        {          "id": 90,          "objectgroup": {            "draworder": "index",            "objects": [              {                "id": 1,                "x": 0,                "y": 0,                "width": 16,                "height": 16,                "rotation": 0,                "visible": true              }            ]          }        }      ]    }  ],  "layers": [    {      "id": 1,      "name": "Ground",      "type": "tilelayer",      "visible": true,      "opacity": 1,      "x": 0,      "y": 0,      "width": 40,      "height": 20,      "properties": [        {          "name": "sortingLayer",          "type": "string",          "value": "Ground"        }      ],      "data": [        2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1,        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,        2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2,        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,        1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13, 15, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1,        1, 1, 1, 1, 1, 1, 13, 15, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 25, 27, 2, 1, 1, 1, 1, 1, 2,        1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 25, 27, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 25, 27, 1, 1, 1,        1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 25, 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 25,        27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 25, 27, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        1, 1, 1, 25, 27, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 25, 27, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1,        1, 1, 1, 1, 1, 1, 1, 25, 27, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 25, 27, 1, 1, 1, 2, 1, 1, 1, 1,        1, 2, 2, 13, 14, 14, 14, 14, 14, 14, 14, 41, 42, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,        14, 41, 40, 14, 14, 14, 14, 14, 14, 14, 14, 15, 1, 1, 25, 41, 40, 41, 40, 41, 40, 41, 40, 43, 42, 43, 42, 43,        42, 43, 42, 43, 42, 43, 41, 40, 41, 40, 41, 40, 41, 40, 41, 40, 41, 42, 43, 42, 43, 42, 43, 27, 1, 1, 37, 38,        38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 40, 41, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,        38, 38, 38, 38, 38, 38, 38, 39, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 25, 27, 1, 1, 1, 1, 1,        1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 25, 27, 1, 1,        2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 13, 42,        43, 14, 14, 15, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1,        2, 1, 25, 41, 40, 41, 43, 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        2, 1, 1, 1, 1, 1, 2, 37, 38, 38, 38, 38, 39, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 2, 1, 2,        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1      ]    },    {      "id": 2,      "name": "Solid",      "type": "tilelayer",      "visible": true,      "opacity": 1,      "x": 0,      "y": 0,      "width": 40,      "height": 20,      "properties": [        {          "name": "sortingLayer",          "type": "string",          "value": "Terrain"        },        {          "name": "collision",          "type": "bool",          "value": true        }      ],      "data": [        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 30, 0, 0,        0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,        0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 81, 82, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 89, 78, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 89, 91, 78, 0, 0, 0, 0, 0, 0, 0, 0,        30, 0, 85, 87, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 87, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 30, 0, 0, 0, 0, 58, 45, 46, 46, 46, 0, 0, 46,        46, 46, 47, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 6, 0,        0, 0, 0, 0, 0, 0, 59, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 6, 28, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91,        78, 78, 57, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 0, 85, 74, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 6, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 69, 70, 70, 70, 70, 70, 70, 70, 70, 71, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0      ]    },    {      "id": 3,      "name": "Canopy",      "type": "tilelayer",      "visible": true,      "opacity": 1,      "x": 0,      "y": 0,      "width": 40,      "height": 20,      "properties": [        {          "name": "sortingLayer",          "type": "string",          "value": "Canopy"        }      ],      "data": [        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,        0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 49,        50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 53, 54, 55, 0,        0, 0, 0, 0, 0, 61, 62, 63, 0, 0, 0, 0, 0, 0, 0, 53, 54, 55, 0, 0, 0, 0, 0, 61, 62, 63, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 65, 66, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 66, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 50, 51, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 53, 54, 55, 0, 0, 16, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 62, 63,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 66, 67, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0      ]    },    {      "id": 4,      "name": "Objects",      "type": "objectgroup",      "draworder": "topdown",      "visible": true,      "opacity": 1,      "x": 0,      "y": 0,      "objects": [        {          "id": 1,          "name": "Villager",          "type": "spawn",          "rotation": 0,          "visible": true,          "x": 160,          "y": 208,          "width": 16,          "height": 16,          "properties": []        }      ]    }  ]}

Uses:TilemapTilemapRendererTilemapCollider2DCamera2DCamera2DFollowCharacterController2DSpriteAnimatorimportTiledMap

Assets:Tiny Town tileset — CC0 1.0, KenneyVillager sprite sheet — Apache-2.0, Astrum Forge Studios