Skip to content

Instantly share code, notes, and snippets.

@spydon
Created April 18, 2022 21:00
Show Gist options
  • Save spydon/3682f85300b2aaaf541f7f6d6670d5fe to your computer and use it in GitHub Desktop.
Save spydon/3682f85300b2aaaf541f7f6d6670d5fe to your computer and use it in GitHub Desktop.
padracing
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/particles.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
class Background extends PositionComponent
with HasGameRef<PadRacingGame>, HasPaint {
Background() : super(priority: 0);
final Random rng = Random(1337);
late final Image _image;
@override
Future<void> onLoad() async {
final trackSize = PadRacingGame.trackSize;
paint..color = Colors.green;
final recorder = PictureRecorder();
final canvas = Canvas(recorder, trackSize.toRect());
final colors = [
Colors.green.withAlpha(100),
Colors.brown.withAlpha(100),
Colors.lightGreen.withAlpha(100),
];
for (var x = 0.0; x < trackSize.x; x += 0.2) {
for (var y = 0.0; y < trackSize.y; y += 0.2) {
paint
..color = (colors..shuffle(rng)).first
..darken(rng.nextDouble());
canvas.drawCircle(Offset(x, y), 0.3, paint);
}
}
final picture = recorder.endRecording();
_image = await picture.toImage(trackSize.x.toInt(), trackSize.y.toInt());
}
final _whitePaint = Paint();
@override
void render(Canvas canvas) {
canvas.drawImage(_image, Offset.zero, _whitePaint);
}
}
class Ball extends BodyComponent<PadRacingGame> {
static const radius = 80.0;
final Random rng = Random();
late final Image _image;
late final Path _clipPath;
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
final trackSize = PadRacingGame.trackSize;
final recorder = PictureRecorder();
final canvas = Canvas(recorder, trackSize.toRect());
final colors = [
Colors.lightBlue,
Colors.blue,
Colors.deepPurpleAccent,
];
_clipPath = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius));
canvas.translate(radius, radius);
for (var angle = 0.0; angle < 2 * pi; angle += 0.05) {
canvas.rotate(0.05);
for (var x = radius; x > 0; x -= 0.2) {
paint
..color = (colors..shuffle(rng)).first
..darken(x / radius);
canvas.drawCircle(Offset(x, 0), 3, paint);
}
}
final picture = recorder.endRecording();
_image = await picture.toImage((radius * 2).toInt(), (radius * 2).toInt());
//gameRef.camera.followBodyComponent(this);
}
@override
Body createBody() {
paint..color = Colors.amber;
final startPosition = Vector2(200, 245);
final def = BodyDef()
..type = BodyType.kinematic
..position = startPosition;
final body = world.createBody(def)..angularVelocity = 1;
final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape)
..restitution = 0.5
..friction = 0.5;
return body..createFixture(fixtureDef);
}
@override
void render(Canvas canvas) {
canvas.clipPath(_clipPath);
canvas.translate(-radius, -radius);
canvas.drawImage(_image, Offset.zero, paint);
}
}
class Car extends BodyComponent<PadRacingGame> {
Car({required this.playerNumber, required this.cameraComponent})
: super(priority: 3);
static final colors = [
Colors.orange,
Colors.lightBlue,
];
final ValueNotifier<int> lap = ValueNotifier<int>(0);
late final TextComponent lapText;
final int playerNumber;
final Set<GroundSensor> passedStartControl = {};
final CameraComponent cameraComponent;
final double _backTireMaxDriveForce = 300.0;
final double _frontTireMaxDriveForce = 500.0;
final double _backTireMaxLateralImpulse = 8.5;
final double _frontTireMaxLateralImpulse = 7.5;
late final Image _image;
final size = const Size(6, 10);
final scale = 10.0;
late final _renderPosition = -size.toOffset() / 2;
late final _scaledRect = (size * scale).toRect();
late final _renderRect = _renderPosition & size;
final vertices = <Vector2>[
Vector2(1.5, -5.0),
Vector2(3.0, -2.5),
Vector2(2.8, 0.5),
Vector2(1.0, 5.0),
Vector2(-1.0, 5.0),
Vector2(-2.8, 0.5),
Vector2(-3.0, -2.5),
Vector2(-1.5, -5.0),
];
@override
Future<void> onLoad() async {
super.onLoad();
lapText = TextComponent(
position: -cameraComponent.viewport.size / 2 + Vector2.all(20),
);
void updateLapText() {
lapText.text = 'Lap: ${lap.value}';
}
lap.addListener(updateLapText);
updateLapText();
cameraComponent.viewport.add(lapText);
final recorder = PictureRecorder();
final canvas = Canvas(recorder, _scaledRect);
final path = Path();
paint.color = colors[playerNumber];
for (var i = 0.0; i < _scaledRect.width / 4; i++) {
paint.color = paint.color.darken(0.1);
path.reset();
final offsetVertices = vertices
.map(
(v) =>
v.toOffset() * scale -
Offset(i * v.x.sign, i * v.y.sign) +
_scaledRect.bottomRight / 2,
)
.toList();
path.addPolygon(offsetVertices, true);
canvas.drawPath(path, paint);
}
final picture = recorder.endRecording();
_image = await picture.toImage(
_scaledRect.width.toInt(),
_scaledRect.height.toInt(),
);
}
@override
Body createBody() {
paint..color = ColorExtension.random();
final startPosition =
Vector2.all(20) + Vector2.all(20) * playerNumber.toDouble();
final def = BodyDef()
..type = BodyType.dynamic
..position = startPosition;
final body = world.createBody(def)
..userData = this
..angularDamping = 3.0;
final shape = PolygonShape()..set(vertices);
final fixtureDef = FixtureDef(shape)
..density = 0.2
..restitution = 2.0;
body.createFixture(fixtureDef);
final jointDef = RevoluteJointDef();
jointDef.bodyA = body;
jointDef.enableLimit = true;
jointDef.lowerAngle = 0.0;
jointDef.upperAngle = 0.0;
jointDef.localAnchorB.setZero();
final tires = List.generate(4, (i) {
final isFrontTire = i <= 1;
final isLeftTire = i.isEven;
return Tire(
gameRef.pressedKeys[playerNumber],
isFrontTire ? _frontTireMaxDriveForce : _backTireMaxDriveForce,
isFrontTire ? _frontTireMaxLateralImpulse : _backTireMaxLateralImpulse,
jointDef,
isFrontTire
? Vector2(isLeftTire ? -3.0 : 3.0, 3.5)
: Vector2(isLeftTire ? -3.0 : 3.0, -4.25),
isTurnableTire: isFrontTire,
);
});
gameRef.cameraWorld.addAll(tires);
return body;
}
@override
void update(double dt) {
cameraComponent.viewfinder.position = body.position;
}
@override
void render(Canvas canvas) {
canvas.drawImageRect(
_image,
_scaledRect,
_renderRect,
paint,
);
}
}
class GroundSensor extends BodyComponent {
GroundSensor(this.position, this.size, this.isStart) : super(priority: 1);
final bool isStart;
final Vector2 position;
final Vector2 size;
late final Rect rect = size.toRect();
@override
Body createBody() {
paint.color =
(isStart ? Colors.lightGreenAccent : Colors.red).withOpacity(0.5);
final groundBody = world.createBody(
BodyDef(
position: position,
userData: this,
),
);
final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);
final fixtureDef = FixtureDef(shape, isSensor: true);
return groundBody..createFixture(fixtureDef);
}
@override
void render(Canvas canvas) {
canvas.translate(-size.x / 2, -size.y / 2);
canvas.drawRect(
rect,
paint,
);
}
}
class CarContactCallback extends ContactCallback<Car, GroundSensor> {
@override
void begin(Car car, GroundSensor groundSensor, Contact contact) {
if (groundSensor.isStart) {
if (car.passedStartControl.contains(groundSensor)) {
// If the car has driven over one start control but then backed out
// again, to be able to go over the finish line.
car.passedStartControl.clear();
print('Clearing');
}
print('Adding');
car.passedStartControl.add(groundSensor);
} else if (car.passedStartControl.length == 2) {
print('Finished');
car.lap.value++;
car.passedStartControl.clear();
}
}
}
void main() {
runApp(GameWidget(game: PadRacingGame()));
}
final List<Map<LogicalKeyboardKey, LogicalKeyboardKey>> playersKeys = [
{
LogicalKeyboardKey.arrowUp: LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown: LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft: LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight: LogicalKeyboardKey.arrowRight,
},
{
LogicalKeyboardKey.keyW: LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.keyS: LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.keyA: LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyD: LogicalKeyboardKey.arrowRight,
},
];
class PadRacingGame extends Forge2DGame with KeyboardEvents {
PadRacingGame() : super(gravity: Vector2.zero(), zoom: 1);
@override
Color backgroundColor() => Colors.grey.shade900;
static const numberOfPlayers = 2;
static Vector2 trackSize = Vector2.all(500);
late final World cameraWorld;
late final List<Map<LogicalKeyboardKey, LogicalKeyboardKey>> activeKeyMaps;
late final List<Set<LogicalKeyboardKey>> pressedKeys;
@override
Future<void> onLoad() async {
children.query();
const numberOfPlayers = PadRacingGame.numberOfPlayers;
cameraWorld = World();
await add(cameraWorld);
final viewportSize = Vector2(canvasSize.x / numberOfPlayers, canvasSize.y);
RectangleComponent viewportRimGenerator() =>
RectangleComponent(size: viewportSize, anchor: Anchor.center)
..paint.color = Colors.blue
..paint.strokeWidth = 2.0
..paint.style = PaintingStyle.stroke;
final cameras = List.generate(
numberOfPlayers,
(i) => CameraComponent(
world: cameraWorld,
viewport: FixedSizeViewport(viewportSize.x, viewportSize.y)
..position = Vector2(
(canvasSize.x / numberOfPlayers) * (i + 0.5),
canvasSize.y / 2,
)
..add(viewportRimGenerator()),
)
..viewfinder.anchor = Anchor.center
..viewfinder.zoom = 10,
);
await addAll(cameras);
cameraWorld.add(Background());
cameraWorld.add(GroundSensor(Vector2(25, 50), Vector2(50, 5), true));
cameraWorld.add(GroundSensor(Vector2(25, 70), Vector2(50, 5), true));
cameraWorld.add(GroundSensor(Vector2(52.5, 25), Vector2(5, 50), false));
cameraWorld.addAll(createWalls(trackSize));
for (var i = 0; i < numberOfPlayers; i++) {
cameraWorld.add(Car(playerNumber: i, cameraComponent: cameras[i]));
}
cameraWorld.add(Ball());
pressedKeys = List.generate(numberOfPlayers, (_) => {});
activeKeyMaps = List.generate(numberOfPlayers, (i) => playersKeys[i]);
addContactCallback(CarContactCallback());
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
if (!isLoaded) {
return KeyEventResult.ignored;
}
pressedKeys.forEach((e) => e.clear());
keysPressed.forEach((LogicalKeyboardKey key) {
activeKeyMaps.forEachIndexed((i, keyMap) {
if (keyMap.containsKey(key)) {
pressedKeys[i].add(keyMap[key]!);
}
});
});
return KeyEventResult.handled;
}
}
class Tire extends BodyComponent<PadRacingGame> {
Tire(
this.pressedKeys,
this._maxDriveForce,
this._maxLateralImpulse,
this.jointDef,
this.jointAnchor, {
this.isTurnableTire = false,
}) : super(paint: Paint()..color = Colors.grey.shade700, priority: 2);
final size = Vector2(0.5, 1.25);
late final RRect _renderRect = RRect.fromLTRBR(
-size.x,
-size.y,
size.x,
size.y,
const Radius.circular(0.3),
);
final Set<LogicalKeyboardKey> pressedKeys;
final double _maxDriveForce;
final double _maxLateralImpulse;
double _currentTraction = 1.0;
final double _maxForwardSpeed = 250.0;
final double _maxBackwardSpeed = -40.0;
final RevoluteJointDef jointDef;
late final RevoluteJoint joint;
final Vector2 jointAnchor;
final bool isTurnableTire;
final double _lockAngle = 0.6;
final double _turnSpeedPerSecond = 4;
final random = Random();
final Tween<double> noise = Tween(begin: -1, end: 1);
final ColorTween colorTween = ColorTween(
begin: Colors.brown,
end: Colors.black,
);
@override
Body createBody() {
final def = BodyDef()..type = BodyType.dynamic;
final body = world.createBody(def)..userData = this;
final polygonShape = PolygonShape();
polygonShape.setAsBoxXY(0.5, 1.25);
final fixture = body.createFixtureFromShape(polygonShape, 1.0);
fixture.userData = this;
jointDef.bodyB = body;
jointDef.localAnchorA.setFrom(jointAnchor);
world.createJoint(joint = RevoluteJoint(jointDef));
joint.setLimits(0, 0);
return body;
}
@override
void update(double dt) {
if (body.isAwake || pressedKeys.isNotEmpty) {
body.setAwake(true);
_updateTurn(dt);
_updateFriction();
_updateDrive();
if (body.linearVelocity.length2 > 100) {
gameRef.cameraWorld.add(
ParticleSystemComponent(
position: body.position,
particle: Particle.generate(
count: 8,
generator: (i) {
return AcceleratedParticle(
lifespan: 2,
speed: Vector2(
noise.transform(random.nextDouble()),
noise.transform(random.nextDouble()),
) *
i.toDouble(),
child: CircleParticle(
radius: 0.2,
paint: Paint()
..color = colorTween.transform(random.nextDouble())!,
),
);
},
),
priority: 1,
),
);
}
}
}
@override
void render(Canvas canvas) {
canvas.drawRRect(_renderRect, paint);
}
void _updateFriction() {
final impulse = _lateralVelocity
..scale(-body.mass)
..clampScalar(-_maxLateralImpulse, _maxLateralImpulse)
..scale(_currentTraction);
body.applyLinearImpulse(impulse);
body.applyAngularImpulse(
0.1 * _currentTraction * body.getInertia() * -body.angularVelocity,
);
final currentForwardNormal = _forwardVelocity;
final currentForwardSpeed = currentForwardNormal.length;
currentForwardNormal.normalize();
final dragForceMagnitude = -2 * currentForwardSpeed;
body.applyForce(
currentForwardNormal..scale(_currentTraction * dragForceMagnitude),
);
}
void _updateDrive() {
var desiredSpeed = 0.0;
if (pressedKeys.contains(LogicalKeyboardKey.arrowUp)) {
desiredSpeed = _maxForwardSpeed;
}
if (pressedKeys.contains(LogicalKeyboardKey.arrowDown)) {
desiredSpeed += _maxBackwardSpeed;
}
final currentForwardNormal = body.worldVector(Vector2(0.0, 1.0));
final currentSpeed = _forwardVelocity.dot(currentForwardNormal);
var force = 0.0;
if (desiredSpeed < currentSpeed) {
force = -_maxDriveForce;
} else if (desiredSpeed > currentSpeed) {
force = _maxDriveForce;
}
if (force.abs() > 0) {
body.applyForce(currentForwardNormal..scale(_currentTraction * force));
}
}
void _updateTurn(double dt) {
var desiredAngle = 0.0;
var desiredTorque = 0.0;
var isTurning = false;
if (pressedKeys.contains(LogicalKeyboardKey.arrowLeft)) {
desiredTorque = -15.0;
desiredAngle = -_lockAngle;
isTurning = true;
}
if (pressedKeys.contains(LogicalKeyboardKey.arrowRight)) {
desiredTorque += 15.0;
desiredAngle += _lockAngle;
isTurning = true;
}
if (isTurnableTire && isTurning) {
final turnPerTimeStep = _turnSpeedPerSecond * dt;
final angleNow = joint.jointAngle();
final angleToTurn = (desiredAngle - angleNow)
.clamp(-turnPerTimeStep, turnPerTimeStep)
.toDouble();
final angle = angleNow + angleToTurn;
joint.setLimits(angle, angle);
} else {
joint.setLimits(0, 0);
}
body.applyTorque(desiredTorque);
}
// Cached Vectors to reduce unnecessary object creation.
final Vector2 _worldLeft = Vector2(1.0, 0.0);
final Vector2 _worldUp = Vector2(0.0, -1.0);
Vector2 get _lateralVelocity {
final currentRightNormal = body.worldVector(_worldLeft);
return currentRightNormal
..scale(currentRightNormal.dot(body.linearVelocity));
}
Vector2 get _forwardVelocity {
final currentForwardNormal = body.worldVector(_worldUp);
return currentForwardNormal
..scale(currentForwardNormal.dot(body.linearVelocity));
}
}
List<Wall> createWalls(Vector2 size) {
final topCenter = Vector2(size.x / 2, 0);
final bottomCenter = Vector2(size.x / 2, size.y);
final leftCenter = Vector2(0, size.y / 2);
final rightCenter = Vector2(size.x, size.y / 2);
final filledSize = size.clone() + Vector2.all(5);
return [
Wall(topCenter, Vector2(filledSize.x, 5)),
Wall(bottomCenter, Vector2(filledSize.y, 5)),
Wall(leftCenter, Vector2(5, filledSize.y)),
Wall(rightCenter, Vector2(5, filledSize.y)),
Wall(Vector2(52.5, 240), Vector2(5, 380)),
Wall(Vector2(200, 50), Vector2(300, 5)),
Wall(Vector2(72.5, 300), Vector2(5, 400)),
Wall(Vector2(180, 100), Vector2(220, 5)),
Wall(Vector2(350, 105), Vector2(5, 115)),
Wall(Vector2(350, 312.5), Vector2(5, 180)),
Wall(Vector2(310, 160), Vector2(240, 5)),
Wall(Vector2(210, 400), Vector2(280, 5)),
Wall(Vector2(430, 302.5), Vector2(5, 290)),
Wall(Vector2(292.5, 450), Vector2(280, 5)),
];
}
class Wall extends BodyComponent<PadRacingGame> {
Wall(this.position, this.size) : super(priority: 3);
final Vector2 position;
final Vector2 size;
late final sizeRect = size.toRect();
final Random rng = Random();
late final Image _image;
@override
Future<void> onLoad() async {
await super.onLoad();
paint..color = Colors.green;
final recorder = PictureRecorder();
final canvas = Canvas(recorder, size.toRect());
for (var x = 0.0; x < size.x; x += 0.2) {
for (var y = 0.0; y < size.y; y += 0.2) {
paint..color = paint.color.darken(rng.nextDouble() / 20);
paint..color = paint.color.brighten(rng.nextDouble() / 20);
canvas.drawCircle(Offset(x, y), 0.2, paint);
}
}
final picture = recorder.endRecording();
_image = await picture.toImage(size.x.toInt(), size.y.toInt());
}
@override
void render(Canvas canvas) {
canvas.translate(-size.x / 2, -size.y / 2);
canvas.drawImageRect(
_image,
sizeRect,
sizeRect,
//position.toPositionedRect(size),
paint,
);
}
@override
Body createBody() {
final def = BodyDef()
..type = BodyType.static
..position = position;
final body = world.createBody(def)
..userData = this
..angularDamping = 3.0;
final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);
final fixtureDef = FixtureDef(shape)..restitution = 0.5;
return body..createFixture(fixtureDef);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment