[Flutter] 평생 게임은 인생입니다.

개시하다


대단하지 않은 사람인생 놀이은 Fluter 웹으로 만들어 보았다.
컨디션
기계: M1 맥북 에어
편집기:VScode
보관소: https://github.com/kenta-wakasa/game_of_life

만든 물건


생활 게임이란 작은 칸이 알고리즘에 따라 생기거나 죽는 게임이다.

술수를 부리다

  • 초기 조건을 결정하기 위해 셀을 마음대로 채웁니다
  • .
  • 재생성 버튼을 눌러 시작
  • 조망
  • 특별한 게임성은 없지만 간단한 규칙으로 복잡한 도형이 생생하다.
    이것은 불가사의해서 보자마자 매우 안심이 되는 게임이다.
    여기.부터는 놀 수 있으니 잠 못 이루는 밤에 놀아주세요.

    생명 게임 알고리즘


    일생의 게임에서 세포는 생과 사의 상태에 있다.
    이들 세포의 주위에는 8칸에 사는 몇 개의 세포가 다음 세대의 생사를 결정한다.
    다음은 활칸과 사칸 각자의 알고리즘을 소개한다.

    활성 셀


    주위에 사는 칸이 2개 혹은 3개인 상태에서 다음 세대는 살아남을 수 있다.

    고정 셀


    주위에 사는 세 단원은 다음 세대에 부활할 수 있다.

    구체적 예


    셀을 다음과 같이 나타냅니다.
    살다🟠
    죽다🔵
    가운데 칸이 어떻게 되는지 봅시다.

    생존의 예


    🟠🟠🔵      🔵🟠🔵      🔵🔵🟠      🟠🔵🔵
    🟠🟠🔵      🟠🟠🟠      🔵🟠🔵      🔵🟠🟠
    🔵🔵🔵      🔵🟠🔵      🟠🔵🔵      🔵🔵🔵
    

    죽은 예


    🟠🟠🔵      🟠🟠🟠      🔵🔵🔵      🔵🔵🔵
    🟠🟠🔵      🟠🟠🟠      🔵🟠🔵      🔵🟠🔵
    🔵🔵🟠      🟠🟠🟠      🟠🔵🔵      🔵🔵🔵
    

    부활의 예


    🟠🟠🔵      🟠🟠🟠      🟠🔵🔵      🔵🔵🟠
    🔵🔵🔵      🔵🔵🔵      🔵🔵🟠      🔵🔵🔵
    🔵🔵🟠      🔵🔵🔵      🔵🟠🔵      🟠🔵🟠
    
    두루뭉술하게 말하면 너무 외롭든 밀집적이든 모두 죽는다.
    적당한 거리감이 번영할 것이다.
    인생이야.

    실시 방침

  • Cell class 설치
  • 셀 그리기
  • 초기 조건의 확정 방법
  • 알고리즘의 실현
  • 1. Cell class 설치


    칸은width× 하이라이트 수량밖에 없어요.
    나는
    List.generate(width * height, (index) => false);
    
    이런 느낌으로 생성하는 것이 가장 빠르다고 생각한다.
    1차원 배열이라도
    0 1 2
    3 4 5
    6 7 8
    
    이렇게 이해하시면 됩니다.
    이 때의 x 좌표와 y 좌표는
    x: index  % width
    y: index ~/ width
    
    로 표현할 수 있습니다.
    하지만 이번에는 특별히 x 좌표와 y 좌표를 직접 보유한 반을 만들었다.
    나는 이것이 더욱 직관적이고 가독성도 높아질 것이라고 생각한다.
    cell.dart
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    
    
    class Cell {
      const Cell({
        this.alive = false,
         this.pos,
      });
    
      /// 持っている変数はこの二つだけです。
      /// point は math.dart で定義されています。 x と y を持つので使わせてもらっています。
      final bool alive;
      final Point<int> pos;
    
      Cell copyWith({bool alive, Point<int> pos}) =>
          Cell(alive: alive ?? this.alive, pos: pos ?? this.pos);
    
      /// これは width と height を与えるとその数分のリストを生成してくれるメソッドです。
      static List<Cell> generateCells(int width, int height) {
        final cellList = <Cell>[];
        for (var y = 0; y < height; y++) {
          for (var x = 0; x < width; x++) {
            cellList.add(Cell(pos: Point<int>(x, y)));
          }
        }
        return cellList;
      }
    
      /// これは index から 座標変換の逆ですね。
      /// 座標から index に変換しています。
      /// せっかく x と y の情報を持たせたのにと思ったそこのあなた。自分もそう思いました。
      /// ですが、index 使った方が検索性が高くて都合がいいんですよね。
      static int pointToIndex(Point<int> point, int width) {
        return point.x + point.y * width;
      }
    
      /// 周囲のセルの場所を返すメソッドです
      /// (-1, -1) ( 0, -1), ( 1, -1)
      /// (-1,  0) ( 0,  0), ( 1,  0)
      /// (-1,  1) ( 0,  1), ( 1,  1)
      /// このように考えると 自分の位置から x と y の方向に -1 だけずれたところから開始して
      /// 入れ子の for を回せばよさそうですね。あとは自分自身は除く処理を加えれば完成です。
      List<Point<int>> get getPointAroundMe {
        final pointList = <Point<int>>[];
        for (var x = 0; x < 3; x++) {
          for (var y = 0; y < 3; y++) {
            final point = Point<int>(pos.x - 1 + x, pos.y - 1 + y);
            if (pos != point) {
              pointList.add(point);
            }
          }
        }
        return pointList;
      }
    }
    

    2. 셀 그리기


    Flutter에는 여러 가지 셀을 표현하는 방법이 있습니다.
    갑자기 생각난 건GridView 배열Container 방법이야.
    GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: width
        ),
        itemCount: width * height,
        itemBuilder: (BuildContext context, int index) {
            return Container( 色とか大きさとか );
        },
    )
    
    이런 느낌으로 되겠죠.
    하지만× 100개 정도의 칸을 배열하면 상당히 무거운 운동으로 응고된다.
    따라서 이번에는 낮은 층의 그리기 기법CustomPaint을 사용하여 격자를 표현했다.
    painter.dart
    import 'package:flutter/material.dart';
    import 'package:game_of_life/cell.dart';
    
    class Painter extends CustomPainter {
      Painter({
         this.basicLength,
         this.width,
         this.height,
         this.cells,
      });
      double basicLength; // 1 グリッドの長さ
      int width;
      int height;
      List<Cell> cells;
    
      void paintCells(Canvas canvas, Paint paint, List<Cell> cells) {
        for (final cell in cells) {
          if (cell.alive) {
            /// 正方形を描画しているのはこの部分
            /// Rect.fromLTWH は
            /// legt, top, width, height を与えて 長方形を作る つまり...
            /// 左上の x 座標, 左上の y 座標, 指定した頂点から x 方向への長さ, 指定した頂点から y 方向への長さ
            /// これで 長方形を表現する。
            canvas.drawRect(
              Rect.fromLTWH(
                basicLength * cell.pos.x,
                basicLength * cell.pos.y,
                basicLength,
                basicLength,
              ),
              paint,
            );
          }
        }
      }
    
      
      void paint(Canvas canvas, Size size) {
        final paint = Paint()
          ..style = PaintingStyle.fill
          ..color = Colors.yellow[700];
    
        paintCells(canvas, paint, cells);
      }
    
      
      bool shouldRepaint(Painter oldDelegate) {
        /// 再描画可能かどうかのフラグ
        return true;
      }
    }
    
    여기에 정의된 Painter 클래스는 예를 들어 이렇게 사용됩니다.
    body: CustomPaint(
        painter: Painter(
        basicLength: _provider.baseLength,
        height: _provider.height,
        width: _provider.width,
        cells: _provider.cellList,
        ),
    ),
    
    작은 위젯을 사용해 보십시오.

    3. 초기 조건의 결정 방법


    게임 시작 시 랜덤으로 채워지는 곳을 만드는 방법 등을 고려할 수 있다.
    이번에는 초기 조건을 자유롭게 정하고 싶어 클릭한 곳의 bool이 반전됐다.
    위젯을 클릭하면Inkwell 랩을 쓰면 되지만 이번엔 커스텀페인트를 사용했기 때문에 안 된다.
    이때 GestureDetector를 사용하여 화면의 좌표를 직접 얻는다.
    얻은 좌표와 상응하는 셀을 연결하여 반전시키는 방침을 취하다.
    좌표를 얻는 것은 매우 간단하다.
    GestureDetector(
        /// onTapDown はクリックした時に呼ばれる details のなかにいろいろ情報が入っている。
        onTapDown: (details) {
            if (_provider.editable) {
                /// .localPosition で親 Widget の中での相対的な位置を取得することができる。
                print(details.localPosition);
            }
        },
    )
    
    여기서 얻은position 정보를 idnex 정보로 변환하여 이용한다.
    변환된 코드는 이런 느낌입니다.
    int positionToIndex(Offset position) =>
          position.dx ~/ baseLength + position.dy ~/ baseLength * width;
    
    셀 클래스가 정의한point ToIndex에는 baseLength만 추가되었습니다.

    4. 알고리즘의 실현


    셀 클래스에서 주위 8칸의 좌표 목록을 추출하는 방법을 정의했기 때문에 간단합니다.
    /// 全ての cells について次の世代の生死を調べる
      void _nextGenerations() {
    
        /// もとあるリストに上書きしていく計算途中で値が変化していってしまいます。
        /// なので一度別リストとして避難させています。
        final tmpList = List<Cell>.from(_cellList);
        for (var index = 0; index < cellList.length; index++) {
          final pointList = cellList[index].getPointAroundMe;
          var count = 0;
          for (final point in pointList) {
            /// 座標が 設定した width height を超えていないかチェックしています。
            if (point.x > -1 &&
                point.x < width &&
                point.y > -1 &&
                point.y < height) {
    
                /// ここでわざわざ index に戻して使っています。
                /// こうすれば配列を直接指定して調べられますね。
              if (cellList[Cell.pointToIndex(point, width)].alive) {
                count++;
              }
            }
          }
          /// セルが生きていれば 2, 3 で生存
          if (cellList[index].alive) {
            if (count < 2 || count > 3) {
              tmpList[index] = tmpList[index].copyWith(alive: false);
            }
            /// セルが死んでいれば 3 で誕生
          } else {
            if (count == 3) {
              tmpList[index] = tmpList[index].copyWith(alive: true);
            }
          }
          /// 現状維持を除くと、条件ってこの二つしかないんですよね。
        }
        /// 最後に一時退避していたリストを元のリストに返して更新をかけています。
        _cellList = tmpList;
      }
    
    이상은 해설!코드의 전체 이미지는 여기를 참고하세요.
    https://github.com/kenta-wakasa/game_of_life

    최후


    widget을 사용함으로써 무게가 이렇게 많이 변할 수 있다는 것을 실감했습니다.
    이것과는 다른 실장 지침으로 만든 것을 보고 싶다.
    나는 시간을 보내기 위해 하루 정도면 완성할 수 있을 것 같다.

    좋은 웹페이지 즐겨찾기