跳转至正文

为移动游戏添加成就与排行榜

如何使用 games_services 插件为你的游戏添加相关功能。

玩家玩游戏有多种动机。概括而言,主要有四种动机: 沉浸、成就、合作与竞争。无论你构建什么游戏,总有一些玩家想在游戏中 取得成就,例如赢得奖杯或解锁秘密。也有一些玩家想在游戏中 竞争,例如冲击高分或完成速通。这两种诉求对应 成就 和 排行榜 的概念。

A simple graphic representing the four types of motivation explained above

App Store 和 Google Play 等生态提供成就与排行榜的集中式服务。玩家可以在一处查看所有游戏的成就,开发者也无需为每个游戏重复实现这些功能。

本实用教程演示如何使用 games_services package 为你的移动游戏添加成就与排行榜功能。

1. 启用平台服务

#

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

iOS

#

要在 iOS 上启用 Game Center (GameKit):

  1. 在 Xcode 中打开你的 Flutter 项目。打开 ios/Runner.xcworkspace

  2. 选择根级 Runner 项目。

  3. 进入 Signing & Capabilities 标签页。

  4. 点击 + 按钮,将 Game Center 添加为 capability。

  5. 关闭 Xcode。

  6. 如果尚未注册,请在 App Store Connect 中注册你的游戏,并在 My App 部分点击 + 图标。

    Screenshot of the + button in App Store Connect

  7. 仍在 App Store Connect 中,找到 Game Center 部分(撰写本文时位于 Services)。在 Game Center 页面,可根据游戏需要设置排行榜和若干成就。记下你创建的排行榜与成就的 ID。

Android

#

要在 Android 上启用 Play Games Services:

  1. 如果尚未注册,请前往 Google Play Console 注册你的游戏。

    Screenshot of the 'Create app' button in Google Play Console

  2. 仍在 Google Play Console 中,从导航菜单选择 Play Games Services → Setup and management → Configuration,并按说明操作。

    • 这需要较多时间和耐心。除其他事项外,你还需要在 Google Cloud Console 中配置 OAuth 同意屏幕。如有困惑,请参阅官方 Play Games Services 指南。

      Screenshot showing the Games Services section in Google Play Console

  3. 完成后,可在 Play Games Services → Setup and management 中添加排行榜和成就。创建与 iOS 端完全相同的集合,并记下 ID。

  4. 前往 Play Games Services → Setup and management → Publishing。

  5. 点击 Publish。不必担心,这并不会真正发布你的游戏,只会发布成就和排行榜。例如,排行榜一旦以此方式发布,就无法取消发布。

  6. 前往 Play Games Services → Setup and management → Configuration → Credentials。

  7. 找到 Get resources 按钮。它会返回包含 Play Games Services ID 的 XML 文件。

    xml
    <!-- THIS IS JUST AN EXAMPLE -->
    <?xml version="1.0" encoding="utf-8"?>
    <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>
    
  8. 在 android/app/src/main/res/values/games-ids.xml 添加文件,内容为上一步收到的 XML。

2. 登录游戏服务

#

既然你已配置好 Game Center 和 Play Games Services,并准备好了成就与排行榜 ID,接下来就该写 Dart 了。

  1. 添加对 games_services package 的依赖。

    flutter pub add games_services
    
  2. 在做其他操作之前,你必须让玩家登录游戏服务。

    dart
    try {
      await GamesServices.signIn();
    } on PlatformException catch (e) {
      // ... deal with failures ...
    }
    

登录在后台进行,需要数秒,因此不要在 runApp() 之前调用 signIn(),否则玩家每次启动游戏都会面对空白屏幕。

调用 games_services API 可能因多种原因失败。因此,每次调用都应像上例那样用 try-catch 包裹。为清晰起见,本实用教程其余部分省略异常处理。

3. 解锁成就

#
  1. 在 Google Play Console 和 App Store Connect 中注册成就并记下 ID。现在你可以从 Dart 代码授予这些成就:

    dart
    await GamesServices.unlock(
      achievement: Achievement(
        androidID: 'your android id',
        iOSID: 'your ios id',
      ),
    );
    

    The player's account on Google Play Games or Apple Game Center now lists the achievement. 玩家在 Google Play Games 或 Apple Game Center 的账户中现在会显示该成就。

  2. 要从游戏中显示成就 UI,请调用 games_services API:

    dart
    await GamesServices.showAchievements();
    

    这会在你的游戏上以叠加层形式显示平台成就 UI。

  3. 若要在自己的 UI 中显示成就,请使用 GamesServices.loadAchievements()。

4. 提交分数

#

当玩家完成一局时,你的游戏可将该局结果提交到一个或多个排行榜。

例如,像超级马里奥这样的平台游戏可将最终得分和通关用时分别提交到两个不同的排行榜。

  1. 在第一步中,你已在 Google Play Console 和 App Store Connect 注册排行榜并记下 ID。使用该 ID,你可以为玩家提交新分数:

    dart
    await GamesServices.submitScore(
      score: Score(
        iOSLeaderboardID: 'some_id_from_app_store',
        androidLeaderboardID: 'sOmE_iD_fRoM_gPlAy',
        value: 100,
      ),
    );
    

    你无需检查新分数是否为玩家最高分,平台游戏服务会替你处理。

  2. 要在游戏上以叠加层显示排行榜,请进行以下调用:

    dart
    await GamesServices.showLeaderboards(
      iOSLeaderboardID: 'some_id_from_app_store',
      androidLeaderboardID: 'sOmE_iD_fRoM_gPlAy',
    );
    
  3. 若要在自己的 UI 中显示排行榜分数,可使用 GamesServices.loadLeaderboardScores() 获取。

5. 后续步骤

#

games_services 插件还有更多功能。借助该插件,你可以:

  • 获取玩家头像、名称或唯一 ID

  • 保存和加载游戏状态

  • 退出游戏服务

部分成就可以是递增的,例如:「你已收集齐 McGuffin 的全部 10 个碎片。」

每个游戏对游戏服务的需求各不相同。

开始时,你可以创建如下 controller,将所有成就与排行榜逻辑集中在一处:

dart
import 'dart:async';
​
import 'package:games_services/games_services.dart';
import 'package:logging/logging.dart';
​
/// Allows awarding achievements and leaderboard scores,
/// and also showing the platforms' UI overlays for achievements
/// and leaderboards.
///
/// A facade of `package:games_services`.
class GamesServicesController {
  static final Logger _log = Logger('GamesServicesController');
​
  final Completer<bool> _signedInCompleter = Completer();
​
  Future<bool> get signedIn => _signedInCompleter.future;
​
  /// Unlocks an achievement on Game Center / Play Games.
  ///
  /// You must provide the achievement ids via the [iOS] and [android]
  /// parameters.
  ///
  /// Does nothing when the game isn't signed into the underlying
  /// games service.
  Future<void> awardAchievement({
    required String iOS,
    required String android,
  }) async {
    if (!await signedIn) {
      _log.warning('Trying to award achievement when not logged in.');
      return;
    }
​
    try {
      await GamesServices.unlock(
        achievement: Achievement(androidID: android, iOSID: iOS),
      );
    } catch (e) {
      _log.severe('Cannot award achievement: $e');
    }
  }
​
  /// Signs into the underlying games service.
  Future<void> initialize() async {
    try {
      await GamesServices.signIn();
      // The API is unclear so we're checking to be sure. The above call
      // returns a String, not a boolean, and there's no documentation
      // as to whether every non-error result means we're safely signed in.
      final signedIn = await GamesServices.isSignedIn;
      _signedInCompleter.complete(signedIn);
    } catch (e) {
      _log.severe('Cannot log into GamesServices: $e');
      _signedInCompleter.complete(false);
    }
  }
​
  /// Launches the platform's UI overlay with achievements.
  Future<void> showAchievements() async {
    if (!await signedIn) {
      _log.severe('Trying to show achievements when not logged in.');
      return;
    }
​
    try {
      await GamesServices.showAchievements();
    } catch (e) {
      _log.severe('Cannot show achievements: $e');
    }
  }
​
  /// Launches the platform's UI overlay with leaderboard(s).
  Future<void> showLeaderboard() async {
    if (!await signedIn) {
      _log.severe('Trying to show leaderboard when not logged in.');
      return;
    }
​
    try {
      await GamesServices.showLeaderboards(
        // TODO: When ready, change both these leaderboard IDs.
        iOSLeaderboardID: 'some_id_from_app_store',
        androidLeaderboardID: 'sOmE_iD_fRoM_gPlAy',
      );
    } catch (e) {
      _log.severe('Cannot show leaderboard: $e');
    }
  }
​
  /// Submits [score] to the leaderboard.
  Future<void> submitLeaderboardScore(int score) async {
    if (!await signedIn) {
      _log.warning('Trying to submit leaderboard when not logged in.');
      return;
    }
​
    _log.info('Submitting $score to leaderboard.');
​
    try {
      await GamesServices.submitScore(
        score: Score(
          // TODO: When ready, change these leaderboard IDs.
          iOSLeaderboardID: 'some_id_from_app_store',
          androidLeaderboardID: 'sOmE_iD_fRoM_gPlAy',
          value: score,
        ),
      );
    } catch (e) {
      _log.severe('Cannot submit leaderboard score: $e');
    }
  }
}

更多信息

#

Flutter Casual Games Toolkit 包含以下模板:

  • basic:基础入门游戏

  • card:入门纸牌游戏

  • endless runner:入门游戏(使用 Flame),玩家无尽奔跑、躲避陷阱并获取奖励