CAPITOLO 10

Tilemaps

Intermedio
~40 min
1h 55m

Introduzione

Una tilemap è una struttura dati che consente di creare e manipolare mappe composte da piccole immagini di forma regolare chiamate tile. Queste mappe possono essere usate per creare livelli 2D, applicare la fisica, rilevare collisioni e controllare le posizioni degli sprite.

Terminologia:

  • Tile — singola immagine quadrata che compone la mappa
  • Tileset — l'insieme di tutte le tile disponibili, raccolte in un'unica immagine PNG
  • Layer — livello della mappa; una mappa può avere più layer sovrapposti (visuale, collisioni, oggetti)

Tiled è lo strumento consigliato per creare le mappe. È open source e gratuito.


Tile-extruder — eliminare il bleeding

Quando le tile sono esattamente di 32×32 px, Phaser può generare piccoli spazi visibili tra le tile adiacenti (bleeding). La libreria tile-extruder risolve questo problema aggiungendo a ogni tile un bordo di un pixel che replica i pixel del perimetro.

Esempio: 4 tile da 32×32 → PNG da 128×32 px → PNG estrusa da 136×34 px (ogni tile aggiunge 2px).

Installazione:

npm install --global tile-extruder

Utilizzo:

tile-extruder -w 32 -h 32 -i tilemap-example.png -o tilemap-extruded.png
ParametroSignificato
-wLarghezza della tile in px
-hAltezza della tile in px
-iFile PNG di input
-oFile PNG di output

L'immagine estrusa tilemap-extruded.png è quella da importare in Tiled e in Phaser.


Creare la mappa con Tiled

Nuova mappa

Apri Tiled. Vedrai la schermata iniziale dell'applicazione.

Clicca su Nuova Mappa. Si apre questa finestra di dialogo:

Imposta le dimensioni delle tile a 32×32 per far coincidere con il tileset. Clicca OK e ti troverai in questa visualizzazione:

Vai su File → Salva Come:

Salva la mappa come level-0.json (formato: JSON map files *.json):

Phaser 3.9 legge il formato Tiled JSON classico (non il più recente .tmj, introdotto in versioni successive di Tiled). Se usi una versione recente di Tiled, assicurati di esportare in "JSON map files (*.json)" e non nel formato .tmj.

Importare il tileset

Clicca su Nuovo Tileset:

Sfoglia e seleziona tilemap-extruded.png → spunta Incorpora nella mappa → imposta:

  • Margine: 1 px
  • Spaziatura: 2 px

Questi valori corrispondono al bordo aggiunto da tile-extruder. Clicca OK.

Layer world (visuale)

Rinomina il layer di default in world. Seleziona una tile e usa lo strumento riempimento per coprire l'area:

Oppure usa lo strumento pennello per disegnare manualmente perimetri e strutture:

Layer collision (collisioni)

Clic destro nell'area dei layer → Nuovo livello Tile → nome: collision:

Ora hai due layer. Le icone occhio e lucchetto servono per nascondere/visualizzare e abilitare/disabilitare l'editing sui singoli livelli:

Disegna su questo layer le stesse zone che devono collidere con il personaggio.

Impostare la proprietà collision sulle tile

  1. Clicca l'icona Modifica Tileset:

  1. Seleziona la tile da usare per le collisioni e aggiungi una proprietà personalizzata:

  1. Imposta tipo bool e nome collision:

  1. Inserisci il valore true e salva con CTRL+S:

Questa proprietà verrà letta da Phaser per attivare la collisione fisica.


Importare la tilemap in Phaser

GameData.ts

tilemaps: [
  {
    key: "level-0",
    path: "assets/map/level-0.json",
  },
],

Per il tileset, aggiungilo come spritesheet con margine e spaziatura:

spritesheets: [
  {
    name: "tilemap-extruded",
    path: "assets/map/tilemap-extruded.png",
    width: 32,
    height: 32,
    spacing: 2,
    margin: 1,
  },
],

