- 1:混入
- 1.1:HasCollisionDetection
- 1.2:CollisionCallbacks
- 1.3:碰撞顺序
- 2:ShapeHitbox
- 2.1:碰撞类型
- 3:Hitbox
- 4:碰撞实例
在上一篇文章中,我们介绍了Flame中常见的Component和Camera。在这篇文章中我们将介绍Flame中的碰撞检测。
大多数游戏都需要碰撞检测来检测并处理两个相交的组件。例如,一支箭射中敌人或玩家捡起一枚硬币。
在大多数碰撞检测系统中,使用一种称为命中框的东西来创建更精确的组件边界框。在 Flame 中,命中框是组件中可以对碰撞做出反应(并使手势输入)更准确的区域。
碰撞检测系统支持三种不同类型的形状,您可以从这些形状构建命中框,这些形状是多边形、矩形和圆形。可以将多个命中框添加到组件以形成可用于检测碰撞或是否包含点的区域,后者对于准确的手势检测非常有用。碰撞检测不会处理两个命中框碰撞时应该发生的情况,因此由用户来实现当例如两个 PositionComponents 具有相交的命中框时将发生的情况。
请注意,内置碰撞检测系统不会考虑两个碰撞盒之间相互超过的碰撞,当它们移动非常快或更新调用时间较长时(例如,如果您的应用不在前台),可能会发生这种情况。如果您想了解更多信息,这种行为称为隧道效应。
还请注意,碰撞检测系统有一个限制,如果对碰撞盒的祖先进行某些类型的翻转和缩放组合,则会导致其无法正常工作。
混入
HasCollisionDetection
如果想在游戏中使用碰撞检测,必须将HasCollisionDetection混入到FlameGame中,以便它可以跟踪可能发生碰撞的组件。
class MyGame extends FlameGame with HasCollisionDetection {
// ...
}
现在,当将 ShapeHitboxs 添加到随后添加到游戏中的组件时,它们将自动进行碰撞检测。
还可以将 HasCollisionDetection 直接添加到另一个组件而不是 FlameGame,例如添加到用于 CameraComponent 的 World。如果这样做,则添加到该组件树中的命中框将仅与该子树中的其他命中框进行比较,这使得在一个 FlameGame 中可以有多个具有碰撞检测的世界。
class CollisionDetectionWorld extends World with HasCollisionDetection {}
注意:命中框只会连接到一个碰撞检测系统,即具有混入HasCollisionDetection的最近的父级。
CollisionCallbacks
要对碰撞做出反应,应该将 CollisionCallbacks混入添加到组件中。例如:
class MyCollidable extends PositionComponent with CollisionCallbacks {
@override
void onCollision(Set<Vector2> points, PositionComponent other) {
if (other is ScreenHitbox) {
//...
} else if (other is YourOtherComponent) {
//...
}
}
@override
void onCollisionEnd(PositionComponent other) {
if (other is ScreenHitbox) {
//...
} else if (other is YourOtherComponent) {
//...
}
}
}
在这个例子中,我们使用 Dart 的 is 关键字来检查我们与哪种组件发生了碰撞。点集是命中框边缘相交的地方。
请注意,如果两个 PositionComponent 都实现了 onCollision 方法,则 onCollision 方法将在两个 PositionComponent 上调用,并且两个命中框也会调用 onCollision 方法。当两个组件和命中框开始或停止相互碰撞时,onCollisionStart 和 onCollisionEnd 方法也是如此。
当一个 PositionComponent(和命中框)开始与另一个 PositionComponent 发生碰撞时,onCollisionStart 和 onCollision 都会被调用,因此如果您不需要在碰撞开始时执行特定操作,则只需覆盖 onCollision,反之亦然。
默认情况下,所有碰撞框都是空心的,这意味着一个碰撞框可以被另一个碰撞框完全包围而不会触发碰撞。如果想将碰撞框设置为实心,可以设置 isSolid = true。实心碰撞框内的空心碰撞框将触发碰撞,但反之则不会。如果实心碰撞框上的边缘没有交点,则返回中心位置。
碰撞顺序
如果 Hitbox 在给定的时间步骤内与多个其他 Hitbox 发生碰撞,则 onCollision 回调将以基本随机的顺序调用。在某些情况下,这可能是一个问题,例如在弹跳球游戏中,球的轨迹可能因首先击中哪个其他对象而不同。为了帮助解决这个问题,可以使用 collisionsCompletedNotifier 侦听器 - 这会在碰撞检测过程结束时触发。
如何使用它的一个示例是在PositionComponent 中添加一个局部变量来保存它与之碰撞的其他组件:List<PositionComponent> collisionComponents = [];。然后使用 onCollision 回调将所有其他 PositionComponents 保存到此列表中:
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
collisionComponents.add(other);
super.onCollision(intersectionPoints, other);
}
ShapeHitbox
ShapeHitboxs 是普通组件,因此可以将它们添加到要添加 hitboxes 的组件中,就像任何其他组件一样:
class MyComponent extends PositionComponent {
@override
void onLoad() {
add(RectangleHitbox());
}
}
如果没有向 hitbox 添加任何参数(如上所示),hitbox 将尝试尽可能地填充其父级。除了让 hitbox 尝试填充其父级之外,还有两种方法可以启动 hitbox,一种是使用普通构造函数,您可以自行定义 hitbox,包括大小和位置等。另一种方法是使用相对构造函数,该构造函数根据其预期父级的大小定义 hitbox。
在某些特定情况下,可能只想处理 hitbox 之间的碰撞,而不将 onCollision* 事件传播到 hitbox 的父组件。例如,车辆可以有一个车身 hitbox 来控制碰撞,还有侧面 hitbox 来检查左转或右转的可能性。因此,与车身 hitbox 碰撞意味着与组件本身碰撞,而与侧面 hitbox 碰撞并不意味着真正的碰撞,不应传播到 hitbox 的父级。在这种情况下,可以将 triggersParentCollision 变量设置为 false:
class MyComponent extends PositionComponent {
late final MySpecialHitbox utilityHitbox;
@override
void onLoad() {
utilityHitbox = MySpecialHitbox();
add(utilityHitbox);
}
void update(double dt) {
if (utilityHitbox.isColliding) {
// do some specific things if hitbox is colliding
}
}
// component's onCollision* functions, ignoring MySpecialHitbox collisions.
}
class MySpecialHitbox extends RectangleHitbox {
MySpecialHitbox() {
triggersParentCollision = false;
}
// hitbox specific onCollision* functions
}
可以根据需要向 PositionComponent 添加任意数量的 ShapeHitbox,以构成更复杂的区域。例如,戴帽子的雪人可以用三个 CircleHitbox 和两个 RectangleHitbox 来表示它的帽子。
hitbox 可用于碰撞检测或使组件顶部的手势检测更准确。
碰撞类型
Hitbox有一个名为 collisionType 的字段,它定义碰撞盒何时应与另一个碰撞盒发生碰撞。通常,希望将尽可能多的碰撞盒设置为 CollisionType.passive,以使碰撞检测更高效。默认情况下,CollisionType 处于活动状态。
CollisionType 枚举包含以下值:
- active 与其他类型为active或passive的 Hitbox 发生碰撞
- passive 可以与其他类型为active的 Hitbox 发生碰撞
- inactive 不会与任何其他 Hitbox 发生碰撞
因此,如果有不需要检查彼此碰撞的碰撞箱,可以通过在构造函数中设置 collisionType: CollisionType.passive 将它们标记为被动,例如,这可能是地面组件,或者敌人可能不需要检查彼此之间的碰撞,那么它们也可以被标记为被动。
想象一下,在游戏中,有很多子弹,它们不能相互碰撞,飞向玩家,那么玩家将被设置为 CollisionType.active,子弹将被设置为 CollisionType.passive。
然后我们有inactive类型,它在碰撞检测中根本不被检查。例如,如果屏幕外的组件,目前不关心,但稍后可能会重新出现在视图中,因此它们不会完全从游戏中移除,则可以使用它。
Hitbox
- PolygonHitbox 多边形碰撞箱
- RectangleHitbox 矩形碰撞箱子
- CircleHitbox 圆形碰撞箱
- ScreenHitbox ScreenHitbox 是一个代表视口/屏幕边缘的组件。如果将 ScreenHitbox 添加到游戏,则其他带有碰撞盒的组件在与边缘发生碰撞时将收到通知。
- CompositeHitbox 在 CompositeHitbox 中,可以添加多个碰撞箱,以便它们模拟一个连接的碰撞箱。
碰撞实例
在了解以上的逻辑Hitbox和碰撞混入后,可以实现:定义一个Dash来从屏幕中间向下移动,在坐标(0,300)处放置一个矩形设置为被动碰撞。当Dash与矩形碰撞后将Dash停止移动。
import 'dart:async';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(GameWidget(game: MyGame()));
}
class MyGame extends FlameGame with HasCollisionDetection {
MyGame() : super(world: GameWorld());
@override
Color backgroundColor() => Colors.white;
}
class GameWorld extends World {
@override
FutureOr<void> onLoad() async {
debugMode = true;
await addAll([DashComponent(), BottomComponent()]);
}
}
class BottomComponent extends PositionComponent {
@override
FutureOr<void> onLoad() async {
size = Vector2(200, 20);
position = Vector2(x, 300);
anchor = Anchor.center;
add(RectangleHitbox()..collisionType = CollisionType.passive);
}
}
class DashComponent extends SpriteComponent with CollisionCallbacks {
bool _gameOver = false;
DashComponent() : super(anchor: Anchor.center, size: Vector2.all(80));
@override
FutureOr<void> onLoad() async {
sprite = await Sprite.load("dash.png");
await add(CircleHitbox()..collisionType = CollisionType.active);
}
@override
void update(double dt) {
if (!_gameOver) {
position += Vector2(0, 30) * dt;
}
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
_gameOver = true;
}
}
最终Dash会停留在与矩形碰撞的位置,由此则实现了碰撞检测的整个逻辑。