flutter推箱子小游戏

发布时间 2023-08-25 14:05:17作者: tieyan

flutter无疑是目前最火的前端开发框架其中之一,一套代码可以运行在ios, android, windows, linux, mac, web (mpflutter甚至可以开发小程序)。
今年参加godot开发者比赛用godot写了一个推箱子pc小游戏。前阵子又看到google基于 https://github.com/flame-engine/flame 写的pinball, 计划重新用flame实现一下推箱子练练手, 但是实操过程中flame的体验比godot差太多了,又全部删除了flame, 只用flutter基础控件实现

素材

图片:https://kenney.nl/assets/sokoban
地图:自己写算法随机生成 或者 http://www.sourcecode.se/sokoban/levels.php python写个爬虫下载然后解析

比较通用的规则

# 墙体
  空地
@ 人物
. 目标
$ 箱子
* 箱子在目标上
+ 人物在目标上

关卡选择

PageView 生成分页(当前采集了1000多个地图,一共49页)

    PageView.builder(
              onPageChanged: (pageIndex) {
                textEditingController.text =
                    '${pageIndex + 1} / ${(sokDb.length / PAGE_HAS_BUTTON).ceil()} 页';
              },
              controller: controller,
              itemBuilder: (BuildContext context, int pageIndex) {
                if (pageIndex >= (sokDb.length / PAGE_HAS_BUTTON).ceil()) {
                  return null;
                }

                final model = Provider.of<SokobanModel>(context);

                return Padding(
                  padding: EdgeInsets.all(0.05.sw),
                  child: Card(
                    child: GridView.count(
                      crossAxisCount: 5,
                      children: List.generate(
                          min(sokDb.length - PAGE_HAS_BUTTON * pageIndex,
                              PAGE_HAS_BUTTON), (index) {
                        var currentIndex =
                            pageIndex * PAGE_HAS_BUTTON + index + 1;

                        if (model.getMaxLevel() >= currentIndex) {
                          return InkWell(
                              child: Center(
                                child: Text(
                                  '$currentIndex',
                                ),
                              ),
                              onTap: () {
                                Navigator.pushNamed(context, GamePage.routeName,
                                    arguments: currentIndex);
                              });
                        }

                        return const Icon(Icons.lock);
                      }),
                    ),
                  ),
                );
              },
            ),

游戏画面

根据地图使用Stack进行布局,AnimatedPositioned用于动画

 final ground_03 = Image.asset(
      'assets/images/ground_03.png',
      width: perSize,
      height: perSize,
      fit: BoxFit.fill,
    );

    final block_06 = Image.asset(
      'assets/images/block_06.png',
      width: perSize,
      height: perSize,
      fit: BoxFit.fill,
    );

    final crate_27 = Image.asset(
      'assets/images/crate_27.png',
      width: perSize,
      height: perSize,
      fit: BoxFit.fill,
    );

    for (int x = 0; x < tileCount; ++x) {
      for (int y = 0; y < tileCount; ++y) {
        switch (sokobanStatusOrig[x * tileCount + y]) {
          case ' ':
          case r'@':
          case r'$':
            ret.add(Positioned(
              left: perSize * y,
              top: perSize * x,
              child: ground_03,
            ));
            break;
          case '#':
            ret.add(Positioned(
              left: perSize * y,
              top: perSize * x,
              child: block_06,
            ));
            break;
          case '*':
          // ret.add(Positioned(
          //   left: perSize * y,
          //   top: perSize * x,
          //   child: crate_27,
          // ));
          // break;
          case '+':
          case '.':
            ret.add(Positioned(
              left: perSize * y,
              top: perSize * x,
              child: crate_27,
            ));
            break;
        }
      }
    }

    final crate_07 = Image.asset(
      'assets/images/crate_07.png',
      width: perSize,
      height: perSize,
      fit: BoxFit.fill,
    );

    final crate_42 = Image.asset(
      'assets/images/crate_42.png',
      width: perSize,
      height: perSize,
      fit: BoxFit.fill,
    );

    for (var element in boxes) {
      ret.add(AnimatedPositioned(
        curve: Curves.decelerate,
        duration: const Duration(milliseconds: MOVE_TIME),
        left: perSize * element.y,
        top: perSize * element.x,
        child: origIsGoal(element.x, element.y) ? crate_42 : crate_07,
      ));
    }

    ret.add(AnimatedPositioned(
      curve: Curves.decelerate,
      onEnd: moveEnd,
      duration: const Duration(milliseconds: MOVE_TIME),
      left: perSize * playerPos.y,
      top: perSize * playerPos.x,
      child: Hero(
        tag: HERO_TAG,
        child: Image.asset(
          'assets/images/player_03.png',
          width: perSize,
          height: perSize,
          fit: BoxFit.fill,
        ),
      ),
    ));

箱子移动的逻辑其实很简单,以向上为例子。 判断人物上面一格是否可移动如果是直接移动。如果上一格是箱子再判断上两格是否可移动(移动后同时保持当前的格子状态用于undo操作)

bool goUp() {
    bool ret = false;
    if (!moveFinish) {
      return ret;
    }

    String dir1 = getPosState(playerPos.x - 1, playerPos.y);
    if (dir1 == ' ' || dir1 == '.') {
      ret = true;
      saveStep();

      if (origIsGoal(playerPos.x, playerPos.y)) {
        setCurrentPosState(playerPos.x, playerPos.y, '.');
      } else {
        setCurrentPosState(playerPos.x, playerPos.y, ' ');
      }

      setCurrentPosState(playerPos.x - 1, playerPos.y, '@');
      playerPos = Point(playerPos.x - 1, playerPos.y);
    } else if (dir1 == r'$' || dir1 == r'*') {
      String dir2 = getPosState(playerPos.x - 2, playerPos.y);
      if (dir2 == ' ' || dir2 == '.') {
        ret = true;
        saveStep();

        if (origIsGoal(playerPos.x, playerPos.y)) {
          setCurrentPosState(playerPos.x, playerPos.y, '.');
        } else {
          setCurrentPosState(playerPos.x, playerPos.y, ' ');
        }

        setCurrentPosState(playerPos.x - 1, playerPos.y, '@');
        if (origIsGoal(playerPos.x - 2, playerPos.y)) {
          setCurrentPosState(playerPos.x - 2, playerPos.y, '*');
        } else {
          setCurrentPosState(playerPos.x - 2, playerPos.y, r'$');
        }

        moveBox(playerPos.x - 1, playerPos.y, playerPos.x - 2, playerPos.y);
        playerPos = Point(playerPos.x - 1, playerPos.y);
      }
    }

    return ret;
  }

自动寻路

每关都附赠答案,如果不想消耗脑细胞了(gif录屏看着卡其实flutter非常流畅)