Il name del tileset nel codice deve corrispondere esattamente al nome che hai assegnato in Tiled quando hai importato il tileset.

Static vs Dynamic Layer

In Phaser 3.9 un layer di tilemap può essere di due tipi distinti, creati con due metodi separati:

TipoMetodoCaratteristiche
StaticcreateStaticLayer()Più veloce da renderizzare, ma non supporta tint, alpha, o modifiche alle tile a runtime
DynamiccreateDynamicLayer()Leggermente più lento, ma supporta effetti per-tile (setAlpha, setTint) e modifiche runtime (putTileAt, removeTileAt)

Poiché nel nostro esempio vogliamo rendere il layer collisioni semi-trasparente durante il debug (setAlpha), useremo Dynamic Layer per entrambi i layer.

Variabili nella scena

private map: Phaser.Tilemaps.Tilemap;
private tileset: Phaser.Tilemaps.Tileset;
private layer: Phaser.Tilemaps.DynamicTilemapLayer;    // layer visuale
private layer2: Phaser.Tilemaps.DynamicTilemapLayer;   // layer collisioni

Inizializzazione nel create()

1. Crea la tilemap dalla chiave:

this.map = this.make.tilemap({ key: "level-0" });

2. Adatta i bounds della camera e del mondo fisico alle dimensioni della mappa:

this.cameras.main.setBounds(
  0,
  0,
  this.map.widthInPixels,
  this.map.heightInPixels
);

this.physics.world.setBounds(
  0,
  0,
  this.map.widthInPixels,
  this.map.heightInPixels
);

3. Aggiungi il tileset alla mappa:

this.tileset = this.map.addTilesetImage("tilemap-extruded");

4. Crea i layer con createDynamicLayer():

// Layer visuale (depth alta = in primo piano)
this.layer = this.map.createDynamicLayer("world", this.tileset, 0, 0)
  .setDepth(9)
  .setAlpha(1);

// Layer collisioni (può essere trasparente in produzione)
this.layer2 = this.map.createDynamicLayer("collision", this.tileset, 0, 0)
  .setDepth(0)
  .setAlpha(0); // imposta a 1 durante il debug

5. Attiva le collisioni sul layer:

this.layer2.setCollisionByProperty({ collide: true });

6. Crea il collider tra il player e il layer:

this.physics.add.collider(
  this._player,
  this.layer2,
  (_player: any, _tile: any) => {
    // verifica proprietà personalizzate della tile
    if (_tile.properties.exit === true) {
      console.log("livello completato");
    }
    if (_tile.properties.death === true) {
      // decrementa vita o game over
    }
  },
  undefined,
  this
);

La callback del collider riceve il gameObject e la tile che hanno interagito. Puoi aggiungere a ogni tile qualsiasi proprietà personalizzata in Tiled — exit, death, damage, speed — e leggerle qui per gestire comportamenti specifici.

Durante lo sviluppo imposta setAlpha(1) sul layer collisioni per vedere visivamente dove sono le tile attive. In produzione metti setAlpha(0) per nasconderle.


Proprietà personalizzate avanzate — exit e death

Puoi aggiungere ulteriori proprietà personalizzate alle tile per gestire comportamenti speciali. Ad esempio, una tile di uscita dal livello.

Nel Modifica Tileset, seleziona la tile con la freccia in alto e aggiungi due proprietà personalizzate:

Nel layer world, inserisci la tile che indica visivamente l'uscita:

Nel layer collision, posiziona la stessa tile (o una trasparente) con le proprietà collide=true e exit=true:

Quando il player collide con questa tile, la callback del collider rileva la proprietà exit=true:


Layer oggetti — posizionare gameObject sulla mappa

Un Object Layer in Tiled è un tipo speciale di layer che memorizza segnaposto sulla mappa. Serve a definire dove appariranno i gameObject dinamici (nemici, bonus, spawn point, checkpoint).

Configurare il layer in Tiled

