|
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; |
|
} |