flutter推箱子小游戏
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非常流畅)