Flutter - Flame组件

文章目录[x]
  1. 1:Component
  2. 1.1:Component的生命周期
  3. 1.2:优先级
  4. 1.3:从组件访问世界
  5. 1.4:确保组件有指定的父组件
  6. 1.5:确保组件有指定的祖先
  7. 1.6:查询子组件
  8. 1.7:查询屏幕上特定点的组件
  9. 1.8:组件的可见性
  10. 2:游戏的坐标系
  11. 3:PositionComponent
  12. 3.1:PositionComponent的绘制
  13. 4:SpriteComponent
  14. 5:SpriteAnimationComponent
  15. 6:SpriteAnimationGroupComponent
  16. 7:SpriteGroupComponent
  17. 8:SpawnComponent
  18. 9:SvgComponent
  19. 9.1:ParallaxComponent
  20. 10:ShapeComponents
  21. 10.1:PolygonComponent
  22. 10.2:RectangleComponent
  23. 10.3:CircleComponent
  24. 11:CustomPainterComponent
  25. 12:CameraComponent
  26. 12.1:World
  27. 12.2:CameraComponent
  28. 12.3:具有固定分辨率的 CameraComponent
  29. 12.4:Viewport
  30. 12.5:Viewfinder
  31. 12.6:相机控制
  32. 12.7:visibleWorldRect
  33. 12.8:canSee

在上一篇文章中,我们介绍了Flame的一些基本概念和环境配置。在这篇文章中,我们将介绍Flame中的一些Component,一些常见的Component如上图所示。

Component

所有组件都继承自Component类,并且可以将其他组件作为子组件。这就是Flame 组件系统(简称 FCS)的基础。

可以使用 add(Component c) 方法或直接在构造函数中添加子组件。

void main() {
 final component1 = Component(children: [Component(), Component()]);
 final component2 = Component();
 component2.add(Component());
 component2.addAll([Component(), Component()]);
}

Component的生命周期

  • onGameResize   每当屏幕大小调整时,以及当此组件在onMount之前添加到组件树中时,都会调用 onGameResize 方法。
  • onParentResize  当组件被挂载到组件树中时也会调用该方法,并且当前组件的父组件改变其大小时也会调用该方法。
  • onRemove  在组件从游戏中移除之前运行此方法,即使使用父级移除方法和组件移除方法移除组件,它也只运行一次。
  • onLoad   组件的异步初始化代码,例如加载图像。此方法在 onGameResize 和 onMount 之前执行。此方法保证在组件的生命周期内只执行一次,因此可以将其视为“异步构造函数”。
  • onMount  每次将组件安装到游戏树中时,onMount 方法都会运行。这意味着不应在此处初始化后期最终变量,因为此方法可能会在组件的整个生命周期内运行多次。仅当父级已安装时,此方法才会运行。如果父级尚未安装,则此方法将在队列中等待(这不会对游戏引擎的其余部分产生影响)。
  • onChildrenChanged  如果需要检测父级子级的变化,则可以重写 onChildrenChanged 方法。每当子级添加到父级或从父级移除时(包括子级更改其父级时),都会调用此方法。其参数包含目标子级及其经历的变化类型(添加或移除)。

优先级

在 Flame 中,每个组件都有 int priority 属性,该属性决定了该组件在其父级子级中的排序顺序。在其他语言和框架中,这有时被称为 z-index。优先级设置的越高,组件在屏幕上的显示位置就越近,因为它将呈现在之前呈现的任何优先级较低的组件之上。

例如,如果添加两个组件并将其中一个优先级设置为 1,则该组件将呈现在另一个组件之上(如果它们重叠),因为默认优先级为 0。

class MyGame extends FlameGame {
 @override
 void onLoad() {
 final myComponent = PositionComponent(priority: 5);
 add(myComponent);
 }
}

从组件访问世界

如果某个组件以 World 作为祖先,并且需要访问该 World 对象,则可以混入HasWorldReference。

