Skip to content

Commit

Permalink
Merge pull request #9 from VGVentures/feat/vehicle-sim
Browse files Browse the repository at this point in the history
feat: vehicle simulation
  • Loading branch information
jolexxa authored Aug 19, 2024
2 parents f4b7cbd + 59030f4 commit 6713d52
Show file tree
Hide file tree
Showing 32 changed files with 857 additions and 98 deletions.
51 changes: 30 additions & 21 deletions lib/dashboard/game/gauge_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vehicle_cockpit/dashboard/dashboard.dart';
import 'package:vehicle_cockpit/l10n/l10n.dart';
import 'package:vehicle_sim/vehicle_sim.dart';

class GaugeGame extends FlameGame with KeyboardEvents {
GaugeGame({
required this.sim,
required this.onSpeedChanged,
required this.appTheme,
required this.l10n,
this.timeScale = 2.5,
});

/// Speeds vehicle simulation up by this factor to adjust fun factor.
final double timeScale;
final VehicleSim sim;
final ThemeData appTheme;
final AppLocalizations l10n;
final ValueChanged<double> onSpeedChanged;
Expand All @@ -24,24 +30,29 @@ class GaugeGame extends FlameGame with KeyboardEvents {
@override
Color backgroundColor() => Colors.transparent;

void accelerate() => hittingGas = true;
void acceleratorPedalPushed() => hittingGas = true;

void release() => hittingGas = false;
void acceleratorPedalReleased() => hittingGas = false;

@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final isKeyDown = event is KeyDownEvent || event is KeyRepeatEvent;
final isPressed = event is KeyDownEvent || event is KeyRepeatEvent;
final wasReleased = event is KeyUpEvent;

final isSpace = keysPressed.contains(LogicalKeyboardKey.space);
if (event.logicalKey != LogicalKeyboardKey.space ||
(!isPressed && !wasReleased)) {
return KeyEventResult.ignored;
}

if (isSpace && isKeyDown) {
accelerate();
if (isPressed) {
acceleratorPedalPushed();
} else {
release();
acceleratorPedalReleased();
}

return KeyEventResult.handled;
}

Expand All @@ -51,8 +62,8 @@ class GaugeGame extends FlameGame with KeyboardEvents {
gauge = GaugeComponent(
size: Vector2.all(340),
position: size / 2,
maxRpm: 9,
dangerZone: 8,
maxRpm: (sim.vehicle.engineRpmMaximum / 1000).round(),
dangerZone: (sim.vehicle.engineRpmRedline / 1000).round(),
appTheme: appTheme,
),
);
Expand All @@ -74,18 +85,16 @@ class GaugeGame extends FlameGame with KeyboardEvents {

@override
void update(double dt) {
var rpm = gauge.progress;
var speed = speedometer.speed;
if (hittingGas) {
rpm += .008;
speed += rpm / 8;
} else {
rpm -= .008;
speed -= (speed / 500) + .02;
}
onSpeedChanged(speed);
gauge.progress = rpm;
speedometer.speed = speed;
sim.simulate(dt * timeScale, hittingGas ? 1.0 : -1.3);

gauge.setProgress(
sim.engineRpm / sim.vehicle.engineRpmMaximum,
dt,
);
speedometer.speed = sim.speed;
gear.gearText.text = sim.gear.toString();

onSpeedChanged(sim.speed);

super.update(dt);
}
Expand Down
20 changes: 0 additions & 20 deletions lib/dashboard/game/gear.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,6 @@ class Gear extends PositionComponent with HasGameRef<GaugeGame> {
size.addListener(_buildPath);
}

@override
void update(double dt) {
final speed = gameRef.speedometer.speed;
if (speed <= 0) {
gearText.text = 'N';
} else if (speed <= 10) {
gearText.text = '1';
} else if (speed <= 15) {
gearText.text = '2';
} else if (speed <= 35) {
gearText.text = '3';
} else if (speed <= 55) {
gearText.text = '4';
} else if (speed <= 65) {
gearText.text = '5';
} else {
gearText.text = '6';
}
}

void _buildPath() {
final dimensions = size.toRect();
const curveOffset = 15.0;
Expand Down
16 changes: 10 additions & 6 deletions lib/dashboard/game/rpm_gauge/gauge_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:vehicle_cockpit/dashboard/dashboard.dart';
import 'package:vehicle_cockpit/util/math_utils.dart';

class GaugeComponent extends PositionComponent {
GaugeComponent({
Expand All @@ -18,19 +19,22 @@ class GaugeComponent extends PositionComponent {
final int dangerZone;
final ThemeData appTheme;
double _progress = 0;
double _targetProgress = 0;

final _rng = math.Random();

double get progress => _progress;

set progress(double value) {
if (value < 0) {
_progress = 0;
void setProgress(double value, double delta) {
var current = value;
if (current < 0) {
current = 0;
} else if (value >= 1) {
_progress = 1 - _rng.nextDouble() * 0.02;
} else {
_progress = value;
current = 1 - _rng.nextDouble() * 0.02;
}

_targetProgress = current;
_progress = _progress.expDecay(_targetProgress, 16, delta);
}

@override
Expand Down
3 changes: 1 addition & 2 deletions lib/dashboard/game/speedometer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Speedometer extends TextComponent with HasGameRef<GaugeGame> {
super.position,
}) : super(
anchor: Anchor.center,
text: speed.clamp(0, 160).round().toString(),
text: speed.round().toString(),
);

double speed;
Expand Down Expand Up @@ -57,7 +57,6 @@ class Speedometer extends TextComponent with HasGameRef<GaugeGame> {

@override
void update(double dt) {
speed = speed.clamp(0, 160);
text = speed.round().toString();
_mph.position = Vector2(size.x / 2, size.y);
if (speed >= 120) {
Expand Down
10 changes: 8 additions & 2 deletions lib/dashboard/view/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:vehicle_cockpit/dashboard/dashboard.dart';
import 'package:vehicle_cockpit/l10n/l10n.dart';
import 'package:vehicle_cockpit/ui/ui.dart';
import 'package:vehicle_sim/vehicle_sim.dart';

class Dashboard extends StatefulWidget {
const Dashboard({
Expand All @@ -23,13 +24,17 @@ class DashboardState extends State<Dashboard>
late final AnimationController _controller;

void onSpeedChanged(double speed) {
if (speed <= 0) {
_controller.stop();
return;
}

// Based on the max speed of 160mph, 0.222222 miles would be the distance
// covered in 5000 milliseconds if going max speed.

const distance = 0.222222;
final timeInMilliseconds = (distance / speed) * 3600 * 1000;

if (speed <= 0) _controller.stop();

_controller
..duration = Duration(
milliseconds: timeInMilliseconds.round().abs(),
Expand Down Expand Up @@ -59,6 +64,7 @@ class DashboardState extends State<Dashboard>
final l10n = context.l10n;
final theme = Theme.of(context);
final game = GaugeGame(
sim: VehicleSim(vehicle: Vehicles.compactCrossoverSUV),
appTheme: theme,
l10n: l10n,
onSpeedChanged: onSpeedChanged,
Expand Down
4 changes: 2 additions & 2 deletions lib/dashboard/widgets/accelerator_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ class _AcceleratorButtonState extends State<AcceleratorButton> {
bool _isPressed = false;

void _accelerate(_) {
_game.accelerate();
_game.acceleratorPedalPushed();
setState(() => _isPressed = true);
}

void _release([_]) {
_game.release();
_game.acceleratorPedalReleased();
setState(() => _isPressed = false);
}

Expand Down
28 changes: 15 additions & 13 deletions lib/dashboard/widgets/lap_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,22 @@ class LapSectionState extends State<LapSection> {
}

void newLap(AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
if (stopwatch.elapsed < bestTime || bestLap == 0) {
bestTime = stopwatch.elapsed;
bestLap = currentLap;
}
currentLap++;
stopwatch.reset();
setLapTime();
widget.controller
..reset()
..forward();
});
if (status != AnimationStatus.completed) {
return;
}

setState(() {
if (stopwatch.elapsed < bestTime || bestLap == 0) {
bestTime = stopwatch.elapsed;
bestLap = currentLap;
}
currentLap++;
stopwatch.reset();
setLapTime();
widget.controller
..reset()
..forward();
});
}

String _lapTime(Duration duration) {
Expand Down
15 changes: 15 additions & 0 deletions lib/util/math_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'dart:math' as math;

extension DoubleExtensions on double {
/// Exponential decay function. Use this instead of linear interpolation since
/// it is framerate-independent!
///
/// e.g., `myValue.expDecay(toValue, decay, delta);`
///
/// Note that useful [decay] values are typically between 1 (slow) to 25
/// (fast).
///
/// Thanks, Freya! https://youtu.be/LSNQuFEDOyQ?t=2981
double expDecay(double b, double decay, double delta) =>
b + (this - b) * math.exp(-decay * delta);
}
1 change: 1 addition & 0 deletions lib/util/util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'math_utils.dart';
7 changes: 7 additions & 0 deletions packages/vehicle_sim/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
8 changes: 8 additions & 0 deletions packages/vehicle_sim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Vehicle Sim

[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]

A very simple vehicle simulation model.

[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
1 change: 1 addition & 0 deletions packages/vehicle_sim/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.6.0.0.yaml
20 changes: 20 additions & 0 deletions packages/vehicle_sim/coverage_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6713d52

Please sign in to comment.