文/ Very Good Ventures 团队,5 月 11 日发表于 Flutter 官方博客

为了今年的 Google I/O 大会,Flutter 团队使用 Flutter 以及 Firebase 构建了一款经典的弹球游戏。下面将会介绍我们是如何通过 Flame 游戏引擎将 I/O 弹球游戏 带到 Web 端的。

游戏开发要点

使用 Flutter 打造用户交互类型的游戏是一个很棒的选择,例如拼图或者文字游戏这样的游戏。Flame 是一个在 Flutter 上构建的 2D 游戏引擎,当涉及到使用游戏循环的游戏时它会非常有用。I/O 弹球游戏使用了 Flame 提供的一系列特性,例如动画、物理引擎、碰撞检测等等,同时还借助了 Flutter 框架的基础架构。如果你能用 Flutter 构建应用,你就获得 Flame 构建游戏所需的基础。

游戏循环

通常来说应用屏幕在没有用户交互事件的时候都会保持视觉静止状态。游戏中则是相反的—— UI 会持续的渲染,而且游戏状态会不断变化。Flame 提供了一个 game widget,它内部管理了一个游戏循环,所以能恒定且高效地进行渲染。Game 类包含了游戏组件以及其逻辑的实现,然后被交给 widget 树中的 GameWidget。在 I/O 弹球游戏中,游戏循环反映了弹球在游戏场的位置以及状态,然后如果球与物体碰撞或跌出比赛则需要给出必要的反馈。

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

使用 2D 组件渲染 3D 空间

在做 I/O 弹球游戏的时候,其中遇到的一个挑战即是如何使用 2D 元素渲染一个 3D 的交互体验。组件需要知道在屏幕上渲染的前后顺序。例如,当小球发射到斜坡上时,它的顺序会向前,这样就会让它看起来出现在斜坡的顶部。

弹球、弹射活塞、挡板以及 Chrome 小恐龙等等这些元素都是可活动的,这意味着它应该遵循真实世界的物理规则。而且弹球也需要根据它在板子上的位置改变其大小。当弹球滚到顶部时,它应该越来越小,以让它看着离用户更远。此外,重力还会让弹球调整角度,让它能在斜坡上更快地落下。

/// 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 的物理引擎

I/O 弹球游戏很大程度依赖了 Flame 团队维护的 forge2d package。这个 package 将开源的 Box2D 物理引擎 移植到 Dart 中,以便可以轻松集成到 Flutter。我们使用 forge2d 增强游戏中的物理特性,例如物体(夹板)在游戏场上的之间的碰撞检测。使用 forge2D 能够我们监听夹板发生碰撞的时机。我们就可以在这里向夹板添加交互的回调,当两个物体发生碰撞的时候我们就能收到通知。例如,弹球(它是圆形的)与弹簧(它是椭圆形的)接触时,我们就会增加它的得分。在这些回调中,我们可以清楚地设置接触开始和结束的位置,以便当两个物体相互接触时,会发生碰撞。

@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 动画

在弹球游戏场中有一些小东西,例如 Android、Dash(Dart 吉祥物)、Sparky(Firebase 吉祥物)以及 Chrome 小恐龙,这些都是动画。对于这些东西,我们使用了 sprite sheets,它已经包含在 Flame 引擎中了,叫做 SpriteAnimationComponent。对于每个元素,我们都有一个文件,其中包含不同方向的图像、文件中的帧数以及帧之间的时间。使用这些数据,Flame 中的 SpriteAnimationComponent 能够在一个循环中将所有图像编在一起,使元素看起来在运动。

△ Sprite sheet 示例

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,
),
);

接下来详细解析 I/O 弹球游戏代码。

来自 Firebase 的实时积分排行榜