class MyComponent extends Component with HasWorldReference<MyWorld>,
 TapCallbacks {
 @override
 void onTapDown(TapDownEvent info) {
 // world is of type MyWorld
 world.add(AnotherComponent());
 }
}

确保组件有指定的父组件

当需要将组件添加到特定父类型时,可以混入ParentIsA 来强制执行强类型父类型。

class MyComponent extends Component with ParentIsA<MyParentComponent> {
 @override
 void onLoad() {
 // parent is of type MyParentComponent
 print(parent.myValue);
 }
}

如果尝试将 MyComponent 添加到不是 MyParentComponent 的父级,则会引发断言错误。

确保组件有指定的祖先

当组件需要在组件树中的某个位置具有特定的祖先类型时,可以混入HasAncestor来强制执行该关系。

class MyComponent extends Component with HasAncestor<MyAncestorComponent> {
 @override
 void onLoad() {
 // ancestor is of type MyAncestorComponent.
 print(ancestor.myValue);
 }
}

如果尝试将 MyComponent 添加到不包含 MyAncestorComponent 的树中,则会引发断言错误。

查询子组件

已添加到组件的子组件位于名为 children 的 QueryableOrderedSet 中。要查询集合中特定类型的组件,可以使用 query<T>() 函数。默认情况下,children 集合中的 strictMode 为 false,但如果您将其设置为 true,则必须在使用查询之前使用 children.register 注册查询。

@override
void onLoad() {
 children.register<PositionComponent>();
}

在上面的例子中,为 PositionComponents 注册了一个查询,下面可以看到如何查询已注册组件类型的示例。

@override
void update(double dt) {
 final allPositionComponents = children.query<PositionComponent>();
}

查询屏幕上特定点的组件

方法 componentsAtPoint() 允许检查在屏幕上的某个点渲染了哪些组件。返回的值是组件的可迭代对象,但也可以通过提供可写的 List<Vector2> 作为第二个参数来获取每个组件的本地坐标空间中初始点的坐标。

可迭代对象按从前到后的顺序检索组件,即首先检索前面的组件,然后检索后面的组件。

此方法只能返回实现方法 containsLocalPoint() 的组件。PositionComponent(Flame 中许多组件的基类)提供了这样的实现。但是,如果您要定义从 Component 派生的自定义类,则必须自己实现 containsLocalPoint() 方法。

void onDragUpdate(DragUpdateInfo info) {
 game.componentsAtPoint(info.widget).forEach((component) {
 if (component is DropTarget) {
 component.highlight();
 }
 });
}

组件的可见性

隐藏或显示组件的推荐方法通常是使用 add 和 remove 方法将其添加到树中或从树中移除。

但是,从树中添加和移除组件将触发该组件的生命周期步骤(例如调用 onRemove 和 onMount)。这也是一个异步过程,如果快速连续地移除和添加组件,则需要注意确保组件在再次添加之前已完成移除。

/// Example of handling the removal and adding of a child component
/// in quick succession
void show() async {
// Need to await the [removed] future first, just in case the
// component is still in the process of being removed.
await myChildComponent.removed;
add(myChildComponent);
}

void hide() {
remove(myChildComponent);
}

这些行为并不总是可取的。

显示和隐藏组件的另一种方法是混入HasVisibility,它可以用于从 Component 继承的任何类。此 mixin 引入了 isVisible 属性。只需将 isVisible 设置为 false 即可隐藏组件,设置为 true 即可再次显示它,而无需将其从树中移除。这会影响组件及其所有后代(子代)的可见性。

/// Example that implements HasVisibility
class MyComponent extends PositionComponent with HasVisibility {}

/// Usage of the isVisible property
final myComponent = MyComponent();
add(myComponent);

myComponent.isVisible = false;

