mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 00:17:20 +08:00
feat: quad tree broadphase support (#1894)
Quad tree broadphase support.
This commit is contained in:
432
examples/lib/stories/collision_detection/quadtree_example.dart
Normal file
432
examples/lib/stories/collision_detection/quadtree_example.dart
Normal file
@ -0,0 +1,432 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame/layers.dart';
|
||||
import 'package:flutter/material.dart' hide Image, Draggable;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const tileSize = 8.0;
|
||||
|
||||
class QuadTreeExample extends FlameGame
|
||||
with HasQuadTreeCollisionDetection, KeyboardEvents, ScrollDetector {
|
||||
QuadTreeExample();
|
||||
|
||||
static const description = '''
|
||||
In this example the standard "Sweep and Prune" algorithm is replaced by
|
||||
"Quad Tree". Quad Tree is often a more efficient approach of handling collisions,
|
||||
its efficiency is shown especially on huge maps with big amounts of collidable
|
||||
components.
|
||||
Some bricks are highlighted when placed on an edge of a quadrant. It is
|
||||
important to understand that handling hitboxes on edges requires more
|
||||
resources.
|
||||
Blue lines visualise the quad tree's quadrant positions.
|
||||
|
||||
Use WASD to move the player and use the mouse scroll to change zoom.
|
||||
Hold direction button and press space to fire a bullet.
|
||||
Notice that bullet will fly above water but collides with bricks.
|
||||
|
||||
Also notice that creating a lot of bullets at once leads to generating new
|
||||
quadrants on the map since it becomes more than 25 objects in one quadrant.
|
||||
|
||||
Press O button to rescan the tree and optimize it, removing unused quadrants.
|
||||
|
||||
Press T button to toggle player to collide with other objects.
|
||||
''';
|
||||
|
||||
static const mapSize = 300;
|
||||
static const bricksCount = 8000;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
super.onLoad();
|
||||
|
||||
const mapWidth = mapSize * tileSize;
|
||||
const mapHeight = mapSize * tileSize;
|
||||
initializeCollisionDetection(
|
||||
mapDimensions: const Rect.fromLTWH(0, 0, mapWidth, mapHeight),
|
||||
minimumDistance: 10,
|
||||
);
|
||||
|
||||
final random = Random();
|
||||
final spriteBrick = await Sprite.load(
|
||||
'retro_tiles.png',
|
||||
srcPosition: Vector2.all(0),
|
||||
srcSize: Vector2.all(tileSize),
|
||||
);
|
||||
|
||||
final spriteWater = await Sprite.load(
|
||||
'retro_tiles.png',
|
||||
srcPosition: Vector2(0, tileSize),
|
||||
srcSize: Vector2.all(tileSize),
|
||||
);
|
||||
for (var i = 0; i < bricksCount; i++) {
|
||||
final x = random.nextInt(mapSize);
|
||||
final y = random.nextInt(mapSize);
|
||||
final brick = Brick(
|
||||
position: Vector2(x.toDouble() * tileSize, y.toDouble() * tileSize),
|
||||
size: Vector2.all(tileSize),
|
||||
priority: 0,
|
||||
sprite: spriteBrick,
|
||||
);
|
||||
add(brick);
|
||||
staticLayer.components.add(brick);
|
||||
}
|
||||
|
||||
staticLayer.reRender();
|
||||
camera.viewport = FixedResolutionViewport(Vector2(500, 250));
|
||||
final playerPoint = Vector2.all(mapSize * tileSize / 2);
|
||||
|
||||
final player =
|
||||
Player(position: playerPoint, size: Vector2.all(tileSize), priority: 2);
|
||||
add(player);
|
||||
this.player = player;
|
||||
camera.followComponent(player);
|
||||
|
||||
final brick = Brick(
|
||||
position: playerPoint.translate(0, -tileSize * 2),
|
||||
size: Vector2.all(tileSize),
|
||||
priority: 0,
|
||||
sprite: spriteBrick,
|
||||
);
|
||||
add(brick);
|
||||
staticLayer.components.add(brick);
|
||||
|
||||
final water1 = Water(
|
||||
position: playerPoint.translate(0, tileSize * 2),
|
||||
size: Vector2.all(tileSize),
|
||||
priority: 0,
|
||||
sprite: spriteWater,
|
||||
);
|
||||
add(water1);
|
||||
|
||||
final water2 = Water(
|
||||
position: playerPoint.translate(tileSize * 2, 0),
|
||||
size: Vector2.all(tileSize),
|
||||
priority: 0,
|
||||
sprite: spriteWater,
|
||||
);
|
||||
add(water2);
|
||||
|
||||
final water3 = Water(
|
||||
position: playerPoint.translate(-tileSize * 2, 0),
|
||||
size: Vector2.all(tileSize),
|
||||
priority: 0,
|
||||
sprite: spriteWater,
|
||||
);
|
||||
add(water3);
|
||||
|
||||
add(QuadTreeDebugComponent(collisionDetection));
|
||||
add(LayerComponent(staticLayer));
|
||||
add(FpsTextComponent());
|
||||
camera.zoom = 1;
|
||||
}
|
||||
|
||||
final elapsedMicroseconds = <double>[];
|
||||
|
||||
late Player player;
|
||||
final _playerDisplacement = Vector2.zero();
|
||||
var _fireBullet = false;
|
||||
|
||||
final staticLayer = StaticLayer();
|
||||
static const stepSize = 1.0;
|
||||
|
||||
@override
|
||||
KeyEventResult onKeyEvent(
|
||||
RawKeyEvent event,
|
||||
Set<LogicalKeyboardKey> keysPressed,
|
||||
) {
|
||||
for (final key in keysPressed) {
|
||||
if (key == LogicalKeyboardKey.keyW && player.canMoveTop) {
|
||||
_playerDisplacement.setValues(0, -stepSize);
|
||||
player.position = player.position.translate(0, -stepSize);
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyA && player.canMoveLeft) {
|
||||
_playerDisplacement.setValues(-stepSize, 0);
|
||||
player.position = player.position.translate(-stepSize, 0);
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyS && player.canMoveBottom) {
|
||||
_playerDisplacement.setValues(0, stepSize);
|
||||
player.position = player.position.translate(0, stepSize);
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyD && player.canMoveRight) {
|
||||
_playerDisplacement.setValues(stepSize, 0);
|
||||
player.position = player.position.translate(stepSize, 0);
|
||||
}
|
||||
if (key == LogicalKeyboardKey.space) {
|
||||
_fireBullet = true;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyT) {
|
||||
final collisionType = player.hitbox.collisionType;
|
||||
if (collisionType == CollisionType.active) {
|
||||
player.hitbox.collisionType = CollisionType.inactive;
|
||||
} else if (collisionType == CollisionType.inactive) {
|
||||
player.hitbox.collisionType = CollisionType.active;
|
||||
}
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyO) {
|
||||
collisionDetection.broadphase.tree.optimize();
|
||||
}
|
||||
}
|
||||
if (_fireBullet && !_playerDisplacement.isZero()) {
|
||||
final bullet = Bullet(
|
||||
position: player.position,
|
||||
displacement: _playerDisplacement * 50,
|
||||
);
|
||||
add(bullet);
|
||||
_playerDisplacement.setZero();
|
||||
_fireBullet = false;
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@override
|
||||
void onScroll(PointerScrollInfo info) {
|
||||
camera.zoom += info.scrollDelta.game.y.sign * 0.08;
|
||||
camera.zoom = camera.zoom.clamp(0.05, 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
//#region Player
|
||||
|
||||
class Player extends SpriteComponent
|
||||
with CollisionCallbacks, HasGameRef<QuadTreeExample> {
|
||||
Player({
|
||||
required super.position,
|
||||
required super.size,
|
||||
required super.priority,
|
||||
}) {
|
||||
Sprite.load(
|
||||
'retro_tiles.png',
|
||||
srcSize: Vector2.all(tileSize),
|
||||
srcPosition: Vector2(tileSize * 3, tileSize),
|
||||
).then((value) {
|
||||
sprite = value;
|
||||
});
|
||||
|
||||
add(hitbox);
|
||||
}
|
||||
|
||||
final hitbox = RectangleHitbox();
|
||||
bool canMoveLeft = true;
|
||||
bool canMoveRight = true;
|
||||
bool canMoveTop = true;
|
||||
bool canMoveBottom = true;
|
||||
|
||||
@override
|
||||
void onCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
PositionComponent other,
|
||||
) {
|
||||
final myCenter =
|
||||
Vector2(position.x + tileSize / 2, position.y + tileSize / 2);
|
||||
if (other is GameCollideable) {
|
||||
final diffX = myCenter.x - other.cachedCenter.x;
|
||||
if (diffX < 0) {
|
||||
canMoveRight = false;
|
||||
} else if (diffX > 0) {
|
||||
canMoveLeft = false;
|
||||
}
|
||||
|
||||
final diffY = myCenter.y - other.cachedCenter.y;
|
||||
if (diffY < 0) {
|
||||
canMoveBottom = false;
|
||||
} else if (diffY > 0) {
|
||||
canMoveTop = false;
|
||||
}
|
||||
final newPos = Vector2(position.x + diffX / 3, position.y + diffY / 3);
|
||||
position = newPos;
|
||||
}
|
||||
super.onCollisionStart(intersectionPoints, other);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollisionEnd(PositionComponent other) {
|
||||
canMoveLeft = true;
|
||||
canMoveRight = true;
|
||||
canMoveTop = true;
|
||||
canMoveBottom = true;
|
||||
super.onCollisionEnd(other);
|
||||
}
|
||||
}
|
||||
|
||||
class Bullet extends PositionComponent with CollisionCallbacks, HasPaint {
|
||||
Bullet({required super.position, required this.displacement}) {
|
||||
paint.color = Colors.deepOrange;
|
||||
priority = 10;
|
||||
size = Vector2.all(1);
|
||||
add(RectangleHitbox());
|
||||
}
|
||||
|
||||
final Vector2 displacement;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.drawCircle(Offset.zero, 1, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
final d = displacement * dt;
|
||||
position = Vector2(position.x + d.x, position.y + d.y);
|
||||
super.update(dt);
|
||||
}
|
||||
|
||||
@override
|
||||
bool onComponentTypeCheck(PositionComponent other) {
|
||||
if (other is Player || other is Water) {
|
||||
return false;
|
||||
}
|
||||
return super.onComponentTypeCheck(other);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
PositionComponent other,
|
||||
) {
|
||||
if (other is Brick) {
|
||||
removeFromParent();
|
||||
}
|
||||
super.onCollisionStart(intersectionPoints, other);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Environment
|
||||
|
||||
class Brick extends SpriteComponent
|
||||
with CollisionCallbacks, GameCollideable, UpdateOnce {
|
||||
Brick({
|
||||
required super.position,
|
||||
required super.size,
|
||||
required super.priority,
|
||||
required super.sprite,
|
||||
}) {
|
||||
initCenter();
|
||||
initCollision();
|
||||
}
|
||||
|
||||
bool rendered = false;
|
||||
|
||||
@override
|
||||
void renderTree(Canvas canvas) {
|
||||
if (!rendered) {
|
||||
super.renderTree(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Water extends SpriteComponent
|
||||
with CollisionCallbacks, GameCollideable, UpdateOnce {
|
||||
Water({
|
||||
required super.position,
|
||||
required super.size,
|
||||
required super.priority,
|
||||
required super.sprite,
|
||||
}) {
|
||||
initCenter();
|
||||
initCollision();
|
||||
}
|
||||
}
|
||||
|
||||
mixin GameCollideable on PositionComponent {
|
||||
void initCollision() {
|
||||
add(RectangleHitbox()..collisionType = CollisionType.passive);
|
||||
}
|
||||
|
||||
void initCenter() {
|
||||
cachedCenter =
|
||||
Vector2(position.x + tileSize / 2, position.y + tileSize / 2);
|
||||
}
|
||||
|
||||
late final Vector2 cachedCenter;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Utils
|
||||
|
||||
mixin UpdateOnce on PositionComponent {
|
||||
bool updateOnce = true;
|
||||
|
||||
@override
|
||||
void updateTree(double dt) {
|
||||
if (updateOnce) {
|
||||
super.updateTree(dt);
|
||||
updateOnce = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StaticLayer extends PreRenderedLayer {
|
||||
StaticLayer();
|
||||
|
||||
List<PositionComponent> components = [];
|
||||
|
||||
@override
|
||||
void drawLayer() {
|
||||
for (final element in components) {
|
||||
if (element is Brick) {
|
||||
element.rendered = false;
|
||||
element.renderTree(canvas);
|
||||
element.rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LayerComponent extends PositionComponent {
|
||||
LayerComponent(this.layer);
|
||||
|
||||
StaticLayer layer;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
layer.render(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
extension Vector2Ext on Vector2 {
|
||||
Vector2 translate(double x, double y) {
|
||||
return Vector2(this.x + x, this.y + y);
|
||||
}
|
||||
}
|
||||
|
||||
class QuadTreeDebugComponent extends PositionComponent with HasPaint {
|
||||
QuadTreeDebugComponent(QuadTreeCollisionDetection cd) {
|
||||
dbg = QuadTreeNodeDebugInfo.init(cd);
|
||||
paint.color = Colors.blue;
|
||||
paint.style = PaintingStyle.stroke;
|
||||
priority = 10;
|
||||
}
|
||||
|
||||
late final QuadTreeNodeDebugInfo dbg;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
final nodes = dbg.nodes;
|
||||
for (final node in nodes) {
|
||||
canvas.drawRect(node.rect, paint);
|
||||
final nodeElements = node.ownElements;
|
||||
Paint? boxPaint;
|
||||
if (!node.noChildren && nodeElements.isNotEmpty) {
|
||||
boxPaint = Paint();
|
||||
boxPaint.style = PaintingStyle.stroke;
|
||||
boxPaint.color = Colors.lightGreenAccent;
|
||||
boxPaint.strokeWidth = 1;
|
||||
}
|
||||
for (final box in nodeElements) {
|
||||
if (boxPaint != null) {
|
||||
canvas.drawRect(box.aabb.toRect(), boxPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
Reference in New Issue
Block a user