Loading Tiled maps at runtime
Tiled is a piece of software used to design grid based levels, there are others, and all are easily supported provided they export some readable data format. Limited basic support for the Tiled format is built into Indigo.
In this example, we can see how to load tiled data at runtime as a JSON asset, and parse it into a usable map that can be rendered with the accompanying image asset.
Loading data like this as runtime has the advantage of lowering the initial payload size of your game. The drawback of this approach is the added game logic complexity needed to do the load - and if you are using the asset loader, to wait for the loading to happen, deal with any errors that might occur and store the data in your model / view model some where.
The other approach is to bake the pre-loaded tiled data into your game at build / compile time using a custom generator.
Example Links
How to load and use a Tiled map
In this example we're going to need a bit of a model to help us organise our data.
The top level model will contain the loaded tilemap, which we'll use later directly during rendering, and a custom game map.
Normally, the tiledMap would probably make more sense to live in the view model, since it is purely a rendering concern, but since this example is build in the sandbox we don't have that option. You'll have to use your imagination!
The game map is a custom representation of the tilemap that we can use to drive game logic. It is all very well importing a game level from Tiled, but you will almost certainly want a model representation so that you can, for example, check for collisions, or whether moves are valid, or ask what lives where on a map, etc. In this example, we only have two types of tile.
Note the useful helpers in the GameMap companion object.
final case class Model(tiledMap: TiledMap, gameMap: GameMap)
final case class GameMap(grid: List[List[MapTile]])
object GameMap:
val empty: GameMap = GameMap(List.empty)
def fromTiledGrid(grid: TiledGridMap[MapTile]): GameMap =
GameMap(grid.toList2DPerLayer.head.map(_.map(_.tile)))
enum MapTile:
case Platform, Empty
Loading the data
To make use of the tiled data, we need to load and parse it, using the Tiled Json helper. In this example that will happen during the initial loading phase, but in a real game you might want to load it on demand using the asset loader.
def setup(assetCollection: AssetCollection, dice: Dice): Outcome[Startup[TiledMap]] =
Outcome {
val maybeTiledMap = for {
j <- assetCollection.findTextDataByName(Assets.assets.terrainData)
t <- Json.tiledMapFromJson(j)
} yield t
maybeTiledMap match {
case None =>
Startup.Failure("Could not generate TiledMap from data.")
case Some(tiledMap) =>
Startup.Success(tiledMap)
}
}
Initialising the model
Our model is then initialised with the tilemap (which would normally live in the view model,
probably), and we make use of the toGrid
method on the tilemap to convert it into our custom
game map.
We're not using the custom game map in this example, but it's being println'd out so that you can see it in the JS console if you run the example.
def initialModel(startupData: TiledMap): Outcome[Model] =
val gameMap = startupData
.toGrid {
case 0 => MapTile.Empty
case _ => MapTile.Platform
}
.map(GameMap.fromTiledGrid)
.getOrElse(GameMap.empty)
println(gameMap)
Outcome(
Model(startupData, gameMap)
)
Rendering the tilemap
To display the tiled map, we just use the toGroup
method on the tilemap to convert it into
an Indigo group of primitives that reference the terrain image asset.
toGroup
is a simple convenience method that will create a Graphic
for each tile in the
map. If you want more control you'll either need to interpret the TiledMap
yourself, or make
use of the custom version we made earlier.
def present(context: FrameContext[TiledMap], model: Model): Outcome[SceneUpdateFragment] =
Outcome(
SceneUpdateFragment(
model.tiledMap.toGroup(Assets.assets.terrain)
)
)