即使组件不可见,它仍然在树中,并将继续接收对“更新”和所有其他生命周期事件的调用。它仍将响应输入事件,并仍将与其他组件交互,例如碰撞检测。

游戏的坐标系

在介绍其他组件之前,我们先介绍一下World的坐标系,原点(0,0)在屏幕的正中间,我们打开debug模式,并添加一个像素为100的锚点为中间的PositionComponent:

class GameWorld extends World{

@override
FutureOr<void> onLoad()async {
debugMode = true;
await add(PlayerComponent());
}

}

class PlayerComponent extends PositionComponent{
PlayerComponent():super(size: Vector2.all(100),anchor: Anchor.center);
}

 

PositionComponent

此类表示屏幕上的定位对象,可以是浮动矩形、旋转精灵或其他具有位置和大小的对象。如果向其中添加子项,它还可以表示一组定位组件。

PositionComponent 的基础是它具有位置、大小、比例、角度和锚点,这些锚点可以转换组件的渲染方式。

  • position  是一个 Vector2,它表示组件锚点相对于其父级的位置;如果父级是 FlameGame,则它与视口相关。
  • size  当相机缩放级别为 1.0(无缩放,默认)时,表示组件的大小。
  • scale  比例是组件及其子项应缩放的程度。
  • angle   角度是围绕锚点的旋转角度,以弧度表示。它相对于父级的角度。
  • anchor  锚点是组件上定义位置和旋转的位置(默认值为 Anchor.topLeft)。因此,如果将锚点设置为 Anchor.center,则组件在屏幕上的位置将位于组件的中心,如果应用了角度,则它会围绕锚点旋转。
  • children   PositionComponent 的所有子组件都将相对于父组件进行变换,这意味着位置、角度和比例将相对于父组件的状态。

PositionComponent的绘制

在为扩展PositionComponent的组件实现渲染方法时,请记住从左上角 (0.0) 开始渲染。的渲染方法不应处理组件应在屏幕上的哪个位置进行渲染。要处理组件应在何处以及如何进行渲染,请使用位置、角度和锚点属性,Flame 将自动处理其余部分。

如果要更改组件渲染的方向,还可以使用 flipHorizo​​ntally() 和 flipVertically() 在渲染(Canvas 画布)期间翻转绘制到画布上的任何东西,围绕锚点。这些方法适用于所有 PositionComponent 对象,尤其适用于 SpriteComponent 和 SpriteAnimationComponent。

SpriteComponent

PositionComponent 最常用的实现是 SpriteComponent,可以用 Sprite 创建:

class GameWorld extends World {
@override
FutureOr<void> onLoad() async {
debugMode = true;
await add(SpriteComponent(sprite: await Sprite.load("dash.png"),anchor: Anchor.center,size: Vector2.all(80)));
}
}

SpriteAnimationComponent

此类用于表示具有在单个循环动画中运行的精灵的组件。

这将使用 3 个不同的图像创建一个简单的三帧动画:

@override
Future<void> onLoad() async {
 final sprites = [0, 1, 2]
 .map((i) => Sprite.load('player_$i.png'));
 final animation = SpriteAnimation.spriteList(
 await Future.wait(sprites),
 stepTime: 0.01,
 );
 this.player = SpriteAnimationComponent(
 animation: animation,
 size: Vector2.all(64.0),
 );
}

SpriteAnimationGroupComponent

SpriteAnimationGroupComponent 是 SpriteAnimationComponent 的一个简单包装器,它使组件能够保存多个动画并在运行时更改当前正在播放的动画。由于此组件只是一个包装器,因此可以按照 SpriteAnimationComponent 中所述实现事件侦听器。

它的用法与 SpriteAnimationComponent 非常相似,但不是使用单个动画进行初始化,而是接收泛型类型 T 的 Map 作为键、SpriteAnimation 作为值以及当前动画。

enum RobotState {
idle,
running,
}

final running = await loadSpriteAnimation(/* omitted */);
final idle = await loadSpriteAnimation(/* omitted */);