I/O 弹球排行榜实时地显示世界各地玩家的最高分数,玩家还可以在 Twitter 和 Facebook 上分享他们的分数。我们使用 Firebase Cloud Firestore 记录排名前十的分数,将其显示在排行榜上。当一个新的分数计入排行榜时,一个 Cloud Function 会将分数按降序排列并删除目前不在前十的分数。

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

构建 Web 应用

与传统应用相比,制作响应式的游戏更容易。弹球游戏只需要根据设备的大小进行缩放即可。对于 I/O 弹球游戏,我们基于固定比例的设备大小进行缩放。确保了无论在何种显示大小,坐标系统总是相同的。保证组件在不同设备之间的一致显示和交互非常重要。

I/O 弹球游戏也适配了移动和桌面浏览器。在移动浏览器上,用户可以点击启动按钮开始播放,也可以点击屏幕左右两侧来控制相应的挡板。在桌面浏览器上,用户可以使用键盘来发射弹球和控制挡板。

代码架构

弹球代码遵循分层架构,每个功能都在自己的文件夹中。在这个项目中,游戏逻辑也与视觉组件分离。让我们能独立于游戏逻辑轻松地更新视觉元素。弹球游戏的主题取决于玩家在游戏开始前选择的角色。主题是由 CharacterThemeCubit 类控制的。根据角色的选择,球的颜色、背景和其他元素都会更新。

/// {@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,
];
}

I/O 弹球的游戏状态是用 flam_bloc 这个 package 处理的,这是一个组合了 bloc 和 Flame 组件的 package。例如,我们使用 flame_bloc 来记录剩余的游戏回合数、游戏中获得的奖励以及当前的游戏分数。另外,在 wdget 树顶层有一个 widget,它包含加载页面的逻辑以及玩游戏的说明。我们还遵循 行为型模式 来封装和隔离基于组件的游戏功能元素。例如,保险杠在被球击中时会发出声音,所以我们实现了 BumperNoiseBehavior 类来处理这个问题。

class BumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballPlayer>().play(PinballAudio.bumper);
}
}

代码库还包含全面的单元测试、组件测试和黄金测试。测试游戏会带来一些挑战,因为一个组件可能具有多个职责,使得它们很难单独地进行测试。最终我们定义了更好的隔离和测试组件的模式,并将其改进整合到 flame_test 这个 package 中。

组件沙盒

这个项目高度依赖于 Flame 组件带来的仿真弹球体验。代码附带了一个组件沙盒,它类似于 Flutter Gallery 中展示的 UI 组件库。在开发游戏时,这是一个很有用的工具,因为它提供了独立的每个游戏组件以确保它们的外观和行为符合预期,然后再将它们整合到游戏中。

接下来

邀请你来 I/O Pinball 试玩并获取高分!关注积分排行榜并且在社交媒体上分享你的分数,也可以在 GitHub repo 访问并学习项目的开源代码。


原文链接:

https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d

本地化: CFUG 团队

中文链接: https://flutter.cn/posts/i-o-pinball