Clic destro nell'area layer → Nuovo → Livello Oggetto → nome: gameObjects:

Seleziona la tile da usare come segnaposto e aggiungi una proprietà personalizzata di tipo string chiamata behaviour:

Inserisci come valore un oggetto JSON e posiziona le tile sulla mappa:

{"type":"bonus","score":50}

Se il valore della proprietà appare opaco in Tiled, significa che non è attivo. Clicca sul campo e aggiungi uno spazio alla fine della stringa per attivarlo, poi salva.

Salva level-0.json.

Recuperare gli oggetti in Phaser

Crea un metodo dedicato nella scena, da richiamare nel create() dopo aver inizializzato la mappa:

setupObjects(): void {
  const _objLayer = this.map.getObjectLayer("gameObjects");

  if (_objLayer != null) {
    const _objects = _objLayer.objects as any[];

    _objects.forEach((tile: Phaser.Tilemaps.Tile) => {
      // leggi la proprietà behaviour come stringa JSON
      const _objectValue = JSON.parse(tile.properties[0].value).type;

      switch (_objectValue) {
        case "bonus":
          // istanzia il gameObject nella posizione della tile
          // this.addBonus(new BonusCoin({ scene: this, x: tile.x, y: tile.y, key: "bonus-coin" }));
          break;
        case "enemy":
          // istanzia il nemico
          break;
      }
    });
  }
}

Nel create():

create() {
  // ... inizializzazione mappa ...
  this.setupObjects();
}

Schema completo del create()

create() {
  // 1. Crea la tilemap
  this.map = this.make.tilemap({ key: "level-0" });

  // 2. Bounds camera e physics
  this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
  this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);

  // 3. Tileset
  this.tileset = this.map.addTilesetImage("tilemap-extruded");

  // 4. Layer visuale
  this.layer = this.map.createDynamicLayer("world", this.tileset, 0, 0).setDepth(9);

  // 5. Layer collisioni
  this.layer2 = this.map.createDynamicLayer("collision", this.tileset, 0, 0)
    .setDepth(0).setAlpha(0);
  this.layer2.setCollisionByProperty({ collide: true });

  // 6. Player (deve usare physics.add.sprite per avere il body)
  this._player = this.physics.add.sprite(100, 100, "player");
  this._player.body.setCollideWorldBounds(true);

  // 7. Camera segue il player
  this.cameras.main.startFollow(this._player, true, 0.1, 0.1);

  // 8. Collider player <-> mappa
  this.physics.add.collider(this._player, this.layer2);

  // 9. Object layer
  this.setupObjects();
}

Documentazione ufficiale: Tilemaps namespace

Phaser Labs
Phaser LabsLIVE
Prova tutto dal vivo — centinaia di esempi interattivi
Phaser Labs raccoglie esempi funzionanti per ogni argomento del corso. Copia, modifica, sperimenta — direttamente nel browser.
Esempi sulla classe TILEMAP
Sh4rk0

Le tilemap sono il salto di qualità che trasforma un prototipo in un livello vero. Finché disegni il mondo con sprite posizionati a mano nel codice, stai lavorando contro te stesso. Con Tiled puoi costruire il livello visivamente e importarlo in tre righe.

Il workflow da memorizzare è: Tiled esporta JSON → Phaser carica JSON → layer separati per visuale e collisioni. Una volta che hai questo schema in testa, aggiungere nuovi livelli diventa banale.

Il layer collision trasparente è uno dei pattern più potenti: la mappa che vede il giocatore è una cosa, la mappa che "sente" la fisica è un'altra. Questa separazione ti dà controllo chirurgico su dove il personaggio può camminare.

Tip pro: le proprietà personalizzate delle tile (exit, death, damage) sono il modo professionale per gestire i livelli. Invece di hardcodare la logica nel codice, la metti nella mappa. Il designer può cambiare dove si trova la "porta di uscita" senza toccare il TypeScript. 🗺️