之前使用 Flutter 里的游戏引擎 Flare 已经开发了一版这个游戏。在文章里我说要用 Widget 再来做一次。现在兑现我的承诺,并且上周日在 B 站直播了整个开发过程
请看这篇文章《手写一个在 Flutter 里展示”精灵图“的 Widget 》
首先将飞机放置在画面正中,由于 Widget 的原点统一为左上角,所以要减去飞机图像宽和高的一半。
//获得画布的宽高
Size screenSize = window.physicalSize/window.devicePixelRatio;
//将飞机的 x,y 坐标设定为画面中心
playerLeft = screenSize.width/2-66/2;
playerTop = screenSize.height/2-82/2;
飞机我们需要捕获到用户的手势事件,使用 GestureDetector 这个 Widget 来拖动飞机。
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
playerLeft += details.delta.dx;
playerTop += details.delta.dy;
});
},
child://飞机的 Widget
}
由于没有使用游戏引擎,所以只好自己通过定时器来实现。比如我们要实现 60FPS 的刷新率,可以将定时器设置为 17 毫秒,这样的话刷新率约等于 59fps 。当然可以更精确一些,但没有那个必要。
Timer.periodic(Duration(milliseconds: 17), (timer) {
gameloop();
});
gameloop(){
setState(() {
//触发 build 方法
});
}
不过我这里建议设置为每20
毫秒刷新一次,原因在后面会讲。
我们建立一个子弹管理数组,将所有子弹的数据都放在数组中
List bulletsData = [];
addBullet(){
double bulletX;
double bulletY;
if (Random().nextBool()) {
bulletX = Random().nextDouble() * (screenSize.width + bulletSize.width) -
bulletSize.width;
bulletY = Random().nextBool() ? -bulletSize.height : screenSize.height;
} else {
bulletX = Random().nextBool() ? -bulletSize.width : screenSize.width;
bulletY = Random().nextDouble() * (screenSize.height + bulletSize.height) -
bulletSize.height;
}
bulletsData.add({
"x":bulletX,
"y":bulletY,
"speed": (1+gameTime/10) + Random().nextDouble()*3,
"angle": atan2(((bulletY + bulletSize.height/2) - (playerTop + playerHeight / 2)),
((bulletX + bulletSize.width) - (playerLeft + playerWidth / 2)))
});
}
在gameloop
中遍历数组对子弹进行移动。
for (int i = bulletsData.length - 1; i >= 0; i--) {
var bulletItem = bulletsData[i];
double angle = bulletItem["angle"];
double speed = bulletItem["speed"];
bulletItem["x"] -= cos(angle) * speed;
bulletItem["y"] -= sin(angle) * speed;
if (isHitPlayer(bulletItem["x"], bulletItem["y"])) {
print("gameOver");
gameOver();
}
if (isNotInScreen(bulletItem["x"], bulletItem["y"])) {
print("bullet removed");
bulletsData.removeAt(i);
continue;
}
}
}
上述代码完成后,我们的子弹在数据中就存在了。但是你看不见它们,因为他们没有被绘制到画面中。我们需要利用Stack
和Positioned
控件来展示它们。
注意:这里我是故意为之,如果真的做游戏那么子弹粒子最好使用 CustomPainter 来实现
Stack(
children: getBulletsWidget(),
)
getBulletsWidget(){
List<Positioned> bullets = [];
for(int i = 0;i<bulletsData.length;i++){
var bulletItem = bulletsData[i];
// print(bulletItem);
var bulletWidget = Positioned(
left: bulletItem["x"],
top: bulletItem["y"],
child: bulletImage
);
bullets.add(bulletWidget);
}
return bullets;
}
既然游戏标题叫“是男人就坚持 100 秒”,那游戏中肯定需要一个按秒的计时器。还记得前面为什么我建议将计时器的刷新频率设置为 20 毫秒吗?这样的话,我们每刷新 50 次是不是就是 1 秒钟呢?
Timer.periodic(Duration(milliseconds: 20), (timer) {
if(timer.tick%50==0){
gameSeconds+=1;
//seconds
}
loop();
});
在 Flutter 里我们可以这样做,timer
里的tick
是一个计时器的执行计数,会不断累计,所以我们只需要对 50 取余,每次整除 50 的时候就是 1 秒钟啦~
借助 Flutter 强大的跨端能力,这个游戏我们可以...
flutter run -d macOS
flutter run -d Chrome
flutter run -d 模拟器 ID
还有 Linux,Windows,Android 我就不一一给大家截图了
项目已开源,请自行运行吧!
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.