Tiles and Tileset displaying in Dart

From RogueBasin
Revision as of 23:26, 6 November 2023 by Erik (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The code below demonstrates how to load a tileset and access individual tiles. It reads in the tileset, then renders its tiles in a grid consisting of the indiviual tiles. The code consits of these classes:

MyApp

Renders a GridView of the tileset.

TileSetFactory

Returns an individual TileWidget. It holds one Widget for each tile. Tiles are accessed via (column,row) coordinates.

TileWidget

Paints an individual tile.

SquarePainter

Paints a square section of the tileset.


The tileset used in the example can be downloaded here:

https://rltiles.sourceforge.net/download.html

Dart Implementation

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final String _tilesetPath = 'assets/tilesets/dctile/tile.png';
  final int _tileWidth = 32;
  final int _tileHeight = 32;

  late final Future<ui.Image> _tilesetFuture = _loadUiImage(_tilesetPath);

  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: FutureBuilder<ui.Image>(
          future: _tilesetFuture,
          builder: (ctx, snapshot) {
            if (snapshot.hasData == false) {
              return SizedBox(
                width: _tileWidth.toDouble(),
                height: _tileHeight.toDouble(),
              );
            }

            if (snapshot.hasError == true) {
              return SizedBox(
                width: _tileWidth.toDouble(),
                height: _tileHeight.toDouble(),
              );
            }

            final tileset = snapshot.data!;

            final TileSetFactory factory =
                TileSetFactory(tileset, _tileWidth, _tileHeight);

            final List<Row> rows = [];

            for (int row = 0; row < factory.rows; row++) {
              final List<TileWidget> cells = [];
              for (int col = 0; col < factory.columns; col++) {
                cells.add(factory.getTile(col, row));
              }
              rows.add(Row(children: cells));
            }

            return InteractiveViewer(
              scaleEnabled: false,
              constrained: false,
              child: Column(children: rows),
            );
          },
        ),
      ),
    );
  }

  Future<ui.Image> _loadUiImage(String assetPath) async {
    final data = await rootBundle.load(assetPath);
    final list = Uint8List.view(data.buffer);
    final completer = Completer<ui.Image>();
    ui.decodeImageFromList(list, completer.complete);
    return completer.future;
  }
}

// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------

class TileSetFactory {
  final ui.Image _tileset;
  final int _tileWidth;
  final int _tileHeight;
  final Map<String, TileWidget> _tiles = {};

  TileSetFactory(this._tileset, this._tileWidth, this._tileHeight);

  int get columns => _tileset.width ~/ _tileWidth;
  int get rows => _tileset.height ~/ _tileHeight;
  int get length => columns * rows;

  TileWidget getTile(int col, int row) {
    final key = '${col}_$row';
    _tiles.putIfAbsent(
      key,
      () => TileWidget(
        _tileset,
        col * _tileWidth,
        row * _tileHeight,
        _tileWidth,
        _tileHeight,
      ),
    );
    return _tiles[key]!;
  }
}

// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------

class TileWidget extends StatelessWidget {
  final int left;
  final int top;
  final int width;
  final int height;
  final ui.Image image;

  const TileWidget(this.image, this.left, this.top, this.width, this.height,
      {super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
        size: Size(width.toDouble(), height.toDouble()),
        painter: SquarePainter(image, left, top, width, height));
  }
}

// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------

class SquarePainter extends CustomPainter {
  final int left;
  final int top;
  final int width;
  final int height;
  final ui.Image tileset;

  SquarePainter(this.tileset, this.left, this.top, this.width, this.height);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawAtlas(
      tileset,
      [
        RSTransform.fromComponents(
            rotation: 0.0,
            scale: 1.0,
            anchorX: 0.0,
            anchorY: 0.0,
            translateX: 0.0,
            translateY: 0.0)
      ],
      [
        Rect.fromLTWH(
          left.toDouble(),
          top.toDouble(),
          width.toDouble(),
          height.toDouble(),
        )
      ],
      [],
      BlendMode.src,
      null,
      Paint(),
    );
  }

  @override
  bool shouldRepaint(SquarePainter oldDelegate) => false;
  @override
  bool shouldRebuildSemantics(SquarePainter oldDelegate) => false;
}