利用Flutter制作经典贪吃蛇游戏
作者:大前端之旅 时间:2023-07-21 07:04:13
前言
Flutter (Channel stable, 2.10.3, on Microsoft Windows [Version 10.0.19044.1586], locale zh-CN)
Android toolchain - develop for Android devices (Android SDK version 30.0.3)
Flutter 框架让你可以使用单一代码库为 Android、iOS、Web ,桌面平台构建应用程序。尽管许多大公司都将 Flutter 用于他们蓬勃发展的应用程序,包括 Google Pay 和阿里巴巴闲鱼,但没有多少人正在探索使用 Flutter 进行游戏开发。这就是你将在本教程中所做的。
由于 Flutter 能够以高达 60 FPS 的速度渲染 UI,你将利用该功能在 Flutter 中构建简单的 2D Snake 游戏。
在此过程中,你将学习如何:
使用 Flutter 作为游戏引擎
移动对象
控制运动
构建游戏用户界面
添加游戏元素
启动项目设置了竞争环境。你将在构建游戏时为其添加 UI 元素。入门项目还提供了一些有用的实用方法,因此你可以专注于更大的场景:编写游戏。以下是你可以在项目中找到的预构建函数,以及如何使用它们:
roundToNearestTens(int num):将传递的整数参数四舍五入到最接近的步长值。 这使你可以在假想的网格上获得与当前位置相距步进单位的确切下一个位置。
getRandomPositionWithinRange() :在游戏区域范围内的屏幕上生成一个随机位置。你将使用此函数生成一条新蛇并为蛇提供食物。
showGameOverDialog() :当 Snake 与游戏区域的任何边界发生碰撞时,显示一个样式化的对话框弹出窗口。此对话框显示用户的分数和重新开始游戏的按钮。
getRandomDirection([String type]) :随机返回四个方向之一:上、下、左或右。你主要使用此函数在 Snake 生成时将其沿随机方向移动。它可选地接受一个参数,指定你希望它返回的随机方向是水平还是垂直。
getRandomPositionWithinRange() :返回游戏区域内屏幕上的随机位置。它确保随机位置位于蛇移动的网格上。
getControls() : 实现
ControlPanel
,一个在屏幕上显示四个圆形按钮以控制蛇的移动的小部件。getPlayAreaBorder() :沿屏幕边缘绘制边框,表示屏幕移动的播放区域。
使用 Flutter 作为游戏引擎
游戏引擎是一套工具和服务,可帮助游戏开发者构建游戏。他们处理很多不同的事情,比如图形、声音、人工智能、用户输入、网络等等。Flutter 也可以处理大部分这些事情。作为 Flutter 开发人员,你可以访问整个世界的 Dart 插件。另外,如果不存在,你始终可以自己编写代码。
要从 Flutter 作为游戏开发引擎开始,你将使用框架的渲染功能来为蛇、食物和游戏分数等游戏对象设置动画。在这种情况下,你将使用计时器每秒重新生成 60 次 UI,并在屏幕上显示一条蛇。通过在改变其位置的同时以高达 60 FPS 的速度渲染蛇,你可以获得非常平滑的移动动画效果。
像 Unity 这样的游戏引擎以类似的方式工作。这里的不同之处在于,过多地推动框架的限制可能最终导致需要大量资源的应用程序。然而,在测试这款游戏时,Flutter 的表现惊人地好,根本没有加热设备。
画蛇
你的第一步是创建蛇。你将从启动Piece
项目中的小部件开始,它会在屏幕上呈现一个彩色圆圈。使用此小部件,你将逐个绘制蛇以及蛇食。
但是,在绘制蛇之前,有必要花点时间了解使用 Flutter 进行 2D 渲染的基础知识。
2D 渲染的基础
要渲染任何在屏幕上不断改变其位置的东西,你需要使用 和 之类的小Stack
部件Positioned
。给定 x 和 y 坐标,这些小部件可以将子小部件放置在屏幕上的任何位置。
要记住的最重要的事情是,你正在开发一个移动应用程序,该应用程序需要在具有各种屏幕尺寸和纵横比的设备上运行。这意味着你不能对游戏领域的任何维度进行硬编码。相反,你将使用MediaQuery
获取用户屏幕的宽度和高度,然后根据这些值计算位置。
例如,这是一个简单的代码片段,它声明变量,然后将屏幕的宽度和高度分配给它们。
final screenSize = MediaQuery.of(context).size;
final screenWidth = screenSize.width;
final screenHeight = screenSize.height;
在上面的代码片段中,MediaQuery
继承的小部件用于获取当前设备屏幕的宽度和高度。这需要传递内容,因此,我们在其内部拥有此代码build()
。
既然你知道如何在屏幕上放置对象,就该画蛇了。
创建蛇
你将使用它Piece
来创建蛇。首先,你将根据蛇在屏幕上的位置进行创建。然后,你会将组成蛇的所有部分的位置存储在被调用的。
首先将getPieces()
位于lib/game.dart中的 替换为:
List<Piece> getPieces() {
final pieces = <Piece>[];
draw();
drawFood();
// 1
for (var i = 0; i < length; ++i) {
// 2
if (i >= positions.length) {
continue;
}
// 3
pieces.add(
Piece(
posX: positions[i].dx.toInt(),
posY: positions[i].dy.toInt(),
// 4
size: step,
color: Colors.red,
),
);
}
return pieces;
}
这是上面代码中发生的事情:
你使用的
for loop
一直运行到它覆盖了蛇的整个长度。当蛇的长度与列表的长度不匹配时,
if
内部的块处理for loop
边缘情况。当蛇在吃一些食物后长度增加时,就会发生这种情况。``对于每次迭代,它都会创建一个
Piece
具有正确位置的 a 并将其添加到pieces
它返回的列表中。除了位置,你还通过
size
和。在这种情况下,大小是,这确保了 Snake 沿着网格移动,其中每个网格单元的大小都是。颜色值是个人喜好。随意使用自己的颜色。``
保存文件并让应用程序热重新加载。到目前为止,什么都没有发生,你也不会注意到 UI 中的任何变化。
填写列表
你需要实施draw()
以实际填写positions
列表。因此draw()
,在同一文件中替换为以下内容:
void draw() async {
// 1
if (positions.length == 0) {
positions.add(getRandomPositionWithinRange());
}
// 2
while (length > positions.length) {
positions.add(positions[positions.length - 1]);
}
// 3
for (var i = positions.length - 1; i > 0; i--) {
positions[i] = positions[i - 1];
}
// 4
positions[0] = await getNextPosition(positions[0]);
}
以下是上述函数的工作原理:
如果
positions
为空,则getRandomPositionWithinRange()
生成一个随机位置并开始该过程。如果蛇刚刚吃了食物,它
length
就会增加。增加了一个新的while loop
位置,positions
这样length
并且positions
始终保持同步。它检查
positions
的长度并移动每个位置。这会产生蛇在移动的错觉。最后,
getNextPosition()
将第一块蛇头移动到一个新位置。
你在这里需要做的最后一件事是实现 at getNextPosition()
。
将蛇移动到下一个位置
将以下代码添加到lib/game.dart中的函数中:
Future<Offset> getNextPosition(Offset position) async {
Offset nextPosition;
if (direction == Direction.right) {
nextPosition = Offset(position.dx + step, position.dy);
} else if (direction == Direction.left) {
nextPosition = Offset(position.dx - step, position.dy);
} else if (direction == Direction.up) {
nextPosition = Offset(position.dx, position.dy - step);
} else if (direction == Direction.down) {
nextPosition = Offset(position.dx, position.dy + step);
}
return nextPosition;
}
以下是上面代码的作用:
根据对象的当前位置及其方向值创建对象的新位置。改变方向会导致对象朝不同的方向移动。稍后你将使用控制按钮来执行此操作。
如果方向设置为right则增加 x 坐标值,如果方向设置为left则减小值。
同样,如果方向设置为向上,则增加 y 坐标的值,如果方向设置为向下,则减少它。
最后,
return Scaffold(
body: Container(
color: Color(0XFFF5BB00),
child: Stack(
children: [
Stack(
children: getPieces(),
),
],
),
),
);
在上面的代码片段中,我们只是添加了getPieces()
将小部件返回到堆栈的方法,因此我们可以在屏幕上看到一些 UI。请注意,如果你不将小部件添加到build()
,屏幕上不会发生任何变化。
保存所有内容并重新启动应用程序。你会看到的:
你可以看到一条蛇……它什么也没做。那是因为你没有添加任何东西来重建 UI。但是,一次又一次地保存文件并让热重载完成它的工作,你会看到蛇移动。
添加运动和速度
现在,让蛇移动所需的只是一种重建 UI 的方法。每次build
调用,都需要计算新的位置,并在Piece
屏幕上渲染新的 s 列表。为此,你将使用Timer
.
changeSpeed()
在lib/game.dart中添加以下定义:
void changeSpeed() {
if (timer != null && timer.isActive) timer.cancel();
timer = Timer.periodic(Duration(milliseconds: 200 ~/ speed), (timer) {
setState(() {});
});
}
上面的代码只是简单地重置了timer
一个包含speed
. speed
每次蛇吃食物时,你控制并增加它。最后,在计时器的每一个滴答声中,你都会调用setState()
,它会重建整个 UI。
changeSpeed()
接下来,从restart()
同一文件中调用:
void restart() {
changeSpeed();
}
timer
每次用户重新启动游戏时都会重新初始化。
保存所有文件并重新启动应用程序。现在,每次重新启动应用程序时,蛇都会向随机方向移动。
添加控件
现在你有了一条移动的蛇,你需要添加更多的旋钮和转盘,以便用户可以控制蛇的方向和速度。请记住以下几点:
你将使用 来控制运动和方向
ControlPanel
。每次蛇吃食物时,速度都必须增加。
restart()
重置蛇与边界框碰撞时的速度、长度和方向。发生碰撞后,你将向用户显示Game Over警报。
改变方向
ControlPanel
拥有让用户控制蛇的方向所需的一切。它由四个圆形按钮组成,每个按钮都会改变蛇的运动方向。该小部件位于lib/control_panel.dart中。随意挖掘并查看实现。
现在,将以下代码添加到getControls()
lib /game.dart 中:
Widget getControls() {
return ControlPanel( // 1
onTapped: (Direction newDirection) { // 2
direction = newDirection; // 3
},
);
}
在上面的代码片段中:
我们正在使用
ControlPanel
已经在启动项目中为你创建的小部件。ControlPanel 小部件呈现 4 个按钮,你将使用这些按钮来控制蛇的移动。我们还使用
onTapped
接收蛇移动的新方向作为参数的方法。我们
direction
用新的方向更新变量newDirection
。这将导致蛇改变方向。
此外,在文档顶部添加以下导入:
import 'control_panel.dart';
接下来,添加getControls()
为外部Stack
in的第二个孩子build()
:
@override
Widget build(BuildContext context) {
// ...
return Scaffold(
body: Container(
color: Color(0XFFF5BB00),
child: Stack(
children: [
Stack(
children: getPieces(),
),
getControls(),
],
),
),
);
}
上面的代码将 getControls 方法返回的小部件添加到 Stack 内屏幕上的 UI,但位于 UI 的其余部分之上。
保存文件并重新启动应用程序。现在,你将看到一组控制蛇方向的按钮。
点击按钮会改变蛇的方向。到目前为止,游戏在一个简单的级别上运行,但你仍然需要添加玩家应该努力的东西。只是在屏幕上移动一条五颜六色的蛇并不是很有趣,对吧?所以你的下一步是给蛇一些食物来加速它。
吃东西和提高速度
要在屏幕上渲染食物,你将Piece
再次使用,但你将更改其颜色。请记住,你不希望食物出现在任意位置。相反,它应该始终在游戏区域内的随机位置渲染,并且它应该始终位于蛇移动的网格上。
现在,你将实现在屏幕上drawFood()
呈现食物。Piece
将以下代码添加到drawFood()
同一文件中:
void drawFood() {
// 1
if (foodPosition == null) {
foodPosition = getRandomPositionWithinRange();
}
// 2
food = Piece(
posX: foodPosition.dx.toInt(),
posY: foodPosition.dy.toInt(),
size: step,
color: Color(0XFF8EA604),
isAnimated: true,
);
}
这是上面发生的事情。
上面的代码创建 a
Piece
并将其存储在food
.它存储对象
food
内部的位置 。最初,这是,因此你可以在游戏区域内的屏幕上任意位置随机渲染食物。`
在屏幕上显示食物
接下来,你需要添加food
到build
insideStack
以将其呈现在屏幕上。
@override
Widget build(BuildContext context) {
//...
return Scaffold(
body: Container(
color: Color(0XFFF5BB00),
child: Stack(
children: [
Stack(
children: getPieces(),
),
getControls(),
food,
],
),
),
);
}
保存所有文件并重新启动应用程序以查看实际更改。应用程序重新启动后,你会在屏幕上看到绿色食物。
然而,如果你试图让蛇找到食物,什么都不会发生——它只会越过食物图标。那是因为你没有做任何事情让蛇吃掉食物。接下来你会解决这个问题。
消耗和再生食物
现在,你需要检查蛇和食物在 2D 空间中是否处于相同坐标。如果是,你将对蛇进行一些更改并在新位置呈现一些新食物。
drawFood()
在第一个if
块结束后将以下代码添加到:
void drawFood() {
// ...
if (foodPosition == positions[0]) {
length++;
speed = speed + 0.25;
score = score + 5;
changeSpeed();
foodPosition = getRandomPositionWithinRange();
}
// ...
}
上面的代码只是检查你存储foodPosition
的位置和蛇的第一Piece
个小部件的位置是否相同。如果它们匹配,你将length
增加1
、speed
和。然后调用,它使用新设置重新初始化。0.25``score``5``changeSpeed()``timer
最后,你foodPosition
在屏幕上使用新的随机位置进行更新,从而呈现新的食物Piece
。
保存文件并重新启动应用程序以使更改生效。
蛇现在可以吃食物了。当它这样做时,它的长度会大大增加。
检测碰撞并显示游戏结束对话框
到目前为止,这条蛇可以自由地四处走动——这就造成了一个问题。没有什么可以阻止蛇走出矩形游乐区。你需要限制蛇的移动以保持在你在屏幕上定义的游戏区域内。
首先,使用 渲染该游戏区域getPlayAreaBorder()
,这会为游戏区域添加轮廓。只需添加getPlayAreaBorder()
到外部Stack
in build()
。
@override
Widget build(BuildContext context) {
//...
return Scaffold(
body: Container(
color: Color(0XFFF5BB00),
child: Stack(
children: [
getPlayAreaBorder(),
Stack(
children: getPieces(),
),
getControls(),
food,
],
),
),
);
}
上面的代码将food
小部件添加到Stack
inbuild()
所以现在它将呈现在屏幕上。
接下来,将以下代码添加到detectCollision()
:
bool detectCollision(Offset position) {
if (position.dx >= upperBoundX && direction == Direction.right) {
return true;
} else if (position.dx <= lowerBoundX && direction == Direction.left) {
return true;
} else if (position.dy >= upperBoundY && direction == Direction.down) {
return true;
} else if (position.dy <= lowerBoundY && direction == Direction.up) {
return true;
}
return false;
}
上面的函数检查蛇是否到达了四个边界中的任何一个。如果有,则返回true
,否则返回false
。它使用lowerBoundX
、upperBoundX
和变量lowerBoundY
来upperBoundY
检查蛇是否仍在游戏区域内。
接下来,你需要在每次生成蛇的下一个位置时使用detectCollision()
inside来检查碰撞。getNextPosition()
将以下代码添加到getNextPosition()
的声明之后nextPosition
:
Future<Offset> getNextPosition(Offset position) async {
//...
if (detectCollision(position) == true) {
if (timer != null && timer.isActive) timer.cancel();
await Future.delayed(
Duration(milliseconds: 500), () => showGameOverDialog());
return position;
}
//...
}
上面的代码检查碰撞。如果蛇与边界框发生碰撞,代码会取消计时器并向用户显示Game Over对话框showGameOverDialog()
。
保存所有文件并热重载应用程序。
这一次,如果蛇接触到周围的边界框,你将立即看到一个对话框,通知用户游戏结束并显示他们的分数。点击对话框中的重新启动按钮以关闭对话框并重新开始游戏。接下来你会这样做。
添加一些收尾工作
比赛开始成形。你接下来的步骤是编写代码以重新启动游戏,然后添加分数以赋予游戏竞争元素。
重启游戏
接下来,你将在用户点击Restart按钮时重新启动游戏。将lib/game.dartrestart
中的现有替换为:
void restart() {
score = 0;
length = 5;
positions = [];
direction = getRandomDirection();
speed = 1;
changeSpeed();
}
上面的代码只是将所有内容重置为其初始值。它也会清除positions
,因此蛇会失去它length
并从头开始重生。
显示分数
接下来,你需要添加代码以在屏幕右上角显示分数。为此,请实施getScore()
:
Widget getScore() {
return Positioned(
top: 50.0,
right: 40.0,
child: Text(
"Score: " + score.toString(),
style: TextStyle(fontSize: 24.0),
),
);
}
添加getScore()
到build()
作为外部的最后一个孩子Stack
在. 最后,你应该如下所示:`
@override
Widget build(BuildContext context) {
//...
return Scaffold(
body: Container(
color: Color(0XFFF5BB00),
child: Stack(
children: [
getPlayAreaBorder(),
Stack(
children: getPieces(),
),
getControls(),
food,
getScore(),
],
),
),
);
}
保存所有文件并重新启动应用程序。
现在,你应该会实时看到分数更新。
来源:https://juejin.cn/post/7090196257666252831