初探Flutter跨端遊戲開發

語言: CN / TW / HK

本文作者為奇舞團大前端CodeFarmer

背景

筆者在公司前前後後做了有小一年Flutter 開發,從入門到後面業務方變動,到暫時放棄Flutter。對於Flutter爭議不提,我們得承認Flutter 是一款很優秀的跨端解決方案,到前段時間的Flutter3.0的提出,3.0對遊戲做了很友好的支援,筆者又重新開始以遊戲為切入點 去上手Flutter。所以我們探索一下Flutter3.0 對於遊戲的支援力度,是否可以低成本寫出一個自己的小遊戲呢?

Why?為什麼要做Flutter遊戲開發?

  • 一套程式碼多端執行(Flutter 特性):可以想想開發一款遊戲,既能爭安卓市場,蘋果市場還能掙Web市場的錢,是不是很好?

  • 比較流暢的仿原生環境,與純原生環境相比流暢度無太大的降低;

  • 遊戲很掙錢,apple store 收入70%來自遊戲;

  • Flutter 3.0 新出了對廣告、應用內購買、Firebase、Play 服務和遊戲中心等服務的預構建整合加快遊戲開發;(方便釋出遊戲,3.0對遊戲支援很友好)。

  • Flutter 側重2d遊戲,3D遊戲 參考其他技術,如 unity3d

Flutter3.0環境準備

  • 以Mac 電腦為例,去準備Flutter 環境

  • Flutter 3.0 SDK 下載

  1. 下載以下安裝包以獲取 Flutter SDK 的最新穩定版本:

    Intel晶片 M1 晶片
    flutter_macos_3.0.1-stable.zip flutter_macos_arm64_3.0.1-stable.zip
  2. 解壓SDK

     cd ~/development
     unzip ~/Downloads/flutter_macos_arm64_3.0.1-stable.zip
    
  3. 新增環境變數:(關於Mac 環境變數 不累述:參考)

    export PATH="$PATH:`pwd`/flutter/bin"
    open ~/.bash_profile
    source  ~/.bash_profile
    
      - 檢視Flutter環境完整性: flutter doctor
      flutter doctor
    

環境常見問題

  1. 問題1:CocoaPods環境依賴安裝cocoapods

    sudo gem install cocoapods
    Error: To set up CocoaPods for ARM macOS, run:
      arch -x86_64 sudo gem install ffi
      
    
    arch -x86_64 sudo gem install ffi、
    Building native extensions. This could take a while...
    Successfully installed ffi-1.15.5
    Parsing documentation for ffi-1.15.5
    Done installing documentation for ffi after 3 seconds
    1 gem installed 
    
  2. 問題2:Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

    flutter doctor --android-licenses
    

完整環境如下:非必須,缺失部分環境不影響開發

    [✓] Flutter (Channel stable, 3.0.1, on macOS 12.4 21F79 darwin-arm, locale
        zh-Hans-CN)
    [✓] Android toolchain - develop for Android devices (Android SDK version
        32.1.0-rc1)
    [✓] Xcode - develop for iOS and macOS (Xcode 13.4)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2021.2)
    [✓] IntelliJ IDEA Ultimate Edition (version 2020.3.2)
    [✓] VS Code (version 1.67.2)
    [✓] Connected device (4 available)
    [✓] HTTP Host Availability

    • No issues found!

第一個遊戲模板

這個 Flutter 示例遊戲 repo 預先集成了應用內購買、移動廣告 SDK 和許多其他成功遊戲的模組;

    cd flutterdemo 
    git clone https://github.com/flutter/samples.git

Flutter 中的入門遊戲,具有移動端(iOS 和 Android)遊戲的所有到釋出基本整合,包括以下功能:

  • 聲音

  • 音樂

  • 主選單畫面

  • 設定

  • 廣告 (AdMob)

  • 應用內購買

  • 遊戲服務(遊戲中心和 Google Play 遊戲服務)

  • 崩潰報告 (Firebase Crashlytics)

lib
├── src
│   ├── ads//廣告
│   ├── app_lifecycle//生命週期
│   ├── audio//音訊
│   ├── crashlytics//崩潰日誌
│   ├── game_internals//
│   ├── games_services//遊戲服務
│   ├── in_app_purchase//應用內購買
│   ├── level_selection//等級
│   ├── main_menu//menu
│   ├── play_session//
│   ├── player_progress//使用者進度
│   ├── settings//設定
│   ├── style
│   └── win_game//勝利
├── ...
└── main.dart
cd samples/game_template/
flutter run
Multiple devices found:
[1]: ANA AN00 (NAB5T20525007949)
[2]: iPhone 12 (159CF48A-D131-4187-9E51-391759D8ADC8)
[3]: macOS (macos)
[4]: Chrome (chrome)
Warning: CocoaPods not installed. Skipping pod install.
  CocoaPods is used to retrieve the iOS and macOS platform side's plugin code
  that responds to your plugin usage on the Dart side.
  Without CocoaPods, plugins will not work on iOS or macOS.
  For more info, see https://flutter.dev/platform-plugins
To install see
https://guides.cocoapods.org/using/getting-started.html#installation for
instructions.

啟動工程

flutter clean
flutter pub get
flutter run

通過以上模板,我們發現關鍵引入資訊如下

  games_services: ^2.0.7  # 成就和排行榜
  google_mobile_ads: ^1.1.0  # 廣告
  in_app_purchase: ^3.0.1  # 應用內購買

廣告id切換: ios/Runner/Info.plist android/app/src/main/AndroidManifest.xml

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-1234567890123456~0987654321</string>

<meta-data
   android:name="com.google.android.gms.ads.APPLICATION_ID"
   android:value="ca-app-pub-1234567890123456~1234567890"/>

games_services

  • 要啟用遊戲服務,請先在 iOS 上設定 Game Center ,在 Android 上設定 Google Play 遊戲服務

登入

讓使用者登入遊戲中心 (iOS) 或 Google play 遊戲服務 (Android)。在進行任何操作(例如傳送分數或解鎖成就)之前,應該先登入。

 GamesServices.signIn();

判斷登入

檢查當前使用者是否登入遊戲服務(ios遊戲中心或者Google play 遊戲服務)

GamesServices.isSignedIn;

登出

讓使用者退出 ios遊戲中心/Goole Play 服務。呼叫後,將無法再對該使用者的帳戶進行任何操作。

 GamesServices.signOut();

顯示成就介面

GamesServices.showAchievements();

顯示排行榜-入參需要ios_leaderboard_id和android_leaderboard_id

 GamesServices.showLeaderboards(iOSLeaderboardID: 'ios_leaderboard_id', androidLeaderboardID: 'android_leaderboard_id');

提交分數

提交分數到排行榜

/**
入參需要android_leaderboard_id和ios_leaderboard_id
*/
GamesServices.submitScore(score: Score(androidLeaderboardID: 'android_leaderboard_id',
                                       iOSLeaderboardID: 'ios_leaderboard_id',
                                       value: 5));

解鎖成就

/**
android_id
ios_id
percentComplete` 成就的完成百分比,這個引數在iOS的情況下是可選的
`steps` Android 的成就步驟
*/
GamesServices.unlock(achievement: Achievement(androidID: 'android_id',
                                              iOSID: 'ios_id',
                                              percentComplete: 100,
                                              steps: 2)); 

增加步驟 (Android Only)

增加安卓成就的步驟

final result = await GamesServices.increment(achievement: Achievement(androidID: 'android_id', steps: 50));
print(result);

顯示接入點 (iOS Only)

GamesServices.showAccessPoint(AccessPointLocation.topLeading);

隱藏接入點 (iOS Only)

GamesServices.hideAccessPoint();

獲取Player id

final playerID = GamesServices.getPlayerID();

獲取Player name

final playerName = GamesServices.getPlayerName();

小結

以上介紹了Flutter3.0和3.0對遊戲友好的支援,可以方便的打通移動端,方便的接入廣告等服務,可以讓開發者更專注遊戲本身開發,而非 廣告、音訊控制、使用者排名,應用支付等,下面我們介紹一下Flutter 遊戲的核心,常用的遊戲引擎和使用。

遊戲引擎

Flame engine:https://github.com/flame-engine/flame/blob/main/i18n/README-ZH.md

Flame 引擎的目的是為使用 Flutter 開發的遊戲會遇到的常見問題提供一套完整的解決方案,Flame 利用了 Flutter 的強大功能,並提供了一種輕量級的方法來為所有平臺開發 2-D 遊戲。

目前 Flame 提供了以下功能:

  • 遊戲迴圈 (game loop)

  • 元件/物件系統 (FCS)

  • 特效與粒子效果

  • 碰撞檢測

  • 手勢和輸入支援

  • 圖片、動畫、精靈 (sprite) 以及精靈組

  • 一些簡化開發的實用工具類

除了以上的功能以外,你可以使用一些橋接 Flame 的 package 來增強引擎本身的功能。 通過這些橋接 package,你可以訪問 Flame 的元件、幫助程式, 或是與其他 package 進行繫結,從而達到平滑整合的效果。 目前我們有以下的橋接 package(Flame 引擎是模組化的,允許使用者選擇他們想要使用的 API):

  • Flame – 核心包,提供遊戲迴圈、基本碰撞檢測、Sprite 和元件。

  • flame_audio 橋接 AudioPlayers :可同時播放多個音訊。

  • flame_bloc 橋接 Bloc :BloC 狀態管理。

  • flame_fire_atlas 橋接 FireAtlas :為遊戲建立紋理圖集。

  • flame_forge2d 橋接 Forge2D :基於 Box2D 的物理引擎,具有高階碰撞檢測的物理引擎,從 Box2D 移植到與 Flame 一起使用

  • flame_lint - 引擎的程式碼格式規則 ( analysis_options.yaml )。

  • flame_oxygen 橋接 Oxygen :輕量的實體-元件-系統 (ECS)。

    • Oxygen 是一個用 Dart 編寫的輕量級實體元件系統框架,專注於效能和易用性。 Oxygen 在設計上是不可知的,您想要使用的任何遊戲引擎都可以與 Oxygen 一起使用。

      目標 Oxygen 深受 ECSY 的啟發,因此它具有相同的設計原則。 Oxygen 的主要目標是輕巧、高效能和易於使用。 藉助 API 嘗試並幫助您充分利用 ECS 設計模式,而不會限制您構建邏輯。

  • flame_rive 橋接 Rive :建立可互動的動畫。https://rive.app/get-started/

    • RiveAnimation.asset('assets/truck.riv');
      
  • flame_svg 橋接 flutter_svg :在 Flutter 中繪製 SVG。

    • final String assetName = 'assets/image.svg';
      final Widget svg = SvgPicture.asset(
        assetName,
        semanticsLabel: 'Acme Logo'
      );
      
  • flame_tiled 橋接 Tiled :二維平面的地圖編輯器。

    • 下載

  • flame_audio– 為 Flame 遊戲新增音訊功能的模組。

2D遊戲小例子

  1. dependencies:
      flutter:
        sdk: flutter
      flame: 1.1.1
    
  2. runApp(const App());-> GameWidget(game)& 一個自定義pad 佈局
    
    class Joypad extends StatefulWidget {
      //自定義pad 略 可以參考:https://pub.dev/packages/control_pad
    }
    
  1. class Player extends SpriteComponent with HasGameRef{
     @override
      Future<void> onLoad() async {
        super.onLoad();
        // TODO 1
        sprite = await gameRef.loadSprite('player/player.png');
        position = gameRef.size / 2;
      }
    }
    

    Plyaer:

  2. class MyGame extends FlameGame {
      final Player _player = Player();
    
      @override
      Future<void> onLoad() async {
        add(_player);
        // empty
      }
    }
    
  3. //Joypad(onDirectionChanged: onJoypadDirectionChanged) 方向控制pad
    void onJoypadDirectionChanged(Direction direction) {
        // TODO 2
        game.onJoypadDirectionChanged(direction);
      }
    
  4. game->player->更新方向

  5. //Player update 更新頻率為 16毫秒左右
    @override
      void update(double delta) {
        super.update(delta);
        //移動小孩
        movePlayer(delta);
        print('update 更新時間---》${(DateTime.now().microsecondsSinceEpoch - _timeNow)}');
        _timeNow = DateTime.now().microsecondsSinceEpoch;
      }
    

