从0开始使用flutter和flame创建手机游戏

为啥要做游戏开发呢?
不得不承认,游戏是一个非常让人着迷的活动(孩子们不要沉迷哟)。
我很喜欢玩游戏,虽然不是非常非常痴迷,但是也因此入手了好几部游戏机,其实我一直心心念念的向入手一部小霸王(此处可以认为是软广,小霸王不要忘了给我广告费,哈哈)红白机,和我的孩子一起怀念一下我小时候的快乐。
你们是不是也有这样的情愫呢?

游戏之所以这么吸引人,也许是因为,他可以从简单和线性的游戏玩法到真正复杂的涉及 3D、几乎真实的物理引擎、逼真的图形、程序生成的世界以及适应玩家选择的故事情节。

有很多很多的人,肯定都像木辛老师一样,想做出属于自己的精美、好玩的游戏。如果你在这一群人里边,那么恭喜你,本篇内容将带领你入门一款经典的游戏引擎。不过,本篇更多的关注的是理论知识,暂时没有涉猎实际开发效果,也并没有讲到如何发布游戏。所以,如果你想巩固游戏开发过程中的理论知识,建议你好好读读这篇文章哈。

编写过程中难免会有纰漏,如果恰巧您发现了这些问题,可以给我留言或者私信告诉我,我会以最快的速度修正,再次非常感谢!

阅前必备技能

本文将假设您已经是一名开发人员,并且对开发软件的概念有扎实的掌握。
当然了,如果您是新手,那也没有关系,只要您对游戏开发有足够的兴趣,这也是可以的。

虽然,兴趣是第一老师,是催动你进步的助力,但是这还不算准备就绪。开始游戏,你还需要一台电脑,足够让你配置好开发所需要的环境,以便于可以编写和编译代码。
更好一些的,如果你恰巧配备了一台Android手机,那么你就可以在其上进行软件的测试和实际的使用体验啦。
SO,欲善其事,先来利其器吧!

使用Flutter编写的程序可以被编译和构建在Android手机和iOS手机上边。
Android手机用的十分广泛,所以本章节会主要以Android手机为例,你可以通过准备的Android设备进行开发和调试,一旦你完成了这个程序,就可以很轻易的通过不同的构建命令实现iOS设备上的运行。

再一次假设,你已经顺利完成了如下步骤:

  1. Microsoft Visual Studio Code:使用任何IDE或者文本编辑工具都可以编写程序,当然了,如果你是一个新手,那么强烈推荐你使用VSCode,它将会给你带来一个与众不同的感觉。
    一定要从官网下载软件哟,否则一不小心下载到流氓软件就不好了。
    还有一件挺重要的事情,安装好VScode之后可以安装一下Flutte和Dart的插件,虽然这个不是必须要的,但是配置好了以后,对你的开发工作会有很大的帮助。

如下两个准备,必须要做,一定要做,不得不做吖。
Android SDK:如果你想编写Android程序的话,这个一定要有。
如果你觉得一步步地配置太麻烦,也可以直接下载Android Studio开发工具,这样就可以一键完成所有配置。
当然了,也有一些同学不想安装这个“巨大”的工具,指向最小化安装,那么你只需要安装SDK就可以啦。
接下来的这个“装备”,你是必须、必须、必须一定要安装的,否则你就没有办法开发Flutter程序啦!
Flutter SDK/Framework:除了Flutter还有Flame这个插件,这个才是我们今天内容的主角,使用这两个玩意才能最终完成游戏的开发。

那么,接下来让我们开始游戏开发之旅吧!

让我们先从一个非常非常简单的开始。
我们这款游戏是这样的,在一整个黑色的屏幕中有一块白色的方块,我们把它放置在屏幕的正中间,当你点击这个白色的方块的时候,它就会变成绿色,这样你就赢了(不要太简单啦)。

我们暂时先不用比较复杂的元素,所以,在这个游戏中我么不会使用图片文件什么的。

所以,现在正式开始!

第一步,创建一个Flutter程序项目

打开终端(就是计算机中可以输入命令行的东东),并且定位到你要创建的项目目录下,然后执行如下命令:

$ flutter create boxgame

此命令使用 Flutter 命令行工具为您初始化和引导一个基本的移动应用程序。

