Flame实战2D游戏开发——Flame基础知识
环境创建
开发我们的2D游戏。因此,这是一个在后续章节创建新项目时应参考的章节。
让我们从创建我们将用于构建应用程序的项目开始:
$ flutter create <ProjectName>
一旦在Flutter中创建了项目,我们切换到项目文件夹:
$ cd <ProjectName>
然后,我们添加Flame库:
$ flutter pub add flame
最后,我们打开VSC或您用于开发Flutter的编辑器;对于这一点,您可以执行手动过程(从VSC中打开我们之前创建的项目)或使用VSC命令(如果您已经进行了配置):
$ code .
Flame基础
在这一节中,我们将了解Flame的关键元素及其组织、组件和关键结构。这一章纯粹作为一个参考;如果您不理解所有解释的术语,不要担心。接下来的章节提供了一个更实际的方案,我们将逐步构建一个应用程序,当您遇到本章介绍的类和函数之一时,可以返回到本章查看关于它的解释。 您可以按照上一节中显示的过程创建一个名为“testsflame”的项目。
游戏类和组件
Flame中的项目可以分为两个部分:
- 主类,它允许应用程序的所有模块进行通信并使用Flame的内置过程,如碰撞和输入系统(键盘、手势等)。
- 组件,这是我们游戏的元素,例如背景、玩家、敌人等。 为了更容易理解这个概念,您可以将Flame的Game类视为Flutter的MaterialApp,而将Flame组件视为构成Flutter应用程序的每个页面。
让我们更详细地了解这些Flame元素。
组件
Flame的一个很棒的方面是我们可以利用其组件系统的不同特性。组件可以是许多东西,如玩家、敌人、背景、粒子效果、文本或摇杆等,借助这些组件,我们可以加入更多功能,例如使用碰撞、更新以及与键的交互,如拖放、轻触等。正如您所看到的,Flame在本质上与Flutter有很多相似之处,但在这种情况下,是以组件而不是小部件为基础,且面向游戏。
实质上,游戏实体,比如玩家,由一个组件表示,这是一个类。通过我们应用程序的Game类(Game类型的类),这些组件可以相互通信,例如在条目或碰撞中,这为我们提供了一个非常模块化和可扩展的应用程序环境。
我们有许多类型的组件。在本书中,我们将看到一些,如
- SpriteComponent
- SpriteAnimationComponent
- PositionComponent
- TextComponent
您可以在docs.flame-engine.org/latest/flam…上看到完整的列表。
游戏类:Game 和 FlameGame
与任何项目一样,拥有一个基本结构,我们可以轻松维护和扩展,是非常重要的。Flame被设计为具有模块化。全局而言,我们有一个Game类,它允许我们对构成应用程序的每个组件进行很多控制。这个Game类对整个应用程序都是全局的,可以由许多类型的类表示;FlameGame是Flame中最常用的Game类,也是我们在本书中主要关注的类。我们在FlameGame类中添加的任何组件都可以用于碰撞检测,为Flame中的组件提供了一种简单的通信方式。
由于FlameGame类是应用程序的Game类型类,它可以将我们的游戏划分为表示游戏中的每个实体的组件(类),如玩家、背景、敌人、设置等。
您可以在docs.flame-engine.org/latest/flam…找到更多信息。
至于名为Game的类本身,它是一个低级别的类。像FlameGame类一样,我们可以将其用作游戏的主类或Game类型类,但与FlameGame不同的是,它提供了一种更基本的方法来创建游戏;例如,我们不能在单独的类中添加组件,并且必须实现update()和render()方法,我们稍后会讨论。
在本书中,当提到“Game类型类”这个术语时,它指的是这种类型的任何类,它们允许创建游戏的全局实例;正如我们之前所指出的,FlameGame类是我们在整本书中将用于开发应用程序的类,因为它允许我们使用典型的Flame功能,如碰撞处理和在类中管理组件。
示例 1:绘制精灵
在下面的代码中,您可以看到Flame中应用程序的基本结构,其中我们有FlameGame类及其定义,以及后续添加组件:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
class MySprite extends SpriteComponent {
MySprite() : super(size: Vector2.all(16));
@override
void onLoad() async {
sprite = await Sprite.load('image.png');
}
}
class MyGame extends FlameGame {
@override
void onLoad() async {
await add(MySprite());
}
}
void main() {
runApp(GameWidget(game: MyGame()));
}
对于上述实现,我们加载一张图片:
sprite = await Sprite.load('image.png');
这张图片必须存在于以下路径:
assets/images/image.png
并在应用程序中注册该图像:
pubspec.yaml
assets:
- assets/images/image.png
并且精灵的大小将是类构造函数中指定的16像素:
size: Vector2.all(16)
然后我们从全局实例中添加它:
add(MySprite());
一旦在您的项目中注册了一张图像,您将看到类似以下的结果。
示例 2:绘制圆圈
在这个例子中,我们将看到如何使用FlameGame类绘制一个圆圈:
lib/main.dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
class MyCircle extends PositionComponent {
MyCircle() : super();
@override
void onLoad() async {}
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// BasicPalette.red.paint());
}
}
class MyGame extends FlameGame {
@override
void onLoad() async {
await add(MyCircle());
}
}
void main() {
runApp(GameWidget(game: MyGame()));
}
在另一个示例中,我们有相同的应用程序,但使用Game类而不是FlameGame类:
lib/main.dart
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
void main() async {
runApp(GameWidget(game: MyCircle()));
}
class MyCircle with Game {
@override
void onLoad() async {
super.onLoad();
// init
}
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// BasicPalette.red.paint());
}
@override
void update(double deltaTime) {}
}
如果使用Game类或FlameGame类运行任何一个程序,您将看到类似以下的输出。
正如您可以得出的结论,使用类组织代码的这种方法使我们能够更轻松地重用组件,以及扩展或创建其他组件;因此,这些示例强化了使用它的原因。除此之外,使用FlameGame类,我们可以访问Flame API的其他功能。
FlameGame类维护所有游戏组件的列表,这些组件可以根据需要动态添加到游戏中;例如,我们可以向游戏中添加许多敌人的SpriteComponent,然后在玩家杀死敌人时从游戏中删除它们。然后,FlameGame类将迭代这些组件,告诉每个组件更新和渲染自身。
GameWidget类代表我们通常在Flame中用于实例化游戏并将其设置到Flutter小部件树中的构造函数。
作为建议,Ctrl/Command-click FlameGame、Game、PositionComponent、SpriteComponent和其他类,查看它们的详细信息以及它们实现的属性,如下所示:
class PositionComponent extends Component
implements
AnchorProvider,
AngleProvider,
PositionProvider,
ScaleProvider,
CoordinateTransform {
PositionComponent({
Vector2? position,
Vector2? size,
Vector2? scale,
double? angle,
this.nativeAngle = 0,
Anchor? anchor,
super.children,
***
您还可以查看您可以实现的函数以及可以发送的参数及其类型,例如Canvas的情况,并测试它们并评估结果。
例如,要绘制一个矩形,我们可以通过在屏幕上定义两个点来实现:
canvas.drawRect(Rect.fromPoints(const Offset(10,10), const Offset(500,500)), BasicPalette.purple.paint());
或者,我们可以使用圆绘制矩形,而不是使用canvas.drawCircle():
canvas.drawRect(Rect.fromCircle(center: const Offset(100, 100), radius: 50.0), BasicPalette.brown.paint());
案例3:更新圆的位置
另一个例子,稍微复杂一些,使用了附加功能来更新游戏,创建一个从左到右移动的圆,直到在几秒钟内消失在屏幕上。让我们看看这两个实现,首先是使用FlameGame类:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
class MyCircle extends PositionComponent {
MyCircle() : super();
double circlePos = 10;
final Paint _paint = BasicPalette.red.paint();
@override
void onLoad() async {}
@override
void render(Canvas canvas) {
canvas.drawCircle(
Offset(circlePos, circlePos),
10,
_paint,
);
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// _paint );
}
@override
void update(double dt) {
super.update(dt);
circlePos++;
}
}
class MyGame extends FlameGame {
@override
void onLoad() async {
await add(MyCircle());
}
}
main() {
runApp(GameWidget(game: MyGame()));
}
接下来是使用Game类的实现:
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
void main() async {
runApp(GameWidget(game: MyCircle()));
}
class MyCircle with Game {
double circlePos = 10;
@override
void onLoad() async {
super.onLoad();
// 初始化
}
@override
void render(Canvas canvas) {
canvas.drawCircle(
Offset(circlePos, circlePos), 10, BasicPalette.red.paint());
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// _paint);
}
@override
void update(double dt) {
circlePos++;
}
}
在Game类中,游戏组件,这里是圆,直接在Game类中实现,而使用FlameGame类的实现中,我们可以使用一个独立的类(组件)来处理该组件的所有逻辑,例如移动圆,这使得我们的实现更清晰。此外,在Game类中,必须实现render()方法(在画布上绘制,这里是圆)和update()方法(对游戏进行更新),我们将在下一节中解释。
目前我们不会详细讨论代码的每一行具体执行什么。重要的是注意两种Game类型类的实现并进行比较。在下一章中,我们将详细说明并解释前面示例中每一行代码的作用。
在Flame API中,还有一个称为CircleComponent的组件,我们也可以以类似的方式使用: docs.flame-engine.org/latest/flam…
或者,如果你想绘制一个矩形,可以取消注释先前显示的这段代码:
canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20), _paint);
关键Flame中的过程和方法
在这一节中,我们将了解游戏的基本循环以及它的关键功能。无论使用何种引擎创建,每个现有的游戏都由两个定义明确的过程组成:
- 一个允许我们在画布上绘制的过程,也就是说,我们可以在画布上绘制图形和图像,以在屏幕上显示它们。它的使用方式非常类似于我们在HTML API中可以处理的方式。
- 另一个更新游戏元素的过程。例如,假设我们有一个玩家。然后,通过点击屏幕上的按钮或按键,玩家移动;通过受到敌人的伤害,玩家也会移动。您可以在其他类型的组件和场景中实现相同的逻辑。再举个例子,我们可以有一个草地组件,当角色踩在上面时,会变形,而这种变形在这种情况下是通过组件之间的交互完成的:草地和玩家。
这两个过程被称为游戏循环,下面进行解释。
游戏循环
Flame的名为GameLoop或游戏循环的模块实际上只是游戏循环概念的简单抽象。基本上,大多数视频游戏都基于两种方法:
render
方法使用画布来绘制游戏的当前状态。update
方法接收自上次更新以来的时间(delta),并允许游戏进入下一个状态,也就是更新游戏状态。
也就是说,我们有一个初始化游戏的方法,另一个用于执行更新。Flame遵循这些原则,我们有一对方法允许我们执行这些操作。
Render 方法
render()
函数接收一个类型为对象的参数,该参数引用了画布:
@override
void render(Canvas canvas) {
canvas.drawRect(squarePos, squarePaint);
}
这就像在其他技术(如HTML5)中一样;这只是一个空白的画布,可以在上面绘制任何东西。在这里,我们可以绘制任何东西,例如一个圆:
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
}
Update 方法
游戏中的元素需要根据游戏的当前状态不断重新绘制;为了更清楚地理解这一点,让我们看一个例子。
假设一个游戏元素由一个精灵(一张图像)表示,在这个例子中我们将其称为“玩家”。当用户点击按钮时,玩家必须更新其位置;这个更新通过一个称为update()
的函数在游戏中应用。
就像我们在移动圆的例子中看到的那样,正是这个函数负责更新屏幕上圆的位置。
update()
方法就像其名称所示,是一个更新函数,它接收一个称为“delta time”(dt)的参数,告诉我们自上一帧绘制以来经过的时间。您应该使用此变量以便您的组件在所有设备上以相同的速度移动。
设备以不同的速度工作,这取决于处理能力(即设备具有的处理器,特别是处理器的工作频率)。因此,如果我们忽略 delta 值,仅以处理器能够运行的最大速度运行所有内容,游戏可能会在控制角色时出现速度问题,因为它可能太快或太慢。通过在运动计算中使用 deltaTime 参数,我们可以确保我们的精灵在具有不同处理器速度的设备上以任何我们想要的速度移动。
通过update()
函数更新游戏的任何方面时,它会自动反映到render()
函数中,并通过这个方式,游戏在图形层面上得以更新。
游戏循环被所有Game类及其组件的实现使用: docs.flame-engine.org/latest/flam…
Flame中的其他重要函数
前面的函数在组件的使用中有一个等效的版本。我们有一个update()
函数来执行更新;就像在Game类型的类中一样,它接收delta time
参数。我们还有一个函数来初始化组件;在这种情况下,它被称为onLoad()
,它仅用于初始化数据,而不是像render()
函数中那样进行渲染。我们可以使用这个方法,例如在屏幕上绘制图形或精灵。
当然,在Flame中还有许多其他函数,我们将在这本书中随着在不同示例中前进所需而逐渐了解;然而,本章中我们看到的函数是Flame中的关键和最重要的。请记住,本章充当参考,您可以在查看我们游戏的实现时随时参考。
转载自:https://juejin.cn/post/7320169904342040628