Posted by the Very Good Ventures team, published on the official Flutter blog on May 11
For this year's Google I/O conference, the Flutter team used Flutter and Firebase to build a classic pinball game. Here's how we brought the I/O pinball game to the Web through the Flame game engine.
Game Development Essentials
Using Flutter to create user interaction type games is a great choice, such as puzzles or word games. Flame is a 2D game engine built on Flutter that can be very useful when it comes to games that use game loops. The I/O pinball game uses a series of features provided by Flame, such as animation, physics engine, collision detection, etc., and also uses the infrastructure of the Flutter framework. If you can build apps with Flutter, you have the foundations Flame needs to build games.
game loop
Typically the app screen remains visually still when there are no user interaction events. In games it's the opposite - the UI is constantly being rendered and the game state is constantly changing. Flame provides a game widget that internally manages a game loop so rendering is constant and efficient. Game
The class contains the game components and the implementation of their logic, and is then passed to GameWidget
in the widget tree. In an I/O pinball game, the game loop reflects the position and state of the pinball on the playing field, and then needs to give the necessary feedback if the ball collides with an object or falls out of play.
@override
void update(double dt) {
super.update(dt);
final direction = -parent.body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) *
parent.body.fixtures.first.shape.radius;
}
Render 3D space using 2D components
When doing I/O pinball games, one of the challenges is how to use 2D elements to render a 3D interactive experience. Components need to know the order in which they are rendered on the screen. For example, when the ball is launched onto a slope, its order goes forward, which makes it appear to appear at the top of the slope.
Elements like the pinball, ejection piston, flapper, and the Chrome Dinosaur are all movable, which means it should follow the rules of real-world physics. And the pinball also needs to change its size according to its position on the board. As the pinball rolls to the top, it should get smaller and smaller to make it look further away from the user. In addition, gravity causes the pinball to adjust its angle, allowing it to fall faster down the slope.
/// Scales the ball's body and sprite according to its position on the board.
class BallScalingBehavior extends Component with ParentIsA<Ball> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final standardizedYPosition = parent.body.position.y + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
final ballSprite = parent.descendants().whereType<SpriteComponent>();
if (ballSprite.isNotEmpty) {
ballSprite.single.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}
}
Forge 2D's physics engine
The I/O pinball game relies heavily on the forge2d package maintained by the Flame team. This package ports the open source Box2D physics engine to Dart so that it can be easily integrated into Flutter. We use forge2d
to enhance in-game physics, such as collision detection between objects (clamps) on the playing field. Using forge2D
allows us to monitor when the splint collides. We can add interactive callbacks to the splint here, and we can be notified when two objects collide. For example, when a pinball (which is round) comes into contact with a spring (which is oval), we increase its score. In these callbacks, we can explicitly set where the contact starts and ends so that when two objects touch each other, a collision occurs.
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final bodyDef = BodyDef(
position: initialPosition,
type: BodyType.dynamic,
userData: this,
);
return world.createBody(bodyDef)
..createFixtureFromShape(shape, 1);
}
Sprite sheet animation
There are little things in the pinball field, like Android, Dash (Dart mascot), Sparky (Firebase mascot), and the little Chrome dinosaur, all animated. For these things, we used sprite sheets, which are already included in the Flame engine, called SpriteAnimationComponent
. For each element, we have a file with images in different orientations, the number of frames in the file, and the time between frames. Using this data, Flame SpriteAnimationComponent
was able to weave all the images together in a loop, making the elements appear to be moving.
△ Sprite sheet example
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
Next, analyze the I/O pinball game code in detail.
Live points leaderboards from Firebase
The I/O Pinball Leaderboard shows the top scores of players around the world in real time, and players can also share their scores on Twitter and Facebook. We use Firebase Cloud Firestore to record the top ten scores and display them on the leaderboard. When a new score is added to the leaderboard, a Cloud Function sorts the scores in descending order and removes scores that are not currently in the top ten.
/// Acquires top 10 [LeaderboardEntryData]s.
Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async {
try {
final querySnapshot = await _firebaseFirestore
.collection(_leaderboardCollectionName)
.orderBy(_scoreFieldName, descending: true)
.limit(_leaderboardLimit)
.get();
final documents = querySnapshot.docs;
return documents.toLeaderboard();
} on LeaderboardDeserializationException {
rethrow;
} on Exception catch (error, stackTrace) {
throw FetchTop10LeaderboardException(error, stackTrace);
}
}
Build web applications
It's easier to make responsive games than traditional apps. The pinball game just needs to be scaled according to the size of the device. For the I/O pinball game, we scale based on a fixed ratio of device size. This ensures that the coordinate system is always the same regardless of the display size. It is important to ensure consistent display and interaction of components across different devices.
The I/O pinball game also works with mobile and desktop browsers. On mobile browsers, users can click the start button to start playback, or click the left and right sides of the screen to control the corresponding bezels. On desktop browsers, users can use the keyboard to launch pinballs and control paddles.
Code Architecture
Pinball code follows a layered architecture, with each function in its own folder. In this project, the game logic is also separated from the visual components. Allows us to easily update visual elements independent of game logic. The theme of the pinball game depends on the character the player chooses before the game starts. Themes are controlled by the CharacterThemeCubit
class. Depending on the character's selection, the ball's color, background, and other elements are updated.
/// {@template character_theme}
/// Base class for creating character themes.
///
/// Character specific game components should have a getter specified here to
/// load their corresponding assets for the game.
/// {@endtemplate}
abstract class CharacterTheme extends Equatable {
/// {@macro character_theme}
const CharacterTheme();
/// Name of character.
String get name;
/// Asset for the ball.
AssetGenImage get ball;
/// Asset for the background.
AssetGenImage get background;
/// Icon asset.
AssetGenImage get icon;
/// Icon asset for the leaderboard.
AssetGenImage get leaderboardIcon;
/// Asset for the the idle character animation.
AssetGenImage get animation;
@override
List<Object> get props => [
name,
ball,
background,
icon,
leaderboardIcon,
animation,
];
}
The game state of I/O Pinball is handled with the flam_bloc package, a package that combines bloc and Flame components. For example, we use flame_bloc
to record the number of game rounds remaining, the rewards obtained in the game, and the current game score. Also, at the top level of the wdget tree is a widget that contains the logic for loading the page and instructions for playing the game. We also follow a behavioral pattern to encapsulate and isolate component-based game functionality elements. For example, bumpers make a sound when hit by a ball, so we implemented the BumperNoiseBehavior
class to handle this.
class BumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballPlayer>().play(PinballAudio.bumper);
}
}
The codebase also contains comprehensive unit tests, component tests, and golden tests. Testing games presents some challenges because a component can have multiple responsibilities, making them difficult to test individually. Ultimately we defined better patterns for isolating and testing components, and incorporated their improvements into the flame_test package.
Component Sandbox
This project relies heavily on the simulated pinball experience brought by the Flame component. The code comes with a component sandbox, which is similar to the UI component library shown in the Flutter Gallery. This is a useful tool when developing games, as it provides each game component independently to ensure they look and behave as expected before integrating them into the game.
next
You are invited to try and get high scores at I/O Pinball ! Follow the leaderboards and share your scores on social media, or visit and learn the project's open source code on the GitHub repo .
Original link :
https://medium.com/flutter/io-pinball-powered-by-flutter-and-firebase-d22423f3f5d
Localization : CFUG Team
Chinese link : https://flutter.cn/posts/io-pinball
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。