一个Flutter入门游戏,包含移动(iOS和Android)游戏的所有高级功能,包括以下功能:

  • 声音
  • 音乐
  • 主菜单画面
  • settings
  • 广告(AdMob)
  • 应用内购买
  • 游戏服务(Game Center 和 Google Play Games Services)
  • 崩溃报告(Firebase Crashlytics)

入门

游戏开箱即用,无需配置。它包含主菜单、路由器、设置画面和音频等功能。在构建新游戏时,这很可能是您首先需要的一切。

当您准备好启用更高级的集成(如广告和应用内支付)时,请阅读下面的“集成”部分。

开发

在调试模式下运行应用

flutter run

这假定您已连接Android模拟器、iOS模拟器或物理设备。

将游戏开发为桌面应用程序通常很方便。例如,您可以运行flutter run -d macOS,然后在Mac上的桌面窗口中获得相同的UI。这样,您就不需要使用模拟器或连接移动设备。此模板通过禁用AdMob等桌面集成来支持桌面开发。

代码组织

代码以一种松散且浅层的功能优先的方式组织。因此,在lib/src中,您会找到adsaudiomain_menu等目录。没什么花哨的,但可用。

lib
├── src
│   ├── ads
│   ├── app_lifecycle
│   ├── audio
│   ├── crashlytics
│   ├── game_internals
│   ├── games_services
│   ├── in_app_purchase
│   ├── level_selection
│   ├── main_menu
│   ├── play_session
│   ├── player_progress
│   ├── settings
│   ├── style
│   └── win_game
├── ...
└── main.dart

状态管理方法故意采用低级方式。这样,您可以轻松地采用此项目并继续进行,而无需学习新的范例,也无需记住运行flutter pub run build_runner watch。当然,我们鼓励您使用您喜欢的任何范例、助手包或代码生成方案。

为生产版本构建

构建iOS应用(完成后打开Xcode)

flutter build ipa && open build/ios/archive/Runner.xcarchive

构建Android应用(完成后打开包含bundle的文件夹)

flutter build appbundle && open build/app/outputs/bundle/release

虽然该模板是为移动游戏设计的,但您也可以发布到Web。这对于Web演示或快速试玩可能很有用。上面的命令需要安装peanut

flutter pub global run peanut \
--web-renderer canvaskit \
--extra-args "--base-href=/name_of_your_github_repo/" \
&& git push origin --set-upstream gh-pages

上述命令的最后一行会自动将您新构建的Web游戏推送到GitHub pages,前提是您已进行相应的设置。

集成

更高级的集成默认是禁用的。例如,成就最初并未启用,因为您作为开发者需要进行设置(成就必须先存在于App Store Connect和Google Play Console中才能在代码中使用)。

本节包括有关如何启用任何给定集成的说明。

一些通用说明

  • 在开始任何更深入的集成之前,请更改游戏的包名。StackOverflow上有说明,pub.dev上的rename工具可以自动化此过程。
  • 以下指南都假定您已在Google Play Console和Apple的App Store Connect中注册了您的游戏。

广告

广告使用官方google_mobile_ads包实现,默认禁用。

// TODO: When ready, uncomment the following lines to enable integrations.

AdsController? adsController;
// if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
//   /// Prepare the google_mobile_ads plugin so that the first ad loads
//   /// faster. This can be done later or with a delay if startup
//   /// experience suffers.
//   adsController = AdsController(MobileAds.instance);
//   adsController.initialize();
// }

lib/main.dart中的AdsController代码默认情况下为null,因此模板会优雅地回退到不显示桌面广告。

您可以在lib/src/ads/中找到与广告相关的代码。