final robot = SpriteAnimationGroupComponent<RobotState>(
animations: {
RobotState.running: running,
RobotState.idle: idle,
},
current: RobotState.idle,
);

// Changes current animation to "running"
robot.current = RobotState.running;

SpriteGroupComponent

SpriteGroupComponent 与其动画对应部分非常相似,但尤其适用于精灵。

class PlayerComponent extends SpriteGroupComponent<ButtonState>
with HasGameReference<SpriteGroupExample>, TapCallbacks {
@override
Future<void>? onLoad() async {
final pressedSprite = await gameRef.loadSprite(/* omitted */);
final unpressedSprite = await gameRef.loadSprite(/* omitted */);

sprites = {
ButtonState.pressed: pressedSprite,
ButtonState.unpressed: unpressedSprite,
};

current = ButtonState.unpressed;
}

// tap methods handler omitted...
}

SpawnComponent

如果想要在某个区域内随机生成敌人或强化道具,可以使用这个Component。SpawnComponent 需要一个工厂函数,用于创建新组件,以及应该在其中(或沿着边缘)生成组件的区域。

对于该区域,可以使用 Circle、Rectangle 或 Polygon 类,如果只想沿着形状的边缘生成组件,请将 within 参数设置为 false(默认为 true)。

例如,这将在定义的圆圈内每 0.5 秒随机生成 MyComponent 类型的新组件:

 

SpawnComponent(
 factory: (i) => MyComponent(size: Vector2(10, 20)),
 period: 0.5,
 area: Circle(Vector2(100, 200), 150),
);

SvgComponent

该组件使用 Svg 类的实例来表示具有在游戏中渲染的 svg 的组件:

@override
Future<void> onLoad() async {
 final svg = await Svg.load('android.svg');
 final android = SvgComponent.fromSvg(
 svg,
 position: Vector2.all(100),
 size: Vector2.all(100),
 );
}

ParallaxComponent

此组件可用于渲染具有深度感的背景,方法是在彼此之上绘制多个透明图像,其中每个图像或动画 (ParallaxRenderer) 以不同的速度移动。

其原理是,当看着地平线并移动时,较近的物体似乎比远处的物体移动得更快。

此组件模拟了这种效果,从而产生了更逼真的背景效果。

最简单的 ParallaxComponent 是这样创建的:

@override
Future<void> onLoad() async {
 final parallaxComponent = await loadParallaxComponent([
 ParallaxImageData('bg.png'),
 ParallaxImageData('trees.png'),
 ]);
 add(parallaxComponent);
}

ShapeComponents

ShapeComponent 是用于表示可扩展几何形状的基类。这些形状有不同的定义外观的方式,但它们都有可以修改的大小和角度,并且形状定义将相应地缩放或旋转形状。

这些形状旨在作为一种工具,以比与碰撞检测系统一起使用更通用的方式使用几何形状,在碰撞检测系统中,需要使用 ShapeHitboxes。

PolygonComponent

通过在构造函数中为其提供一个点列表(称为顶点)来创建 PolygonComponent。此列表将转换为具有一定大小的多边形,该多边形仍可缩放和旋转。

例如,这将创建一个从 (50, 50) 到 (100, 100) 的正方形,其中心位于 (75, 75):

void main() {
 PolygonComponent([
 Vector2(100, 100),
 Vector2(100, 50),
 Vector2(50, 50),
 Vector2(50, 100),
 ]);
}

RectangleComponent

RectangleComponent 的创建方式与 PositionComponent 的创建方式非常相似,因为它也有一个边界矩形。

void main() {
 RectangleComponent(
 position: Vector2(10.0, 15.0),
 size: Vector2.all(10),
 angle: pi/2,
 anchor: Anchor.center,
 );
}

CircleComponent

如果您知道圆的位置和/或半径从起点开始的长度,则可以使用可选参数 radius 和 position 来设置它们。

