Skip to content

Instantly share code, notes, and snippets.

@Skyost
Last active June 21, 2023 11:22
Show Gist options
  • Save Skyost/fe57eac9b49162a8382a5a973ec88d94 to your computer and use it in GitHub Desktop.
Save Skyost/fe57eac9b49162a8382a5a973ec88d94 to your computer and use it in GitHub Desktop.
Minimal reproducible example for https://github.com/flame-engine/flame/issues/2574. Note that this code doesn't pretend to be clean.

Sadly, Github Gist doesn't support subdirectories. Here's the structure :

test_flame/
├── assets/
│   ├── images/
│   │   └── tiles.png
│   └── tiles/
│       ├── level.tmx
│       └── tiles.tsx
├── lib/
│   ├── game.dart
│   ├── ground.dart
│   ├── main.dart
│   ├── player.dart
│   └── utils.dart
└── pubspec.yaml
import 'dart:math';
import 'package:flame/camera.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flutter/material.dart';
import 'package:test_flame/ground.dart';
import 'package:test_flame/player.dart';
class GravityTestGame extends FlameGame with HasCollisionDetection, HasKeyboardHandlerComponents {
static final Vector2 tileSize = Vector2.all(32);
static final Vector2 viewportSize = Vector2(640, 330);
late final CameraComponent cameraComponent;
@override
Future<void>? onLoad() async {
await Flame.device.fullScreen();
await Flame.device.setLandscape();
cameraComponent = CameraComponent(
world: TiledWorld(),
viewport: MaxViewport(),
viewfinder: Viewfinder()..visibleGameSize = Vector2(viewportSize.x, viewportSize.y),
);
addAll([cameraComponent, cameraComponent.world]);
}
}
class TiledWorld extends World with HasGameRef<GravityTestGame> {
late final RenderableTiledMap tileMap;
@override
Future<void> onLoad() async {
TiledComponent mapComponent = await TiledComponent.load('level.tmx', GravityTestGame.tileSize);
add(mapComponent);
ObjectGroup spawnPointsLayer = mapComponent.tileMap.getLayer<ObjectGroup>('spawnPoints')!;
for (TiledObject spawnPoint in spawnPointsLayer.objects) {
if (spawnPoint.class_ == 'player') {
add(PlayerComponent(
spawnPoint: spawnPoint,
));
}
}
ObjectGroup groundObjects = mapComponent.tileMap.getLayer<ObjectGroup>('ground')!;
for (TiledObject groundObject in groundObjects.objects) {
add(GroundComponent(
groundObject: groundObject,
));
}
}
@override
void render(Canvas canvas) {
canvas.drawColor(const Color(0xffffffff), BlendMode.srcIn);
super.render(canvas);
}
}
class PhysicalSystem {
static const double playerJumpHeight = 70;
static const double playerJumpTime = 1 / 5;
static const double terminalVelocity = 200;
static final double gravity = playerJumpHeight / (2 * pow(playerJumpTime, 2));
static final double jumpSpeed = sqrt(2 * playerJumpHeight * gravity);
static const double playerHorizontalMoveSpeed = slowMoveSpeed * 4;
static const double slowMoveSpeed = 50;
}
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame_tiled/flame_tiled.dart';
class GroundComponent extends PositionComponent {
final List<Vector2> polygon;
final List<(Vector2, Vector2)> _horizontalGrounds = [];
GroundComponent({
required TiledObject groundObject,
}) : polygon = groundObject.polygon.map((point) => Vector2(point.x, point.y)).toList(),
super(
position: Vector2(groundObject.x, groundObject.y),
);
@override
void onLoad() {
add(
PolygonHitbox(polygon)..collisionType = CollisionType.passive,
);
for (int i = 0; i < polygon.length; i++) {
Vector2 vertex1 = position + polygon[i];
Vector2 vertex2 = position + polygon[i == polygon.length - 1 ? 0 : (i + 1)];
if (vertex1.y == vertex2.y) {
_horizontalGrounds.add(vertex1.x <= vertex2.x ? (vertex1, vertex2) : (vertex2, vertex1));
}
}
}
(Vector2, Vector2)? findClosestHorizontalGround(Vector2 position) {
(Vector2, Vector2)? result;
for ((Vector2, Vector2) horizontalGround in _horizontalGrounds) {
Vector2 vertex1 = horizontalGround.$1;
Vector2 vertex2 = horizontalGround.$2;
if (vertex1.x > position.x || vertex2.x < position.x || vertex1.y < position.y) {
continue;
}
if (result == null || vertex1.y < result.$1.y) {
result = horizontalGround;
}
}
return result == null ? null : (result.$1, result.$2);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="15" height="36" tilewidth="32" tileheight="32" infinite="0" nextlayerid="7" nextobjectid="133">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="6" name="tiles" width="15" height="36">
<data encoding="base64">
AQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAA
</data>
</layer>
<objectgroup id="4" name="ground">
<object id="129" x="160" y="864">
<polygon points="160,64 192,64 192,32 128,32 128,0 192,0 192,-32 224,-32 224,-64 192,-64 192,-96 128,-96 128,-128 192,-128 192,-224 96,-224 96,-256 224,-256 224,-320 192,-320 192,-352 128,-352 128,-384 192,-384 192,-480 96,-480 96,-512 224,-512 224,-576 192,-576 192,-608 128,-608 128,-640 192,-640 192,-736 160,-736 160,-768 -32,-768 -32,-672 -64,-672 -64,-608 -32,-608 -32,-576 64,-576 64,-544 -32,-544 -32,-448 64,-448 64,-416 -32,-416 -32,-384 -64,-384 -64,-320 64,-320 64,-288 -32,-288 -32,-192 64,-192 64,-160 -32,-160 -32,-64 64,-64 64,-32 -32,-32 -32,0 0,0 0,96 32,96 32,192 160,192"/>
</object>
</objectgroup>
<objectgroup id="2" name="spawnPoints">
<object id="9" type="player" x="320" y="800" width="32" height="32"/>
</objectgroup>
</map>
import 'package:flame/game.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:test_flame/game.dart';
import 'package:window_manager/window_manager.dart';
GravityTestGame _game = GravityTestGame();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
await windowManager.ensureInitialized();
windowManager.waitUntilReadyToShow(
WindowOptions(
minimumSize: GravityTestGame.viewportSize.toSize(),
size: GravityTestGame.viewportSize.toSize(),
center: true,
), () async {
await windowManager.show();
await windowManager.focus();
});
}
runApp(MaterialApp(
title: 'Test',
home: Scaffold(
backgroundColor: Colors.white,
body: GameWidget<GravityTestGame>(game: _game),
)
));
}
import 'dart:math';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:test_flame/game.dart';
import 'package:test_flame/ground.dart';
import 'package:test_flame/utils.dart';
class PlayerComponent extends PositionComponent with HasGameRef<GravityTestGame>, KeyboardHandler, CollisionCallbacks, HasVelocity, HasGroundCollision, HasHorizontalMove, HasJump {
PlayerComponent({
required TiledObject spawnPoint,
}) : super(
position: Vector2(spawnPoint.x, spawnPoint.y),
size: Vector2(spawnPoint.width, spawnPoint.height),
anchor: Anchor.center,
);
@override
void onLoad() {
super.onLoad();
gameRef.cameraComponent.follow(this);
}
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
resetHorizontalMove();
if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
moveHorizontally(PlayerHorizontalMovement.right, reset: false);
}
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
moveHorizontally(PlayerHorizontalMovement.left, reset: false);
}
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) || keysPressed.contains(LogicalKeyboardKey.space)) {
jump();
}
return true;
}
}
mixin HasVelocity on PositionComponent {
final Vector2 velocity = Vector2.zero();
bool _cancelNextGravity = false;
bool _cancelNextXVelocity = false;
@override
@mustCallSuper
void update(double dt) {
velocity.y = min(velocity.y + (_cancelNextGravity ? 0 : PhysicalSystem.gravity) * dt, PhysicalSystem.terminalVelocity);
_cancelNextGravity = false;
if (_cancelNextXVelocity) {
position.y += velocity.y * dt;
_cancelNextXVelocity = false;
} else {
position += velocity * dt;
}
}
void cancelNextGravityRun() => _cancelNextGravity = true;
void cancelNextXVelocity() => _cancelNextXVelocity = true;
}
mixin HasGroundCollision on HasVelocity, CollisionCallbacks {
static final Vector2 _leftDirection = Vector2(-1, 0);
static final Vector2 _upDirection = Vector2(0, -1);
bool isFalling = true;
@override
@mustCallSuper
void onLoad() {
super.onLoad();
add(CircleHitbox()..renderShape = true);
}
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is! GroundComponent) {
return;
}
CollisionIntersectionPoints collisionIntersectionPoints = CollisionIntersectionPoints.fromCollision(intersectionPoints);
if (collisionIntersectionPoints.shouldHandle) {
if (collisionIntersectionPoints.verticalIntersectionPoints != null) {
(Vector2, double) collisionNormalAndSeparationDistance = _calculateCollisionNormalAndSeparationDistance(collisionIntersectionPoints.verticalIntersectionPoints!);
double orientation = _leftDirection.dot(collisionNormalAndSeparationDistance.$1);
bool hasCancelledXVelocity = false;
if (orientation.abs() > 0.9 && velocity.x * orientation > 0) {
cancelNextXVelocity();
hasCancelledXVelocity = true;
}
if (!hasCancelledXVelocity) {
position += collisionNormalAndSeparationDistance.$1.scaled(collisionNormalAndSeparationDistance.$2);
}
}
if (collisionIntersectionPoints.horizontalIntersectionPoints != null) {
(Vector2, double) collisionNormalAndSeparationDistance = _calculateCollisionNormalAndSeparationDistance(collisionIntersectionPoints.horizontalIntersectionPoints!);
bool hasCancelledGravity = false;
if (_upDirection.dot(collisionNormalAndSeparationDistance.$1) > 0.9) {
velocity.y = min(velocity.y, 0);
if (collisionNormalAndSeparationDistance.$1.x == 0) {
cancelNextGravityRun();
hasCancelledGravity = true;
}
isFalling = false;
} else {
isFalling = true;
}
if (!hasCancelledGravity) {
position += collisionNormalAndSeparationDistance.$1.scaled(collisionNormalAndSeparationDistance.$2);
}
} else {
isFalling = true;
}
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (other is GroundComponent && !isRemoved && !isColliding) {
isFalling = true;
}
}
(Vector2, double) _calculateCollisionNormalAndSeparationDistance((Vector2, Vector2) intersectionPoints) {
Vector2 collisionNormal = absoluteCenter - intersectionPoints.calculateMid();
double separationDistance = (width / 2) - collisionNormal.length;
collisionNormal.normalize();
return (collisionNormal, separationDistance);
}
}
mixin HasJump on HasVelocity {
void jump() {
if (!isRemoved) {
if (this is! HasGroundCollision || !(this as HasGroundCollision).isFalling) {
velocity.y = -PhysicalSystem.jumpSpeed;
}
}
}
}
mixin HasHorizontalMove on HasVelocity {
int _horizontalDirection = 0;
@override
void update(double dt) {
velocity.x = _horizontalDirection * PhysicalSystem.playerHorizontalMoveSpeed;
super.update(dt);
}
void resetHorizontalMove() => _horizontalDirection = PlayerHorizontalMovement.none.horizontalDirection;
void moveHorizontally(PlayerHorizontalMovement movement, {bool reset = true}) {
if (reset) {
_horizontalDirection = PlayerHorizontalMovement.none.horizontalDirection;
}
if (isRemoved) {
return;
}
_horizontalDirection += movement.horizontalDirection;
if ((_horizontalDirection < 0 && !isFlippedHorizontally) || (_horizontalDirection > 0 && isFlippedHorizontally)) {
flipHorizontallyAroundCenter();
}
}
}
enum PlayerHorizontalMovement {
none(horizontalDirection: 0),
right(horizontalDirection: 1),
left(horizontalDirection: -1);
final int horizontalDirection;
const PlayerHorizontalMovement({
required this.horizontalDirection,
});
}
class CollisionIntersectionPoints {
final (Vector2, Vector2)? horizontalIntersectionPoints;
final (Vector2, Vector2)? verticalIntersectionPoints;
CollisionIntersectionPoints._internal({
this.horizontalIntersectionPoints,
this.verticalIntersectionPoints,
});
factory CollisionIntersectionPoints.fromCollision(Set<Vector2> intersectionPoints) {
(Vector2, Vector2)? horizontalIntersectionPoints;
(Vector2, Vector2)? verticalIntersectionPoints;
if (intersectionPoints.length >= 2) {
(Vector2, Vector2) intersectionPoints1 = (intersectionPoints.elementAt(0), intersectionPoints.elementAt(1));
if (intersectionPoints.length >= 4) {
(Vector2, Vector2) intersectionPoints2 = (intersectionPoints.elementAt(2), intersectionPoints.elementAt(3));
if (intersectionPoints2.$1.y == intersectionPoints2.$2.y) {
horizontalIntersectionPoints = intersectionPoints2;
verticalIntersectionPoints = intersectionPoints1;
} else {
horizontalIntersectionPoints = intersectionPoints1;
verticalIntersectionPoints = intersectionPoints2;
}
} else {
if (intersectionPoints1.$1.y == intersectionPoints1.$2.y) {
horizontalIntersectionPoints = intersectionPoints1;
} else {
verticalIntersectionPoints = intersectionPoints1;
}
}
}
return CollisionIntersectionPoints._internal(
horizontalIntersectionPoints: horizontalIntersectionPoints,
verticalIntersectionPoints: verticalIntersectionPoints,
);
}
bool get shouldHandle => horizontalIntersectionPoints != null || verticalIntersectionPoints != null;
}
name: test_flame
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=3.0.5 <4.0.0'
dependencies:
flutter:
sdk: flutter
flame: ^1.8.0
flame_tiled: ^1.11.0
window_manager: ^0.3.4
dev_dependencies:
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/tiles/
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.10.1" name="tiles" tilewidth="32" tileheight="32" tilecount="2" columns="1">
<image source="../images/tiles.png" width="32" height="32"/>
<tile id="0"/>
<tile id="1"/>
</tileset>
import 'package:flame/components.dart';
extension VectorRecordUtils on (Vector2, Vector2) {
Vector2 calculateMid() => ($1 + $2) / 2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment