背景
筆者在公司前前后后做了有小一年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 下載
- 下載以下安裝包以獲取 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游戲小例子
-
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
}
}
//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