以下将创建一个 CircleComponent,其中心位于 (100, 100),半径为 5,因此大小为 Vector2(10, 10)。

void main() {
 CircleComponent(radius: 5, position: Vector2.all(100), anchor: Anchor.center);
}

CustomPainterComponent

CustomPainter 是一个 Flutter 类,与 CustomPaint 小部件一起使用,在 Flutter 应用程序内渲染自定义形状。

Flame 提供了一个可以渲染 CustomPainter 的组件,称为 CustomPainterComponent,它接收自定义画家并将其渲染在游戏画布上。

这可用于在您的 Flame 游戏和 Flutter 小部件之间共享自定义渲染逻辑。

CameraComponent

FlameGame
├── World
│ ├── Player
│ └── Enemy
└── CameraComponent
 ├── Viewfinder
 │ ├── HudButton
 │ └── FpsTextComponent
 └── Viewport

为了理解 CameraComponent的工作原理,想象一下游戏世界是一个独立于应用程序而存在的实体。想象一下游戏仅仅是一个窗口,可以通过它查看那个世界。可以随时关闭该窗口,游戏世界仍然存在。或者,相反,可以同时打开多个窗口来查看同一个世界(或不同的世界)。

有了这种思维方式,我们现在可以理解 CameraComponent 的工作原理。

首先,有 World 类,它包含游戏世界内的所有组件。World 组件可以安装在任何地方,例如在游戏类的根目录下,就像内置的 World 一样。

然后,有一个 CameraComponent 类“查看”世界。CameraComponent 内部有一个 Viewport 和一个 Viewfinder,既可以灵活地在屏幕上的任何位置渲染世界,也可以控制查看位置和角度。CameraComponent 还包含一个背景组件,该组件在世界下方静态渲染。

World

此组件应用于托管组成游戏世界的所有其他组件。World 类的主要属性是它不通过传统方式渲染 - 而是由一个或多个 CameraComponents 渲染以“查看”世界。在 FlameGame 类中,有一个名为 world 的世界,默认情况下会添加并与名为 camera 的默认 CameraComponent 配对。

游戏可以有多个 World 实例,可以同时或在不同时间渲染。例如,如果您有两个世界 A 和 B 以及一个摄像头,那么将该摄像头的目标从 A 切换到 B 将立即将视图切换到世界 B,而无需卸载 A 然后安装 B。

与大多数组件一样,可以通过在其构造函数中使用 children 参数或使用 add 或 addAll 方法将子项添加到 World。

对于许多想要扩展世界并在其中创建逻辑的游戏,这样的游戏结构可能如下所示:

void main() {
runApp(GameWidget(FlameGame(world: MyWorld())));
}

class MyWorld extends World {
@override
Future<void> onLoad() async {
// Load all the assets that are needed in this world
// and add components etc.
}
}

CameraComponent

这是一个渲染世界的组件。多个摄像机可以同时观察同一个世界。

FlameGame 类中有一个名为 camera 的默认 CameraComponent,它与默认世界配对,因此如果您的游戏不需要,您无需创建或添加自己的 CameraComponent。

CameraComponent 内部还有两个其他组件:Viewport 和 Viewfinder,这些组件始终是摄像机的子组件。

FlameGame 类的构造函数中有一个 camera 字段,因此您可以设置所需的默认摄像机类型,例如具有固定分辨率的摄像机:

void main() {
 runApp(
 GameWidget(
 FlameGame(
 camera: CameraComponent.withFixedResolution(
 width: 800,
 height: 600,
 ),
 world: MyWorld(),
 ),
 ),
 );
}

具有固定分辨率的 CameraComponent

此命名构造函数可让用户的设备具有选择的固定分辨率。例如:

final camera = CameraComponent.withFixedResolution(
 world: myWorldComponent,
 width: 800,
 height: 600,
);

