用Flutter构建一个2D游戏

1,315 阅读7分钟

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() ,提供用于调用上面逻辑中写的函数startGameonTap 功能。还写了一个子程序,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 小部件的正下方;它显示每个球员的得分。

为变量enemyScoreplayerScore 创建一个构造函数来处理每个玩家的分数,并为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显示了一个游戏的测试。

Gif of the Flutter game

总结

在这篇文章中,我们在代码中涵盖了alignmentRawKeyboardListener 、widgets、booleans、用于容器的ClipRect和数学函数,这些都是用来重现游戏Pong的。这个游戏还可以通过增加球的数量或减少砖的长度来改进,使其更加复杂。

我希望这篇文章和建造及记录它的过程一样有帮助和有趣。欢迎使用文章中的原则来重新创造其他经典游戏,或发明一个新的游戏。你可以在GitHub上找到本文的代码链接

The postBuilding a 2D game with Flutterappeared first onLogRocket Blog.