在游戏中启用广告

  1. 前往AdMob并设置一个帐户。这可能需要相当长的时间,因为您需要提供银行信息、签署合同等。

  2. AdMob中为Android和iOS分别创建两个“应用”。

  3. 获取Android应用和iOS应用的AdMob“应用ID”。您可以在“应用设置”部分找到它们。它们看起来像ca-app-pub-1234567890123456~1234567890(注意两个数字之间的波浪号)。

  4. 打开android/app/src/main/AndroidManifest.xml,找到名为com.google.android.gms.ads.APPLICATION_ID<meta-data>条目,并将值更新为您在上一步中获取的Android AdMob应用的“应用ID”。

    <meta-data
       android:name="com.google.android.gms.ads.APPLICATION_ID"
       android:value="ca-app-pub-1234567890123456~1234567890"/>
  5. 打开ios/Runner/Info.plist,找到名为GADApplicationIdentifier的条目,并将值更新为iOS AdMob应用的“应用ID”。

    <key>GADApplicationIdentifier</key>
    <string>ca-app-pub-1234567890123456~0987654321</string>
  6. 回到AdMob,为每个AdMob应用创建一个“广告单元”。这会询问广告单元的格式(横幅、插页式、激励式)。该模板已设置为横幅广告单元,因此如果您想避免修改lib/src/ads中的代码,请选择该格式。

  7. 获取Android应用和iOS应用的“广告单元ID”。您可以在“广告单元”部分找到它们。它们看起来像ca-app-pub-1234567890123456/1234567890(是的,格式与“应用ID”非常相似;请注意两个数字之间的斜杠)。

  8. 打开lib/src/ads/ads_controller.dart并更新其中“广告单元”ID的值。

    final adUnitId = defaultTargetPlatform == TargetPlatform.android
        ? 'ca-app-pub-1234567890123456/1234567890'
        : 'ca-app-pub-1234567890123456/0987654321';
  9. 取消注释lib/main.dart中与广告相关的代码,并添加以下两个导入:

    import 'dart:io';
    import 'package:google_mobile_ads/google_mobile_ads.dart';
  10. AdMob的“设置”→“测试设备”部分注册您的测试设备。

该游戏模板定义了一个示例AdMob“应用ID”和两个示例“广告单元ID”。这些允许您在不从AdMob获取真实ID的情况下测试代码,但这“功能”的文档很少,仅适用于“hello world”项目。示例ID在已发布的AdMob游戏中不起作用

如果您在任何时候感到迷茫,可以在Google AdMob的文档网站上找到完整的AdMob for Flutter教程

如果您想实现更多的AdMob格式(例如插页式广告),一个好的起点是package:google_mobile_ads中的示例。

音频

音频默认启用,已准备就绪。您可以根据自己的喜好修改lib/src/audio/中的代码。

您可以在assets/music中找到一些音乐曲目——这些音乐已获得Creative Commons Attribution(CC-BY)许可,并经许可包含在此存储库中。如果您决定将这些曲目保留在游戏中,请不要忘记感谢音乐家Mr Smith

该存储库还在assets/sfx中包含了一些音效样本。这些是公共领域(CC0)的,您几乎肯定想替换它们,因为它们只是开发人员用嘴发出的傻傻的声音的录音。

Crashlytics

Crashlytics集成默认禁用。但即使您不启用它,也可能会在lib/src/crashlytics中找到有用的代码。它收集所有日志消息和错误,以便您至少可以将它们打印到控制台。

启用后,此集成功能会强大得多。

  • 您应用的任何崩溃都会发送到Firebase Crashlytics控制台。
  • 在您的代码中任何地方抛出的任何未捕获异常都会被捕获并发送到Firebase Crashlytics控制台。
  • 这些报告中的每一项都包含以下信息:
    • 错误消息
    • 堆栈跟踪
    • 设备型号、方向、可用RAM、可用磁盘空间
    • 操作系统版本
    • 应用版本
  • 此外,在您的应用(以及您使用的包)中生成的任何日志消息都会在内存中记录,并与报告一起发送。这意味着您可以了解崩溃或异常发生之前发生了什么。
  • 此外,任何带有Level.severe或更高严重级的日志消息也会发送到Crashlytics。
  • 您可以在lib/src/crashlytics中自定义这些行为。

要启用Firebase Crashlytics,请执行以下操作:

  1. console.firebase.google.com创建一个新项目。Firebase项目可以随意命名;只需记住名称。如果您不想使用Analytics,则无需在项目中启用它。
  2. 在您的机器上安装firebase-tools
  3. 在您的机器上安装flutterfire CLI
  4. 在此项目根目录(包含pubspec.yaml的目录)下,运行以下命令:

    • flutterfire configure
      • 此命令会询问您之前创建的Firebase项目的名称以及您支持的目标平台列表。截至2022年4月,Crashlytics仅完全支持androidios
      • 该命令会使用正确的代码重写lib/firebase_options.dart
  5. 转到lib/main.dart并取消注释与Crashlytics相关的行。

现在您应该可以在console.firebase.google.com中看到崩溃、错误和严重日志消息。为了测试,请在项目中添加一个按钮,并在玩家按下时抛出您想要的任何异常。

TextButton(
  onPressed: () => throw StateError('whoa!'),
  child: Text('Test Crashlytics'),
)

游戏服务(Game Center 和 Play Games Services)

游戏服务(如成就和排行榜)由games_services包实现,默认禁用。

要启用游戏服务,请首先在iOS上设置Game Center,在Android上设置Google Play Games Services

在iOS上启用Game Center(GameKit)

  1. 在Xcode中打开您的Flutter项目(open ios/Runner.xcodeproj)。
  2. 选择根Runner项目,然后转到Signing & Capabilities选项卡。
  3. 点击+按钮添加Game Center作为一项功能。您现在可以关闭Xcode。
  4. 转到App Store Connect中的您的应用,并在Features部分设置Game Center。例如,您可能想设置一个排行榜和几个成就。记下您创建的排行榜和成就的ID。

在Android上启用Play Games Services

  1. 转到Google Play Console中的您的应用。

  2. 从导航菜单中选择Play Games ServicesSetup and managementConfiguration,然后按照说明进行操作。

    • 这需要花费大量的时间和耐心。除其他事项外,您需要在Google Cloud Console中设置OAuth同意屏幕。如果您在任何时候感到迷茫,请查阅官方Play Games Services指南
  3. 完成后,您可以开始在Play Games ServicesSetup and management中添加排行榜和成就。创建与您在iOS端相同的成就和排行榜。记下ID。

  4. 转到Play Games ServicesSetup and management → Publishing,然后点击‘Publish’。不用担心,这实际上并不会发布您的游戏。它只会发布成就和排行榜。例如,一旦排行榜以这种方式发布,就无法取消发布。

  5. 转到Play Games ServicesSetup and managementConfigurationCredentials。找到一个名为‘Get resources’的按钮。您将获得一个包含Play Games Services ID的XML文件。

    <?xml version="1.0" encoding="utf-8"?>
    <!--Google Play game services IDs. Save this file as res/values/games-ids.xml in your project.-->
    <resources>
        <!--app_id-->
        <string name="app_id" translatable="false">424242424242</string>
        <!--package_name-->
        <string name="package_name" translatable="false">dev.flutter.tictactoe</string>
        <!--achievement First win-->
        <string name="achievement_first_win" translatable="false">sOmEiDsTrInG</string>
        <!--leaderboard Highest Score-->
        <string name="leaderboard_highest_score" translatable="false">sOmEiDsTrInG</string>
    </resources>
  6. 用您在上一步中获得的XML替换android/app/src/main/res/values/games-ids.xml文件。

现在您已经设置了Game CenterPlay Games Services,并且您的成就和排行榜ID也已准备就绪,终于可以开始Dart开发了。

  1. 打开lib/src/games_services/games_services.dart并在showLeaderboard()函数中编辑排行榜ID。

    // TODO: When ready, change both these leaderboard IDs.
    iOSLeaderboardID: "some_id_from_app_store",
    androidLeaderboardID: "sOmE_iD_fRoM_gPlAy",
  2. 同一文件中的awardAchievement()函数将ID作为参数。因此,您可以从游戏中的任何位置调用它,如下所示:

    final gamesServicesController = context.read<GamesServicesController?>();
    await gamesServicesController?.awardAchievement(
        iOS: 'an_achievement_id',
        android: 'aNaChIeVeMenTiDfRoMgPlAy',
    );

    您可能想将成就ID附加到关卡、敌人、地点、物品等。例如,该模板在lib/src/level_selection/levels.dart中定义了关卡,如下所示:

    GameLevel(
      number: 1,
      difficulty: 5,
      achievementIdIOS: 'first_win',
      achievementIdAndroid: 'sOmEtHinG',
    ),

    这样,在玩家到达一个关卡后,我们会检查该关卡是否具有非空成就ID,如果是,则使用这些ID调用awardAchievement()

  3. 取消注释lib/main.dart中与游戏服务相关的代码。

    // TODO: When ready, uncomment the following lines.
    
    GamesServicesController? gamesServicesController;
    // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
    //   gamesServicesController = GamesServicesController()
    //     // Attempt to log the player in.
    //     ..initialize();
    // } 

如果您在任何时候感到迷茫,可以参考package:games_services的作者撰写的How To指南。指南中的一些说明和截图可能有点过时(例如,在文章发布后,iTunes Connect已更名为App Store Connect),但它仍然是一个很好的资源。

应用内购买

应用内购买使用官方in_app_purchase包实现。集成默认禁用。

在Android上启用应用内购买

  1. 将游戏上传到Google Play Console的Closed Testing(封闭测试)轨道。

    • 由于游戏已经依赖于package:in_app_purchase,因此它会向Play Store表明自己是具有应用内购买的项目。
    • 发布到封闭测试会触发审核流程,这是应用内购买正常工作的前提。审核流程可能需要几天时间,在此期间您无法在Android方面继续进行。
  2. Play ConsoleMonetizeIn-app products中添加一个应用内产品。想一个产品ID(例如,ad_removal)。
  3. 在Play Console中,激活该应用内产品。

在iOS上启用应用内购买

  1. 确保您已在App Store Connect中签署了Paid Apps Agreement
  2. 在App Store Connect中,转到FeaturesIn-App Purchases,然后通过点击+按钮添加一个新的应用内购买。使用您在Android端使用的相同产品ID。
  3. 按照说明获取应用内购买的批准。

现在一切都已准备好在Dart代码中启用集成。

  1. 打开lib/src/in_app_purchase/ad_removal.dart并将productId更改为您在Play Console和App Store Connect中输入的产品ID。

    /// The representation of this product on the stores.
    static const productId = 'remove_ads';
    • 如果您的应用内购买不是广告移除,则创建一个类似于模板中AdRemovalPurchase的类。
    • 如果您创建了多个应用内购买,则需要修改lib/src/in_app_purchase/in_app_purchase.dart中的代码。默认情况下,该模板仅支持一次应用内购买。
  2. 取消注释lib/main.dart中与应用内购买相关的代码。

    // TODO: When ready, uncomment the following lines.
    
    InAppPurchaseController? inAppPurchaseController;
    // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
    //   inAppPurchaseController = InAppPurchaseController(InAppPurchase.instance)
    //     // Subscribing to [InAppPurchase.instance.purchaseStream] as soon
    //     // as possible in order not to miss any updates.
    //     ..subscribe();
    //   // Ask the store what the player has bought already.
    //   inAppPurchaseController.restorePurchases();
    // }

如果您在任何时候感到迷茫,可以查看官方的为您的Flutter应用添加应用内购买 codelab。

设置

设置页面默认启用,并且可以从主菜单和游戏会话屏幕中的“齿轮”按钮访问。

设置使用package:shared_preferences保存到本地存储。要更改保存哪些首选项以及如何保存,请编辑lib/src/settings/persistence中的文件。

abstract class SettingsPersistence {
  Future<bool> getMusicOn();

  Future<bool> getMuted({required bool defaultValue});

  Future<String> getPlayerName();

  Future<bool> getSoundsOn();

  Future<void> saveMusicOn(bool value);

  Future<void> saveMuted(bool value);

  Future<void> savePlayerName(String value);

  Future<void> saveSoundsOn(bool value);
}

Icon

要更新启动图标,首先更改assets/icon-adaptive-foreground.pngassets/icon.png文件。然后,运行以下命令:

flutter pub run flutter_launcher_icons:main

您可以在pubspec.yamlflutter_icons:部分配置图标的外观。

GitHub

查看 Github