Question about Isolate in Flutter and match-3 game implementation
Published: 2023-06-13 00:50:23

I have been making match-3 game for implementing tile matching detection code. I have tried three kind of variations that do the work. The following error occurs each time known solution is tried:


ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'package:flutter/src/scheduler/ticker.dart' Class: TickerFuture (see restrictions listed at `SendPort.send()` documentation for more information)


I have experienced as if type mistake which I didn't do, when I copy and paste the same code I wrote then syntax error does not continue. But the error the above is occurring any codes as solution, changing many things to code and reverted back, the file location error occurred changed to scheduler/ticker.dart.

I know the file is resposible for animation work, but there is no problem at all.

I am worrying I had to be like Shimomura to resolve this problem, the following code would contain type mistake while I write this asking too.

The following is code:

```
import 'dart:async';
import 'dart:isolate';
import 'dart:math';

import 'package:flutter/material.dart';

// ignore: must_be_immutable
class Tile extends StatelessWidget {
  List mBitmap = [
    Image.asset('assets/images/games/match-3/tile0.jpg'),
    Image.asset('assets/images/games/match-3/tile1.jpg'),
    Image.asset('assets/images/games/match-3/tile2.jpg'),
    Image.asset('assets/images/games/match-3/tile3.jpg'),
  ];

  var mBitmapDestroy = Image.asset('assets/images/games/match-3/heart.jpg');

  late int value;
  late int row;
  late int col;

  late double mTileWidth;
  late double mTileHeight;

  Tile({required this.value, required this.row, required this.col, Key? key})
      : super(key: key);

  get image {
    switch (value) {
      case 0:
        return mBitmap[0];
      case 1:
        return mBitmap[1];
      case 2:
        return mBitmap[2];
      case 3:
        return mBitmap[3];
      case 4:
        return mBitmapDestroy;
    }
  }

  get getRow => row;

  get getCol => col;

  get getValue => value;

  setRow(int row) {
    this.row = row;
  }

  setCol(int col) {
    this.col = col;
  }

  setValue(int value) {
    this.value = value;
  }

  @override
  Widget build(BuildContext context) {
    mTileWidth = MediaQuery.of(context).size.width;
    mTileHeight = mTileWidth;

    return SizedBox(
      width: mTileWidth,
      height: mTileHeight,
      child: image,
    );
  }
}

class TileMatchingGamePage extends StatelessWidget {
  const TileMatchingGamePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => const TileMatchingGameStatePage();
}

class TileMatchingGameStatePage extends StatefulWidget {
  const TileMatchingGameStatePage({Key? key}) : super(key: key);

  @override
  State<TileMatchingGameStatePage> createState() =>
      _TileMatchingGameStatePage();
}

class _TileMatchingGameStatePage extends State<TileMatchingGameStatePage>
    with SingleTickerProviderStateMixin {
  late List<List> tiles;
  late List<List> toRemove;
  late List<List<Tween<Offset>>> _offsetTweens;
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    tiles = List.generate(
      7,
      (i) => List.generate(
        7,
        (j) => Tile(value: Random().nextInt(4), row: i, col: j),
      ),
    );

    toRemove = List.generate(
      7,
      (i) => List.generate(
        7,
        (j) => false,
      ),
    );

    _offsetTweens = List.generate(
      7,
      (i) => List.generate(
        7,
        (j) => Tween<Offset>(begin: Offset.zero, end: Offset.zero),
      ),
    );
  }

  int mScore = 0;
  int mCombo = 0;

  bool checkAdjacentTiles() {
    bool bFound = false;

    // 모든 칸 리셋
    for (int x = 0; x < 7; x++) {
      for (int y = 0; y < 7; y++) {
        toRemove[x][y] = false;
      }
    }

    // 전체 순회, 3연속 찾기
    for (int x = 0; x < 7; x++) {
      for (int y = 0; y < 7; y++) {
        int value = tiles[x][y].getValue;

        // 수평 3개가 같으면 제거 대상
        if (x > 0 && x < 7 - 1) {
          if ((tiles[x - 1][y].getValue == value) &&
              (tiles[x + 1][y].getValue == value)) {
            toRemove[x - 1][y] = true;
            toRemove[x][y] = true;
            toRemove[x + 1][y] = true;
            bFound = true;
          }
        }

        // 수직 3개가 같으면 제거 대상
        if (y > 0 && y < 7 - 1) {
          if ((tiles[x][y - 1].getValue == value) &&
              (tiles[x][y + 1].getValue == value)) {
            toRemove[x][y - 1] = true;
            toRemove[x][y] = true;
            toRemove[x][y + 1] = true;
            bFound = true;
          }
        }
      }
    }

    mCombo++;

    return bFound;
  }

  void removeTiles(_) {
    setState(() {
      int count = 0;
      for (int x = 0; x < 7; x++) {
        for (int y = 0; y < 7; y++) {
          if (toRemove[x][y]) {
            tiles[x][y] = Tile(value: 4, row: x, col: y);
            count++;
          }
        }
      }

      mScore += (count * mCombo);

      bool bFound = false;
      for (int y = 7 - 1; y >= 0; y--) {
        bFound = false;
        for (int x = 0; x < 7; x++) {
          if (toRemove[x][y]) {
            bFound = true;
            for (int ty = y; ty > 0; ty--) {
              tiles[x][ty].setValue(tiles[x][ty - 1].getValue);
            }
            tiles[x][0].setValue(4);
          }
        }

        if (bFound) y++;
      }
    });

    Future.delayed(const Duration(milliseconds: 600));

    setState(() {
      for (int x = 0; x < 7; x++) {
        for (int y = 0; y < 7; y++) {
          if (tiles[x][y].getValue == 4) {
            tiles[x][y] = Tile(value: Random().nextInt(4), row: x, col: y);
          }
        }
      }
    });
  }

  void makeNewTiles(_) {
    setState(() {
      for (int x = 0; x < 7; x++) {
        for (int y = 0; y < 7; y++) {
          if (tiles[x][y].getValue == 4) {
            tiles[x][y] = Tile(value: Random().nextInt(4), row: x, col: y);
          }
        }
      }
    });
  }

  void updateRowCol() {
    setState(() {
      for (int i = 0; i < 7; i++) {
        for (int j = 0; j < 7; j++) {
          tiles[i][j].setRow(i);
          tiles[i][j].setCol(j);
        }
      }
    });
  }

  /*
  void doTileMatchingWork() {
    if (checkAdjacentTiles()) {
      compute(removeTiles, null);
      compute(makeNewTiles, null);
    }
    updateRowCol();
  }*/

  doTileMatchingWork() async {
    ReceivePort mReceivePort = ReceivePort();
    if (checkAdjacentTiles()) {
      var receivePort = ReceivePort();
      Isolate isolate = await Isolate.spawn<SendPort>(isolateEntry,
          mReceivePort.sendPort);

      SendPort anotherPort = await mReceivePort.first;
      ReceivePort anotherResponseReceiePort = ReceivePort();

      receivePort.listen((data) {
        switch (data) {
          case "makeNewTiles":
            anotherPort.send(["makeNewTiles", anotherResponseReceiePort.sendPort]);
            break;
          case "updateRowCol":
            anotherPort.send(["updateRowCol", anotherResponseReceiePort.sendPort]);
            break;
        }
      });
    }
  }

  void isolateEntry(SendPort mSendPort) async {
    ReceivePort anotherPort = ReceivePort();
    mSendPort.send(anotherPort.sendPort);
    late var _ = "";

    await for (var message in anotherPort) {
      if (message is List) {
        var function = message[0];
        switch (function) {
          case "removeTiles":
            removeTiles(_);
            anotherPort.sendPort.send("makeNewTiles");
            break;
          case "makeNewTiles":
            makeNewTiles(_);
            anotherPort.sendPort.send("updateRowCol");
            break;
          case "updateRowCol":
            updateRowCol();
            break;
        }
      }
    }
  }

  void swapTiles(int i, int j, int k, int l) {
    setState(() {
      final temp = tiles[i][j];
      tiles[i][j] = tiles[k][l];
      tiles[k][l] = temp;
    });
  }

  bool _isAnimating = false;

  void animateAndSwap(int di, int dj, AxisDirection direction) {
    if (_isAnimating) return;
    _isAnimating = true;
    // 아래쪽 코드는 flutter 3.10 (dart 3.0) 이후로 훨씬 간단히 줄일 수 있음.
    final si = di +
        (direction == AxisDirection.up
            ? -1
            : direction == AxisDirection.down
                ? 1
                : 0);
    final sj = dj +
        (direction == AxisDirection.left
            ? -1
            : direction == AxisDirection.right
                ? 1
                : 0);
    final Offset dragOffset =
        direction == AxisDirection.up || direction == AxisDirection.down
            ? Offset(0, direction == AxisDirection.up ? -1 : 1)
            : Offset(direction == AxisDirection.left ? -1 : 1, 0);
    final swapOffset =
        direction == AxisDirection.up || direction == AxisDirection.down
            ? Offset(0, direction == AxisDirection.up ? 1 : -1)
            : Offset(direction == AxisDirection.left ? 1 : -1, 0);
    if (si < 0 || si >= 7 || sj < 0 || sj >= 7) {
      _isAnimating = false;
      return;
    }
    _offsetTweens[di][dj].end = dragOffset;
    _offsetTweens[si][sj].end = swapOffset;
    _controller.reset();
    _controller.forward().whenCompleteOrCancel(() {
      _offsetTweens[di][dj].end = Offset.zero;
      _offsetTweens[si][sj].end = Offset.zero;
      swapTiles(di, dj, si, sj);
      _isAnimating = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      shrinkWrap: true,
      itemCount: 7 * 7,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 7,
      ),
      itemBuilder: (BuildContext context, int index) {
        int i = index ~/ 7;
        int j = index % 7;

        return SlideTransition(
          position: _offsetTweens[i][j].animate(_controller),
          child: GestureDetector(
            // 가로 방향으로 드래그할 때 콜백 함수
            onHorizontalDragEnd: (details) {
              // 드래그한 방향에 따라 인접한 타일의 인덱스 설정
              final dx = details.primaryVelocity!;
              if (dx > 0) {
                animateAndSwap(i, j, AxisDirection.right);
                doTileMatchingWork();
              } else if (dx < 0) {
                // 왼쪽으로 드래그하면 왼쪽 타일과 교환
                animateAndSwap(i, j, AxisDirection.left);
                doTileMatchingWork();
              }
            },
            onVerticalDragEnd: (details) {
              // 드래그한 방향에 따라 인접한 타일의 인덱스 설정
              final dy = details.primaryVelocity!;
              if (dy > 0) {
                animateAndSwap(i, j, AxisDirection.down);
                doTileMatchingWork();
              } else if (dy < 0) {
                animateAndSwap(i, j, AxisDirection.up);
                doTileMatchingWork();
              }
            },
            child: tiles[i][j],
          ),
        );
      },
    );
  }
}
```

Can you address what is wrong and isolate is legitimate for this code? If not using isolate, app is freezing when it runs. The tile shows 100KB image each, and setState() and mutli for loop and 2-dimensional lists.

I have made sure the code works even before freezing observed, debugger no longer shows buttons and shortcut keys to test step by step inspection.

How can I do for?