使用 Flutter 与 Firebase 制作 I/O 弹球游戏的更多相关文章

  1. 用 Flutter 和 Firebase 轻松构建 Web 应用

    作者 / Very Good Ventures Team 我们 (Very Good Ventures 团队) 与 Google 合作,在今年的 Google I/O 大会上推出了 照相亭互动体验 ( ...

  2. 疯狂java实战演义 弹球游戏代码

    package org.crazyit.ball; import java.awt.Image; import java.io.File; import javax.imageio.ImageIO; ...

  3. 怎样用HTML5 Canvas制作一个简单的游戏

    原文连接: How To Make A Simple HTML5 Canvas Game 自从我制作了一些HTML5游戏(例如Crypt Run)后,我收到了很多建议,要求我写一篇关于怎样利用HTML ...

  4. Pygame制作微信打飞机游戏PC版

    使用Pygame制作微信打飞机游戏PC版 转至:http://www.cnblogs.com/dukeleo/p/3339780.html   前一阵子看了一篇文章:青少年如何使用Python开始游戏 ...

  5. 如何使用纯 CSS 制作四子连珠游戏

    序言:你是否想过单纯使用 CSS 也可以制作一款游戏?甚至可以双人对决!这是一篇非常有趣的文章,作者详细讲解了使用纯 CSS 制作四子连珠游戏的思路以及使用奇淫巧技解决困难问题的方法.因为案例本身比较 ...

  6. 使用html+css+js实现弹球游戏

    使用html+css+js实现弹球游戏 效果图: 代码如下,复制即可使用: <!doctype html> <head> <style type="text/c ...

  7. (14)如何使用Cocos2d-x 3.0制作基于tilemap的游戏:第二部分

    引言 程序截图: 这篇教程是<如何使用Cocos2d-x 3.0制作基于tilemap的游戏>的第二部分.在上一个教程中,我们创建了一个简单的基于tiled地图的游戏,里面有一个忍者在沙漠 ...

  8. Python 练习项目1 弹球游戏

    这几天学习了python的基础知识,然后参考了网上的一些资料,完成了一个自己的小游戏,弹球游戏比较简单,但却具备了一些游戏的普遍特征,对于初学者是一个比较合适的锻炼的项目. 下面是效果图: 完整程序: ...

  9. [译]怎样用HTML5 Canvas制作一个简单的游戏

    这是我翻译自LostDecadeGames主页的一篇文章,原文地址:How To Make A Simple HTML5 Canvas Game. 下面是正文: 自从我制作了一些HTML5游戏(例如C ...

随机推荐

  1. js 关于setTimeout和Promise执行顺序问题

    js 关于setTimeout和Promise执行顺序问题 异步 -- Promise和setTimeout 执行顺序   Promise 和 setTimeout 到底谁先执行 定时器的介绍 Jav ...

  2. 深入解析丨母婴App如何迅速收割2W新用户?

    在讲案例前,我们需要先说一下精细化分析. 我们常说的精细化分析,就是一个持续"解构"的过程,通过像漏斗.留存.细分等高级分析功能,将"整体"按照事件属性解构成& ...

  3. jsp+servlet上传excel并将数据导入到数据库表的实现方法

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...

  4. uni-app中实现左侧导航栏效果

    HTML: 1 <view class="list"> 2 <!-- 一级 --> 3 <scroll-view class="list-l ...

  5. 02 | 自己动手,实现C++的智能指针

    第一步:针对单独类型的模板 为了完成智能指针首先第一步的想法. class shape_wrapper { public: explicit shape_wrapper( shape* ptr = n ...

  6. RTSP可用网络流

    推荐个音视频学习的网站 https://www.zzsin.com,里面有音视频学习资料和测试视频提供下载B站搜乔红 rtsp://wowzaec2demo.streamlock.net/vod/mp ...

  7. Cocos Creator绕远做圆周运动,且变换运动物体的角度

    需求:绕远做圆周运动 并且精灵的角度要随着位置的改变而改变 网上有很多做圆周运动的代码,但是要不然就是角度不变 要不然就是cocos版本老旧 摘了一段3.x的圆周运动,自己加了角度变换 圆周运动,已知 ...

  8. pt-osc又又出现死锁了

    今天使用pt-osc修改mysql表结构,又出现死锁了,老大让尽量解决这个问题,我们先分析一下pt-osc容易出现死锁的原因,再来解决这个问题. 根据pt-osc打印的日志,可以看到pt-osc执行原 ...

  9. 2021.08.05 P5357 康托展开模板(康托展开)

    2021.08.05 P5357 康托展开模板(康托展开) P5367 [模板]康托展开 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 重点: 1.康托展开 算法学习笔记(56): ...

  10. GO语言学习——基本数据类型字符串

    字符串 Go语言中的字符串以原生数据类型出现. Go 语言里的字符串的内部实现使用UTF-8编码. 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符 GO语 ...