Flutter的出现和发展撬动了跨平台游戏设计的发展;Flutter游戏的设计和逻辑只需几行代码就可以创建,同时保持良好的UI/UX。
Flutter有能力以高达60FPS的速度渲染。你可以利用这种能力来建立一个简单的2D,甚至3D游戏。请记住,在Flutter中开发更复杂的游戏并不是一个好主意,因为大多数开发者会倾向于对复杂的应用程序进行本地开发。
在本教程中,我们将重现有史以来最早的电脑游戏之一。乒乓》。乒乓是一个简单的游戏,所以它是一个很好的开始。本文分为两个主要部分:游戏逻辑和用户界面,通过分别关注重要部分,使构建过程更加清晰。
在我们进入构建之前,让我们来看看先决条件和设置。
先决条件
为了理解和编写本课的内容,你将需要以下条件。
- 在您的机器上安装Flutter
- Dart和Flutter的工作知识
- 一个文本编辑器
开始学习
在这篇文章中,我们将使用Alignment(x,y)
,作为屏幕X和Y轴位置的代表Vector(x,y)
,这将有助于开发游戏的物理学。我们还将为我们的一些变量创建无状态的小部件,并在homepage.dart
文件中声明它们,以使代码不那么臃肿和易于理解。
首先,创建一个Flutter项目。清除main.dart
文件中的默认代码,并导入material.dart
包,以便在应用程序中包括Material widgets。
接下来,创建一个名为MyApp()
的类,并返回MaterialApp()
,然后创建一个statefulWidget
HomePage()
,并将其传入MaterialApp()
的参数home
,如下图所示。
import 'package:flutter/material.dart';
import 'package:pong/homePage.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner:false,
home: HomePage(),
);
}
}
游戏逻辑
在HomePage()
,我们需要写一些函数和方法来处理数学和物理相关的操作。这些包括处理碰撞、加速或减速以及游戏中的导航。
但首先,我们需要声明一些参数,这些参数将代表球的位置排列、球员和双方球员的初始得分。这些参数的代码应该放在_HomePageState
,我们将在后面的文章中提到它。
//player variations
double playerX = -0.2;
double brickWidth = 0.4;
int playerScore = 0;
// enemy variable
double enemyX = -0.2;
int enemyScore = 0;
//ball
double ballx = 0;
double bally = 0;
var ballYDirection = direction.DOWN;
var ballXDirection = direction.RIGHT;
bool gameStarted = false;
...
然后,我们为球和砖头的运动方向提供一个枚举。
enum direction { UP, DOWN, LEFT, RIGHT }
...
为了使这个游戏顺利进行,我们需要创建人工重力,以便当球击中顶部的砖(0.9)或底部的砖(-0.9)时,它将向相反的方向移动。否则,如果它没有撞到任何一块砖,而是去了比赛场地的顶部(1)或底部(-1),它就会记录为玩家的损失。
当球击中左边(1)或右边(-1)的墙时,它就会向相反的方向走。
void startGame() {
gameStarted = true;
Timer.periodic(Duration(milliseconds: 1), (timer) {
updatedDirection();
moveBall();
moveEnemy();
if (isPlayerDead()) {
enemyScore++;
timer.cancel();
_showDialog(false);
// resetGame();
}
if (isEnemyDead()) {
playerScore++;
timer.cancel();
_showDialog(true);
// resetGame();
}
});
}
...
在上面的代码中,我们先用一个函数startGame()
,将布尔值gameStarted
改为true
,之后我们调用一个持续时间为一秒的Timer()
。
在定时器内,像updatedDirection()
、moveBall()
、moveEnemy()
这样的函数与一个if
语句一起传递,以检查任何一个玩家是否失败。如果是这样,分数将被累积,计时器被取消,并显示一个对话框。
下面的函数确保球的排列不会超过0.9
,而且当球与砖头接触时,只会向相反的方向移动。
void updatedDirection() {
setState(() {
//update vertical dirction
if (bally >= 0.9 && playerX + brickWidth>= ballx && playerX <= ballx) {
ballYDirection = direction.UP;
} else if (bally <= -0.9) {
ballYDirection = direction.DOWN;
}
// update horizontal directions
if (ballx >= 1) {
ballXDirection = direction.LEFT;
} else if (ballx <= -1) {
ballXDirection = direction.RIGHT;
}
});
}
void moveBall() {
//vertical movement
setState(() {
if (ballYDirection == direction.DOWN) {
bally += 0.01;
} else if (ballYDirection == direction.UP) {
bally -= 0.01;
}
});
//horizontal movement
setState(() {
if (ballXDirection == direction.LEFT) {
ballx -= 0.01;
} else if (ballXDirection == direction.RIGHT) {
ballx += 0.01;
}
});
}
...
另外,如果球打到场地的左边或右边,它就会向相反的方向走。
void moveLeft() {
setState(() {
if (!(playerX - 0.1 <= -1)) {
playerX -= 0.1;
}
});
}
void moveRight() {
if (!(playerX + brickWidth >= 1)) {
playerX += 0.1;
}
}
...
moveLeft()
和moveRight()
函数有助于使用键盘箭头控制我们的砖块从左到右运动。这些函数与if
语句配合使用,以确保砖块不会超出场地两轴的宽度。
函数resetGame()
,将球员和球返回到他们的默认位置。
void resetGame() {
Navigator.pop(context);
setState(() {
gameStarted = false;
ballx = 0;
bally = 0;
playerX = -0.2;
enemyX =- 0.2;
});
}
...
接下来,我们创建了两个函数,isEnemyDead()
和isPlayerDead()
,它们返回一个布尔值。它们检查任何一个球员是否输了(如果球击中了砖块后面的垂直部分)。
bool isEnemyDead(){
if (bally <= -1) {
return true;
}
return false;
}
bool isPlayerDead() {
if (bally >= 1) {
return true;
}
return false;
}
...
最后,当任何一方获胜时,函数_showDialog
显示一个对话框。它传递了一个布尔值,enemyDied
,以区分玩家输掉比赛时。然后,它宣布非输家赢得了这一轮,并使用赢家的颜色来显示文本 "再玩一次:"
void _showDialog(bool enemyDied) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
elevation: 0.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
backgroundColor: Colors.purple,
title: Center(
child: Text(
enemyDied?"Pink Wins": "Purple Wins",
style: TextStyle(color: Colors.white),
),
),
actions: [
GestureDetector(
onTap: resetGame,
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
padding: EdgeInsets.all(7),
color: Colors.purple[100],
child: Text(
"Play Again",
style: TextStyle(color:enemyDied?Colors.pink[300]: Colors.purple[000]),
)),
),
)
],
);
});
}
用户界面
现在,我们将开始开发用户界面。
在homePage.dart
文件中的widgetbuild
内,添加下面的代码。
return RawKeyboardListener(
focusNode: FocusNode(),
autofocus: false,
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
moveLeft();
} else if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
moveRight();
}
},
child: GestureDetector(
onTap: startGame,
child: Scaffold(
backgroundColor: Colors.grey[900],
body: Center(
child: Stack(
children: [
Welcome(gameStarted),
//top brick
Brick(enemyX, -0.9, brickWidth, true),
//scoreboard
Score(gameStarted,enemyScore,playerScore),
// ball
Ball(ballx, bally),
// //bottom brick
Brick(enemyX, 0.9, brickWidth, false)
],
))),
),
);
在代码中,我们返回RawKeyboardListener()
,这将提供从左到右的运动,因为我们正在网络上构建。这也可以被复制到触摸屏设备上。
小部件GestureDetector()
,提供用于调用上面逻辑中写的函数startGame
的onTap
功能。还写了一个子程序,Scaffold()
,用来指定应用程序的背景颜色和主体。
接下来,创建一个名为Welcome
的类,并传入一个布尔值来检查游戏是否已经开始。如果游戏还没有开始,"点击播放 "的文字将变得可见。
class Welcome extends StatelessWidget {
final bool gameStarted;
Welcome(this.gameStarted);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment(0, -0.2),
child: Text(
gameStarted ? "": "T A P T O P L A Y",
style: TextStyle(color: Colors.white),
));
}
}
现在我们可以创建另一个类,Ball
,用Alignment(x,y)
来处理球的设计和它在场内每一点的位置。我们通过一个构造函数来传递这些参数,以获得流动性,就像这样。
class Ball extends StatelessWidget {
final x;
final y;
Ball(this.x, this.y);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment(x, y),
child: Container(
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
width: 20,
height: 20,
),
);
}
}
现在让我们设计Brick
类来处理砖的设计、颜色、位置和球员类型。
在这里,我们使用一个数学方程(Alignment((2* x +brickWidth)/(2-brickWidth), y)
)来传递x和y轴的位置。
class Brick extends StatelessWidget {
final x;
final y;
final brickWidth;
final isEnemy;
Brick( this.x, this.y, this.brickWidth, this.isEnemy);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment((2* x +brickWidth)/(2-brickWidth), y),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
alignment: Alignment(0, 0),
color: isEnemy?Colors.purple[500]: Colors.pink[300],
height: 20,
width:MediaQuery.of(context).size.width * brickWidth/ 2,
),
));
}
}
最后,Score
类应该放在homepage.dart
文件中build
小部件的正下方;它显示每个球员的得分。
为变量enemyScore
和playerScore
创建一个构造函数来处理每个玩家的分数,并为gameStarted
来检查游戏是否已经开始。这将显示Stack()
的内容,或一个空的Container()
。
class Score extends StatelessWidget {
final gameStarted;
final enemyScore;
final playerScore;
Score(this.gameStarted, this.enemyScore,this.playerScore, );
@override
Widget build(BuildContext context) {
return gameStarted? Stack(children: [
Container(
alignment: Alignment(0, 0),
child: Container(
height: 1,
width: MediaQuery.of(context).size.width / 3,
color: Colors.grey[800],
)),
Container(
alignment: Alignment(0, -0.3),
child: Text(
enemyScore.toString(),
style: TextStyle(color: Colors.grey[800], fontSize: 100),
)),
Container(
alignment: Alignment(0, 0.3),
child: Text(
playerScore.toString(),
style: TextStyle(color: Colors.grey[800], fontSize: 100),
)),
]): Container();
}
}
下面的GIF显示了一个游戏的测试。
总结
在这篇文章中,我们在代码中涵盖了alignment
、RawKeyboardListener
、widgets、booleans、用于容器的ClipRect和数学函数,这些都是用来重现游戏Pong的。这个游戏还可以通过增加球的数量或减少砖的长度来改进,使其更加复杂。
我希望这篇文章和建造及记录它的过程一样有帮助和有趣。欢迎使用文章中的原则来重新创造其他经典游戏,或发明一个新的游戏。你可以在GitHub上找到本文的代码链接。
The postBuilding a 2D game with Flutterappeared first onLogRocket Blog.