这将创建一个视口位于屏幕中间的相机,占用尽可能多的空间,同时仍保持 4:3(800x600)的宽高比,并显示大小为 800 x 600 的游戏世界区域。

“固定分辨率”使用起来非常简单,但它会充分利用用户的可用屏幕空间,除非他们的设备恰好具有与选择的尺寸相同的宽高比。

Viewport

视口是一个窗口,通过它可以看见世界。该窗口在屏幕上具有特定的大小、形状和位置。有多种视口可用,可以随时实现自己的视口。

视口是一个组件,这意味着可以向其中添加其他组件。这些子组件将受到视口位置的影响,但不会受到其剪辑蒙版的影响。因此,如果视口是游戏世界的“窗口”,那么它的子项就是可以放在窗口顶部的东西。

向视口添加元素是实现“HUD”组件的一种便捷方式。

以下视口可用:

  • MaxViewport(默认) - 此视口扩展到游戏允许的最大尺寸,即它将等于游戏画布的尺寸。
  • FixedResolutionViewport - 保持分辨率和宽高比固定,如果与宽高比不匹配,则侧面会有黑条。
  • FixedSizeViewport - 具有预定义大小的简单矩形视口。
  • FixedAspectRatioViewport – 一个矩形视口,可展开以适合游戏画布,但保留其纵横比。
  • CircularViewport – 一个圆形视口,大小固定。

如果将子项添加到视口,它们将作为静态 HUD 出现在世界面前。

Viewfinder

相机的这一部分负责了解我们当前正在查看底层游戏世界中的哪个位置。取景器还控制缩放级别和视图的旋转角度。

取景器的锚点属性允许指定视口内的哪个点作为相机的“逻辑中心”。例如,在横向卷轴动作游戏中,通常将相机聚焦在主角身上,主角不在屏幕中心显示,而是靠近左下角。这个偏离中心的位置将是相机的“逻辑中心”,由取景器的锚点控制。

如果将子项添加到取景器,它们将出现在世界前面,但在视口后面,并且具有与应用于世界的相同变换,因此这些组件不是静态的。

相机控制

使用相机函数,例如 follow()、moveBy() 和 moveTo()。从本质上讲,这种方法使用的效果/行为与 (2) 中相同。

将效果和/或行为应用于相机的取景器或视口。效果和行为是特殊类型的组件,其目的是随时间修改组件的某些属性。

手动执行。始终可以覆盖 CameraComponent.update() 方法(或取景器或视口上的相同方法),并在其中根据需要更改取景器的位置或缩放。这种方法在某些情况下可能可行,但一般不建议使用。

CameraComponent 有几种控制其行为的方法:

  • follow() 将强制相机跟随提供的目标。您可以选择限制相机的最大移动速度,或允许其仅水平/垂直移动。
  • stop() 将撤消上一次调用的效果并将相机停止在当前位置。
  • moveBy() 可用于将相机移动指定的偏移量。如果相机已跟随另一个组件或正在移动,则会自动取消这些行为。
  • moveTo() 可用于将相机移动到世界地图上的指定点。如果相机已跟随另一个组件或正在向另一个点移动,则会自动取消这些行为。
  • setBounds() 允许为相机允许移动的位置添加限制。这些限制采用 Shape 的形式,通常是矩形,但也可以是任何其他形状。

visibleWorldRect

相机公开了visibleWorldRect属性,这是一个矩形,用于描述当前通过相机可见的世界区域。可以使用此区域来避免渲染视野之外的组件,或较少更新远离玩家的对象。

visibleWorldRect是一个缓存属性,每当相机移动或视口改变其大小时,它都会自动更新。

canSee

CameraComponent 有一个名为 canSee 的方法,可用于检查组件是否从相机视角可见。例如,这对于剔除不在视野内的组件很有用。

if (!camera.canSee(component)) {
 component.removeFromParent(); // Cull the component
}
点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00