如果需要,您可以选择除 boxgame 以外的任何名称。只需确保将所有 boxgame 实例替换为您在后续操作中使用的任何内容。

此时,您可以在 Visual Studio Code 中打开生成的 boxgame 文件夹,也可以使用以下命令立即运行您的应用程序:

$ cd boxgame
$ flutter run

第一次运行新创建的应用程序可能需要一段时间。当移动应用程序运行时,您应该会看到如下内容:

第二步:安装Flame插件并且清理一下项目

注意:从这里开始,我们将项目目录称为 ./。如果你的游戏项目在 ~/boxgame 中,./lib/main.dart 指的是 ~/boxgame/lib/main.dart 中的文件。

启动 Visual Studio Code 并打开上一步创建的 boxgame 目录。

由于我们将使用简单而强大的 Flame 插件,因此我们需要将其添加到我们的应用程序将依赖的 Dart 包列表中。在 IDE 的左侧,您将看到项目文件夹中的文件列表。打开 ./pubspec.yaml 并在依赖项下的 cupertino_icons 行下方添加以下行(注意缩进)。

flame: ^1.0.0

你应该会看到像我这里差不多的样子:

如果您使用的是 Visual Studio Code,IDE 将在保存文件时自动为您安装插件。

您可以通过打开终端、导航到项目文件夹并运行 flutter packages get 来手动完成。

下一步是清理主代码,删除掉Flutter 项目中./lib/main.dart 文件中所有内容,并将其替换为空程序。

空程序只有一行:void main() {}。

您会注意到的一件事是,我们将 import 语句留在了顶部。我们将在稍后启动游戏时运行 runApp 方法时使用material库。您现在应该有如下所示的内容:

另一件事是 ./test 文件夹中的文件显示错误。如果您没有使用 Visual Studio Code,这可能不会显示,但您的应用程序将不会运行。

测试(和测试驱动的开发)超出了本教程的范围,所以要解决这个问题,只需删除整个测试文件夹。

第三步:设置游戏循环逻辑

现在我们要设置游戏循环……

但是什么是游戏循环?
游戏循环是游戏的核心。计算机反复运行的一组指令。

游戏中通常有个概念叫做FPS。它代表每秒帧数。这意味着如果您的游戏以 60 fps 运行,则计算机每秒运行您的游戏循环 60 次。

简而言之:一帧 = 一次游戏循环。

一个基本的游戏循环由两部分组成,更新和渲染。

更新部分处理对象的移动(如角色、敌人、障碍物、地图本身)和其他需要更新的东西(例如计时器)。

大部分动作都发生在这里。

例如,计算敌人是否被子弹击中或计算敌人是否接触主角(主角通常不喜欢这样)。

渲染部分在屏幕上绘制所有对象。这是一个单独的进程,因此一切都是同步的。

为啥需要同步捏?

想象一下,如果你正更新主角的位置呐。如果这个主角没啥“异议(没有变化)”,那么一切皆无恙。

但是,说来不巧,正好有一颗子弹就在几个像素之外。您恰巧更新了子弹,并且它击中了您的角色。

现在,主角死了,所以你不要拔出子弹。而此时,您绘制完角色垂死动画的第一帧,在下一个周期中,您将跳过更新角色,改为渲染他垂死动画的第一帧(而不是第二帧)。

这会给你的游戏一种生涩的感觉。想象一下玩射击游戏,你射击一个敌人,他没有倒下,你再次射击,但在子弹击中之前他就死了,死了,死了,了。。。

非同步渲染的抖动性能可能并不明显(尤其是在每秒运行 60 帧时),但如果这种情况发生得太频繁,玩家就会感觉你这款游戏是不是还没搞完,直接就弃了。

SO,您是真希望计算所有内容,并且当所有对象的状态都计算并最终确定之后,才会绘制屏幕上。

使用Flame

Flame 已经有处理这些边边角角的代码了,所以我们只需要关注编写实际的更新和渲染过程。

但首先,我们的应用程序需要做两个准备:一个是全屏,一个是竖屏锁定。

Flame 还为这些提供了实用功能。因此,让我们将它们添加到我们的代码中。

以下这几行代码请添加到文件的顶部:

import 'package:flame/flame.dart';
import 'package:flutter/services.dart';

然后在 main 函数中,我们可以直接使用Flame.device静态调用的方式,使用 fullscreen 和 setOrientation 函数,一定要耐心等待哟,因为这些函数返回一个 Future。

await Flame.device.fullScreen();
await Flame.device.setOrientation(DeviceOrientation.portraitUp);

注意:Futures、async 和 await 是编码实践,允许您“等待”一个较长的进程完成而不会阻塞其他所有内容。如果您有兴趣了解它们,

为了能够等待 Futures,上下文必须在异步函数中。所以让我们把 main 函数转换成一个异步函数。

void main() async {

最后,您应该有如下所示的内容:

要利用 Flame 插件提供的游戏循环机制,我们必须创建一个 Flame 的 Game 类的子类。为此,请在 ./lib 下创建一个新文件并将其命名为 box-game.dart。

然后我们将编写一个名为 BoxGame 的类(如果你知道类是如何工作的,你可以使用任何类)来扩展 Flame 的 Game 类。

import 'dart:ui';

import 'package:flame/game.dart';

class BoxGame extends Game {
  void render(Canvas canvas) {
    // TODO: implement render
  }

  void update(double t) {
    // TODO: implement update
  }
}

让我们分解一下:我们导入 Dart 的 ui 库,这样就可以使用 Canvas 类,然后使用 Size 类。
我们导入 Flame 的游戏库,其中包括正在扩展的 Game 类。其他所有内容都是具有两种方法的类定义:更新和渲染。这些方法会覆盖父类(也称为超类)的同名方法。

注意:@override 注释在 Dart 2 中是可选的,所以,如果你找不到它也不找着急,这是正常的。 
new 关键字也是可选的,所以我们也不会使用它。

下一步是创建此 BoxGame 类的实例并将其小部件属性传递给 runApp。

让我们回到 ./lib/main.dart 并在文件的最顶部插入以下行:

import 'package:boxgame/box-game.dart';

该行确保 BoxGame 类可以在 main.dart 中使用。
接下来,创建 BoxGame 类的实例并将其小部件属性传递给 runApp 函数。
在 main 函数的末尾(右大括号 } 上方)插入以下行。

BoxGame game = BoxGame();
runApp(game.widget);

现在我们的移动应用程序就可以称作是一个游戏啦!

如果你运行游戏,你只会看到一个空白/黑屏,因为屏幕上还没有绘制任何东西。

您的 main.dart 文件应如下所示:

第四步:绘制界面

在屏幕上绘图之前,我们必须提前知道屏幕的大小。
Flutter 在屏幕上绘图时使用逻辑像素,因此您不必担心调整游戏对象的大小。

一英寸的设备包含大约 96 个逻辑像素。因此,假设我们将手机作为我们的发布平台。大多数现代和主流手机的尺寸都差不多,因为我们的游戏非常简单,我们不必担心尺寸。

Flame 建立在这个大小系统之上,而 Game 类实际上有一个我们可以覆盖的调整大小的函数。这个函数接受一个 Size 参数,我们可以通过这个参数确定屏幕的大小(以逻辑像素为单位)。

首先,让我们在类级别声明一个变量。这个变量(也称为实例变量)将保存屏幕的大小,并且仅在屏幕改变大小时更新(对于我们的游戏应该只发生一次)。这也是在屏幕上绘制对象时的基础。这个变量的类型应该是 Size。与传递给 resize 函数的内容相同。

class BoxGame extends Game {
  Size screenSize;

screenSize 变量将被初始化为 null。在检查我们是否知道渲染过程中的屏幕大小时,这将很有帮助。稍后再谈。

接下来,让我们在 ./lib/box-game.dart 中添加重载函数resize()。

void resize(Size size) {
  screenSize = size;
  super.resize(size);
}

注意:超类的 resize 函数实际上是空的,但调用我们要覆盖的超类的超函数是个好主意。除非我们完全想重写该函数。所以,让我们先把它留在这里吧。

另一个需要注意的地方:实例变量是可以从类的所有方法/函数访问的变量。例如,您可以在调整大小时设置它,然后在渲染时获取它的值。

您的代码应如下所示: