docs: Added the T-Rex example (#1602)

This commit is contained in:
Lukas Klingsbo
2022-05-08 22:07:33 +02:00
committed by GitHub
parent abb497abe4
commit f88200a1bf
14 changed files with 785 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -8,6 +8,7 @@ import 'stories/collision_detection/collision_detection.dart';
import 'stories/components/components.dart';
import 'stories/effects/effects.dart';
import 'stories/experimental/experimental.dart';
import 'stories/games/games.dart';
import 'stories/input/input.dart';
import 'stories/parallax/parallax.dart';
import 'stories/rendering/rendering.dart';
@ -23,6 +24,10 @@ void main() async {
theme: ThemeData.dark(),
);
// Some small sample games
addGameStories(dashbook);
// Feature examples
addAnimationStories(dashbook);
addCameraAndViewportStories(dashbook);
addCollisionDetectionStories(dashbook);

View File

@ -0,0 +1,26 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import '../../commons/commons.dart';
import 'trex/trex_game.dart';
void addGameStories(Dashbook dashbook) {
dashbook.storiesOf('Sample Games').add(
'T-Rex',
(_) => Container(
color: Colors.black,
margin: const EdgeInsets.all(45),
child: ClipRect(
child: GameWidget(
game: TRexGame(),
loadingBuilder: (_) => const Center(
child: Text('Loading'),
),
),
),
),
codeLink: baseLink('games/trex'),
info: TRexGame.description,
);
}

View File

@ -0,0 +1,62 @@
import 'package:flame/components.dart';
import '../random_extension.dart';
import '../trex_game.dart';
import 'cloud_manager.dart';
class Cloud extends SpriteComponent
with ParentIsA<CloudManager>, HasGameRef<TRexGame> {
Cloud({required Vector2 position})
: cloudGap = random.fromRange(
minCloudGap,
maxCloudGap,
),
super(
position: position,
size: initialSize,
);
static Vector2 initialSize = Vector2(92.0, 28.0);
static const double maxCloudGap = 400.0;
static const double minCloudGap = 100.0;
static const double maxSkyLevel = 71.0;
static const double minSkyLevel = 30.0;
final double cloudGap;
@override
Future<void> onLoad() async {
sprite = Sprite(
gameRef.spriteImage,
srcPosition: Vector2(166.0, 2.0),
srcSize: initialSize,
);
}
@override
void update(double dt) {
super.update(dt);
if (shouldRemove) {
return;
}
x -= parent.cloudSpeed.ceil() * 50 * dt;
if (!isVisible) {
removeFromParent();
}
}
bool get isVisible {
return x + width > 0;
}
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
y = ((absolutePosition.y / 2 - (maxSkyLevel - minSkyLevel)) +
random.fromRange(minSkyLevel, maxSkyLevel)) -
absolutePositionOf(absoluteTopLeftPosition).y;
}
}

View File

@ -0,0 +1,42 @@
import 'package:flame/components.dart';
import '../random_extension.dart';
import '../trex_game.dart';
import 'cloud.dart';
class CloudManager extends PositionComponent with HasGameRef<TRexGame> {
final double cloudFrequency = 0.5;
final int maxClouds = 20;
final double bgCloudSpeed = 0.2;
void addCloud() {
final cloudPosition = Vector2(
gameRef.size.x + Cloud.initialSize.x + 10,
((absolutePosition.y / 2 - (Cloud.maxSkyLevel - Cloud.minSkyLevel)) +
random.fromRange(Cloud.minSkyLevel, Cloud.maxSkyLevel)) -
absolutePosition.y,
);
add(Cloud(position: cloudPosition));
}
double get cloudSpeed => bgCloudSpeed / 1000 * gameRef.currentSpeed;
@override
void update(double dt) {
super.update(dt);
final numClouds = children.length;
if (numClouds > 0) {
final lastCloud = children.last as Cloud;
if (numClouds < maxClouds &&
(gameRef.size.x / 2 - lastCloud.x) > lastCloud.cloudGap) {
addCloud();
}
} else {
addCloud();
}
}
void reset() {
removeAll(children);
}
}

View File

@ -0,0 +1,82 @@
import 'dart:collection';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flame/components.dart';
import '../obstacle/obstacle_manager.dart';
import '../trex_game.dart';
import 'cloud_manager.dart';
class Horizon extends PositionComponent with HasGameRef<TRexGame> {
Horizon() : super();
static final Vector2 lineSize = Vector2(1200, 24);
final Queue<SpriteComponent> groundLayers = Queue();
late final CloudManager cloudManager = CloudManager();
late final ObstacleManager obstacleManager = ObstacleManager();
late final _softSprite = Sprite(
gameRef.spriteImage,
srcPosition: Vector2(2.0, 104.0),
srcSize: lineSize,
);
late final _bumpySprite = Sprite(
gameRef.spriteImage,
srcPosition: Vector2(gameRef.spriteImage.width / 2, 104.0),
srcSize: lineSize,
);
@override
Future<void> onLoad() async {
add(cloudManager);
add(obstacleManager);
}
@override
void update(double dt) {
super.update(dt);
final increment = gameRef.currentSpeed * dt;
for (final line in groundLayers) {
line.x -= increment;
}
final firstLine = groundLayers.first;
if (firstLine.x <= -firstLine.width) {
firstLine.x = groundLayers.last.x + groundLayers.last.width;
groundLayers.remove(firstLine);
groundLayers.add(firstLine);
}
}
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
final newLines = _generateLines();
groundLayers.addAll(newLines);
addAll(newLines);
y = (gameSize.y / 2) + 21.0;
}
void reset() {
cloudManager.reset();
obstacleManager.reset();
groundLayers.forEachIndexed((i, line) => line.x = i * lineSize.x);
}
List<SpriteComponent> _generateLines() {
final number =
1 + (gameRef.size.x / lineSize.x).ceil() - groundLayers.length;
final lastX = (groundLayers.lastOrNull?.x ?? 0) +
(groundLayers.lastOrNull?.width ?? 0);
return List.generate(
max(number, 0),
(i) => SpriteComponent(
sprite: (i + groundLayers.length).isEven ? _softSprite : _bumpySprite,
size: lineSize,
)..x = lastX + lineSize.x * i,
growable: false,
);
}
}

View File

@ -0,0 +1,62 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'trex_game.dart';
class GameOverPanel extends Component {
bool visible = false;
@override
Future<void> onLoad() async {
add(GameOverText());
add(GameOverRestart());
}
@override
void renderTree(Canvas canvas) {
if (visible) {
super.renderTree(canvas);
}
}
}
class GameOverText extends SpriteComponent with HasGameRef<TRexGame> {
GameOverText() : super(size: Vector2(382, 25), anchor: Anchor.center);
@override
Future<void> onLoad() async {
sprite = Sprite(
gameRef.spriteImage,
srcPosition: Vector2(955.0, 26.0),
srcSize: size,
);
}
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
x = gameSize.x / 2;
y = gameSize.y * .25;
}
}
class GameOverRestart extends SpriteComponent with HasGameRef<TRexGame> {
GameOverRestart() : super(size: Vector2(72, 64), anchor: Anchor.center);
@override
Future<void> onLoad() async {
sprite = Sprite(
gameRef.spriteImage,
srcPosition: Vector2.all(2.0),
srcSize: size,
);
}
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
x = gameSize.x / 2;
y = gameSize.y * .75;
}
}

View File

@ -0,0 +1,48 @@
import 'package:flame/components.dart';
import '../random_extension.dart';
import '../trex_game.dart';
import 'obstacle_type.dart';
class Obstacle extends SpriteComponent with HasGameRef<TRexGame> {
Obstacle({
required this.settings,
required this.groupIndex,
}) : super(size: settings.size);
final double _gapCoefficient = 0.6;
final double _maxGapCoefficient = 1.5;
bool followingObstacleCreated = false;
late double gap;
final ObstacleTypeSettings settings;
final int groupIndex;
bool get isVisible => (x + width) > 0;
@override
Future<void> onLoad() async {
sprite = settings.sprite(gameRef.spriteImage);
x = gameRef.size.x + width * groupIndex;
y = settings.y;
gap = computeGap(_gapCoefficient, gameRef.currentSpeed);
addAll(settings.generateHitboxes());
}
double computeGap(double gapCoefficient, double speed) {
final minGap =
(width * speed * settings.minGap * gapCoefficient).roundToDouble();
final maxGap = (minGap * _maxGapCoefficient).roundToDouble();
return random.fromRange(minGap, maxGap);
}
@override
void update(double dt) {
super.update(dt);
x -= gameRef.currentSpeed * dt;
if (!isVisible) {
removeFromParent();
}
}
}

View File

@ -0,0 +1,81 @@
import 'dart:collection';
import 'package:flame/components.dart';
import '../random_extension.dart';
import '../trex_game.dart';
import 'obstacle.dart';
import 'obstacle_type.dart';
class ObstacleManager extends Component with HasGameRef<TRexGame> {
ObstacleManager();
ListQueue<ObstacleType> history = ListQueue();
static const int maxObstacleDuplication = 2;
@override
void update(double dt) {
final obstacles = children.query<Obstacle>();
if (obstacles.isNotEmpty) {
final lastObstacle = children.last as Obstacle?;
if (lastObstacle != null &&
!lastObstacle.followingObstacleCreated &&
lastObstacle.isVisible &&
(lastObstacle.x + lastObstacle.width + lastObstacle.gap) <
gameRef.size.x) {
addNewObstacle();
lastObstacle.followingObstacleCreated = true;
}
} else {
addNewObstacle();
}
}
void addNewObstacle() {
final speed = gameRef.currentSpeed;
if (speed == 0) {
return;
}
var settings = random.nextBool()
? ObstacleTypeSettings.cactusSmall
: ObstacleTypeSettings.cactusLarge;
if (duplicateObstacleCheck(settings.type) || speed < settings.allowedAt) {
settings = ObstacleTypeSettings.cactusSmall;
}
final groupSize = _groupSize(settings);
for (var i = 0; i < groupSize; i++) {
add(Obstacle(settings: settings, groupIndex: i));
gameRef.score++;
}
history.addFirst(settings.type);
while (history.length > maxObstacleDuplication) {
history.removeLast();
}
}
bool duplicateObstacleCheck(ObstacleType nextType) {
var duplicateCount = 0;
for (final type in history) {
duplicateCount += type == nextType ? 1 : 0;
}
return duplicateCount >= maxObstacleDuplication;
}
void reset() {
removeAll(children);
history.clear();
}
int _groupSize(ObstacleTypeSettings settings) {
if (gameRef.currentSpeed > settings.multipleAt) {
return random.fromRange(1.0, ObstacleTypeSettings.maxGroupSize).floor();
} else {
return 1;
}
}
}

View File

@ -0,0 +1,105 @@
import 'dart:ui';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
enum ObstacleType {
cactusSmall,
cactusLarge,
}
class ObstacleTypeSettings {
const ObstacleTypeSettings._internal(
this.type, {
required this.size,
required this.y,
required this.allowedAt,
required this.multipleAt,
required this.minGap,
required this.minSpeed,
this.numFrames,
this.frameRate,
this.speedOffset,
required this.generateHitboxes,
});
final ObstacleType type;
final Vector2 size;
final double y;
final int allowedAt;
final int multipleAt;
final double minGap;
final double minSpeed;
final int? numFrames;
final double? frameRate;
final double? speedOffset;
static const maxGroupSize = 3.0;
final List<ShapeHitbox> Function() generateHitboxes;
static final cactusSmall = ObstacleTypeSettings._internal(
ObstacleType.cactusSmall,
size: Vector2(34.0, 70.0),
y: -55.0,
allowedAt: 0,
multipleAt: 1000,
minGap: 120.0,
minSpeed: 0.0,
generateHitboxes: () => <ShapeHitbox>[
RectangleHitbox(
position: Vector2(5.0, 7.0),
size: Vector2(10.0, 54.0),
),
RectangleHitbox(
position: Vector2(5.0, 7.0),
size: Vector2(12.0, 68.0),
),
RectangleHitbox(
position: Vector2(15.0, 4.0),
size: Vector2(14.0, 28.0),
),
],
);
static final cactusLarge = ObstacleTypeSettings._internal(
ObstacleType.cactusLarge,
size: Vector2(50.0, 100.0),
y: -74.0,
allowedAt: 800,
multipleAt: 1500,
minGap: 120.0,
minSpeed: 0.0,
generateHitboxes: () => <ShapeHitbox>[
RectangleHitbox(
position: Vector2(0.0, 26.0),
size: Vector2(14.0, 40.0),
),
RectangleHitbox(
position: Vector2(16.0, 0.0),
size: Vector2(14.0, 98.0),
),
RectangleHitbox(
position: Vector2(28.0, 22.0),
size: Vector2(20.0, 40.0),
),
],
);
Sprite sprite(Image spriteImage) {
switch (type) {
case ObstacleType.cactusSmall:
return Sprite(
spriteImage,
srcPosition: Vector2(446.0, 2.0),
srcSize: size,
);
case ObstacleType.cactusLarge:
return Sprite(
spriteImage,
srcPosition: Vector2(652.0, 2.0),
srcSize: size,
);
}
}
}

View File

@ -0,0 +1,130 @@
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'trex_game.dart';
enum PlayerState { crashed, jumping, running, waiting }
class Player extends SpriteAnimationGroupComponent<PlayerState>
with HasGameRef<TRexGame>, CollisionCallbacks {
Player() : super(size: Vector2(90, 88));
final double gravity = 1;
final double initialJumpVelocity = -15.0;
final double introDuration = 1500.0;
final double startXPosition = 50;
double _jumpVelocity = 0.0;
double get groundYPos {
return (gameRef.size.y / 2) - height / 2;
}
@override
Future<void> onLoad() async {
// Body hitbox
add(
RectangleHitbox.relative(
Vector2(0.7, 0.6),
position: Vector2(0, height / 3),
parentSize: size,
),
);
// Head hitbox
add(
RectangleHitbox.relative(
Vector2(0.45, 0.35),
position: Vector2(width / 2, 0),
parentSize: size,
),
);
animations = {
PlayerState.running: _getAnimation(
size: Vector2(88.0, 90.0),
frames: [Vector2(1514.0, 4.0), Vector2(1602.0, 4.0)],
stepTime: 0.2,
),
PlayerState.waiting: _getAnimation(
size: Vector2(88.0, 90.0),
frames: [Vector2(76.0, 6.0)],
),
PlayerState.jumping: _getAnimation(
size: Vector2(88.0, 90.0),
frames: [Vector2(1339.0, 6.0)],
),
PlayerState.crashed: _getAnimation(
size: Vector2(88.0, 90.0),
frames: [Vector2(1782.0, 6.0)],
),
};
current = PlayerState.waiting;
}
void jump(double speed) {
if (current == PlayerState.jumping) {
return;
}
current = PlayerState.jumping;
_jumpVelocity = initialJumpVelocity - (speed / 500);
}
void reset() {
y = groundYPos;
_jumpVelocity = 0.0;
current = PlayerState.running;
}
@override
void update(double dt) {
super.update(dt);
if (current == PlayerState.jumping) {
y += _jumpVelocity;
_jumpVelocity += gravity;
if (y > groundYPos) {
reset();
}
} else {
y = groundYPos;
}
if (gameRef.isIntro && x < startXPosition) {
x += (startXPosition / introDuration) * dt * 5000;
}
}
@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
y = groundYPos;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
gameRef.gameOver();
}
SpriteAnimation _getAnimation({
required Vector2 size,
required List<Vector2> frames,
double stepTime = double.infinity,
}) {
return SpriteAnimation.spriteList(
frames
.map(
(vector) => Sprite(
gameRef.spriteImage,
srcSize: size,
srcPosition: vector,
),
)
.toList(),
stepTime: stepTime,
);
}
}

View File

@ -0,0 +1,8 @@
import 'dart:math';
Random random = Random();
extension RandomExtension on Random {
double fromRange(double min, double max) =>
(nextDouble() * (max - min + 1)).floor() + min;
}

View File

@ -0,0 +1,133 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'background/horizon.dart';
import 'game_over.dart';
import 'player.dart';
enum GameState { playing, intro, gameOver }
class TRexGame extends FlameGame
with KeyboardEvents, TapDetector, HasCollisionDetection {
static const String description = '''
A game similar to the game in chrome that you get to play while offline.
Press space or tap/click the screen to jump, the more obstacles you manage
to survive, the more points you get.
''';
late final Image spriteImage;
@override
Color backgroundColor() => const Color(0xFFFFFFFF);
late final player = Player();
late final horizon = Horizon();
late final gameOverPanel = GameOverPanel();
late final TextComponent scoreText;
late int _score;
int get score => _score;
set score(int newScore) {
_score = newScore;
scoreText.text = 'Score:$score';
}
@override
Future<void> onLoad() async {
spriteImage = await Flame.images.load('trex.png');
add(horizon);
add(player);
add(gameOverPanel);
final textStyle = GoogleFonts.pressStart2p(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
);
final textPaint = TextPaint(style: textStyle);
add(
scoreText = TextComponent(
position: Vector2(20, 20),
textRenderer: textPaint,
)..positionType = PositionType.viewport,
);
score = 0;
}
GameState state = GameState.intro;
double currentSpeed = 0.0;
double timePlaying = 0.0;
final double acceleration = 10;
final double maxSpeed = 2500.0;
final double startSpeed = 600;
bool get isPlaying => state == GameState.playing;
bool get isGameOver => state == GameState.gameOver;
bool get isIntro => state == GameState.intro;
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (keysPressed.contains(LogicalKeyboardKey.enter) ||
keysPressed.contains(LogicalKeyboardKey.space)) {
onAction();
}
return KeyEventResult.handled;
}
@override
void onTapDown(TapDownInfo info) {
onAction();
}
void onAction() {
if (isGameOver || isIntro) {
restart();
return;
}
player.jump(currentSpeed);
}
void gameOver() {
gameOverPanel.visible = true;
state = GameState.gameOver;
player.current = PlayerState.crashed;
currentSpeed = 0.0;
}
void restart() {
state = GameState.playing;
player.reset();
horizon.reset();
currentSpeed = startSpeed;
gameOverPanel.visible = false;
timePlaying = 0.0;
score = 0;
}
@override
void update(double dt) {
super.update(dt);
if (isGameOver) {
return;
}
if (isPlaying) {
timePlaying += dt;
if (currentSpeed < maxSpeed) {
currentSpeed += acceleration * dt;
}
}
}
}

View File

@ -14,6 +14,7 @@ dependencies:
flame_svg: ^1.2.0
flame_forge2d: ^0.11.0
dashbook: 0.1.6
google_fonts: 2.2.0
flutter:
sdk: flutter