feat: quad tree broadphase support (#1894)

Quad tree broadphase support.
This commit is contained in:
Alexey Volkov
2022-09-27 14:53:27 +03:00
committed by GitHub
parent e80412c568
commit e33d5410a3
19 changed files with 1992 additions and 806 deletions

View 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