60hz=16毫秒重新整理

  1. 關於移動速度和方向

void movePlayer(double delta) {
    // TODO
    switch (direction) {
      case Direction.up:
        moveUp(delta);
        break;
      case Direction.down:
        moveDown(delta);
        break;
      case Direction.left:
        moveLeft(delta);
        break;
      case Direction.right:
        moveRight(delta);
        break;
      case Direction.none:
        break;
    }
  }
  final double _playerSpeed = 300.0;
 void moveUp(double delta) {
    position.add(Vector2(0, -(delta * _playerSpeed)));
  }

  void moveDown(double delta) {
    position.add(Vector2(0, delta * _playerSpeed));
  }

  void moveRight(double delta) {
    position.add(Vector2(delta * _playerSpeed, 0));
  }

  void moveLeft(double delta) {
    position.add(Vector2(-delta * _playerSpeed, 0));
  }

如果遊戲檢視的直徑為 2500×2500 畫素,則您的玩家從座標 x:1250, y:1250 的中間開始。 呼叫 moveDown 會為玩家的 Y 位置增加大約 300 畫素,使用者在向下方向握住手柄時,會導致精靈向下移動遊戲視口。

  1. 地圖

    //地圖也是精靈,所以載入方式跟精靈一樣
    class MyMap extends SpriteComponent with HasGameRef {
      @override
      Future<void>? onLoad() async {
        sprite = await gameRef.loadSprite('player/rayworld_background.png');
        size = sprite!.originalSize;
        return super.onLoad();
      }
    }
    //地圖載入
    class MyGame extends FlameGame {
      final Player _player = Player();
      final MyMap _map = MyMap();
      @override
      Future<void> onLoad() async {
         await add(_map);//新增地圖
         add(_player);
      }
    } 
    
  2. 新增會動的精靈 player

    player_spritesheet
    Player extends SpriteAnimationComponent with HasGameRef{
      @override
      Future<void> onLoad() async {
        super.onLoad();
         _loadAnimations().then((_) => {animation = _standingAnimation});
      }
     Future<void> _loadAnimations() async {
        final spriteSheet = SpriteSheet(
          image: await gameRef.images.load('player/player_spritesheet.png'),
          srcSize: Vector2(29.0, 32.0),//1個精靈的畫素大小
        );
        _runDownAnimation =
            spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 4);
    
        _runLeftAnimation =
            spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 4);
    
        _runUpAnimation =
            spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 4);
    
        _runRightAnimation =
            spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 4);
    
        _standingAnimation =
            spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 1);
      }
      
      void movePlayer(double delta) {
        
        switch (direction) {
          case Direction.up:
            //動畫方向切換
             animation = _runUpAnimation;
            moveUp(delta);
            break;
          case Direction.down:
            animation = _runDownAnimation;
            moveDown(delta);
            break;
          case Direction.left:
             animation = _runLeftAnimation;
            moveLeft(delta);
            break;
          case Direction.right:
             animation = _runRightAnimation;
            moveRight(delta);
            break;
          case Direction.none:
            break;
        }
      }
    }
    
    

    至此我們的利用Flame 做的一個遊戲入門就結束了

    當然遊戲開發很複雜,想象力最重要!

Bonfire

Bonfire 引擎:(RPG 類)可以創造 Flutter.2D 遊戲的引擎,基於 Flame

引用&資源

FlutterGame:https://flutter.dev/games games-toolkit

文件:https://docs.flutter.dev/resources/games-toolkit

遊戲資源1:https://itch.io/game-assets

遊戲資源2:https://itch.io/ Flutter

遊戲地圖:https://pub.dev/packages/level_map)

-   E N D   -

3 6 0 W 3 C E C M A T C 3 9 L e a d e r 注和加