はじめに
こんにちは。medibaでモバイルアプリエンジニアをしている@m-itakura-medibaです。
本記事は mediba Advent Calender 2023 の11日目の記事です。
私はモバイルアプリ開発以外では、前職以前にスマホ向けブラウザゲームの開発を行っていたことがあります。
せっかくブラウザ向けに作っていたゲームをネイティブアプリ化できないかということで、今回Flutterを使ってネイティブアプリ化する方法を調べて試してみました。
どうやってネイティブアプリ化するか?
「ブラウザゲームをネイティブアプリ化する」というのは要するにWebViewでゲームを表示するということですが、大まかに分けて以下の2種類の手段があると思います。
- どこかのサーバーにホスティングして、ネイティブアプリ内のWebViewで表示する
- ネイティブアプリ内にローカルサーバーを立てて、WebViewで表示する
今回は後者の「ネイティブアプリ内にローカルサーバーを立てて、WebViewで表示する」でやってみたいと思います。
またAndroidエミュレータ/ iOSシミュレータで実行するまでを今回のゴールとします。
環境
Flutter 3.13.8
手順
その1:スマホ向けブラウザゲームを用意
まずスマートフォンのブラウザで動作するゲームを用意してください。
今回は以下で公開されているサンプルゲームを利用してみることにしたいと思います。
上記のサンプルゲームはPCブラウザ向けの作りになっているため、キャンバスのサイズを変更したり、タップでジャンプするようにしたりと、スマートフォンブラウザで動作させやすいように変更した上で利用することにしました。
なお、ゲームのファイルは以下のようなものになります。
ゲームエンジン Phaser について
このサンプルゲームは Phaser CE というゲームエンジンが使われています。Phaserは主に2Dゲーム向けの軽量なゲームエンジンです。日本ではあんまりですが、海外では人気のあるゲームエンジンです。CE(v2系)とv3があり、v3の方が新しいバージョン(CEとv3に互換性はありません)ですが、私はCEのほうが使い慣れているため今回もCEを使用しています。
その2:Flutterプロジェクトを作成
以下のコマンドを実行してFlutterプロジェクトを作成します。
flutter create sample_game
その3:必要なパッケージを追加
必要なパッケージを追加します。
といっても1つだけです!
flutter_inappwebview
というパッケージを以下のコマンドでインストールします。
flutter pub add flutter_inappwebview
なお執筆時点では5.8.0
がインストールされます。
flutter_inappwebviewは多機能なWebViewプラグインで、ローカルサーバーを起動する機能も搭載されています。
その4:ブラウザゲームのコード一式をFlutterプロジェクト内に配置
Flutterプロジェクトにassets
というフォルダを作成し、ブラウザゲームのファイル一式を格納します。以下のようになります。
assets
フォルダに格納したブラウザゲームのファイルをFlutterで読み込めるように、pubspec.yaml
にassets
を記載します。
flutter:
uses-material-design: true
# 以下を追記
assets:
- assets/
- assets/images/
assets/
を指定しただけだとその直下のimages
フォルダが読み込まれないため、assets/images/
も追加することがポイントです。
なお、フォルダ階層が深くて全てを記載するのが大変な場合にはasset_fill
というパッケージを使うと良いかもしれません。(私は使ったことがないため、使ってみた方はぜひ教えてください!)
その5:ゲームを再生
以下2点を実装し、Flutter製ネイティブアプリ上でゲームを再生させます。
- ローカルサーバーを起動
- WebViewを追加し、ゲームをロード
2点ともflutter_inappwebview
パッケージに含まれる機能で実装可能です。
基本的には以下のドキュメントを参考にして実装すれば大丈夫です。
ローカルサーバーを起動
flutter_inappwebview
が持つ In-App Localhost Server という機能を利用します。
InAppLocalhostServer
のインスタンスを生成し、
final InAppLocalhostServer localhostServer = InAppLocalhostServer();
main
関数で起動させます。
// ローカルのサーバーを起動する。
await localhostServer.start();
なおインスタンス生成時にポート番号を指定できますが、省略している場合は8080
が使用されます。
WebViewを追加し、ゲームをロード
あとはゲームをロードするだけです。
flutter_inappwebview
が提供するInAppWebView
というWidgetを利用します。
以下のようにInAppWebView
でゲームが配置されているURLを指定してロードします。
InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse('http://localhost:8080/assets/index.html'),
),
),
全体のコード
全体のコード例は以下のようになります。
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
final InAppLocalhostServer localhostServer = InAppLocalhostServer();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// ローカルのサーバーを起動する。
await localhostServer.start();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse('http://localhost:8080/assets/index.html'),
),
),
),
);
}
}
その6:各OS向けの対応
Android
Androidエミュレータで実行してみたところ、以下のようにERR_CLEARTEXT_NOT_PERMITTED
エラーが発生しました。
Android 9(API レベル 28)からHTTPプロトコル通信が許可されなくなったことが原因のようでした。
そのため、HTTP通信を許可するためにAndroidManifest.xml
にusesCleartextTraffic
を追加しました。
<application
android:label="sample_game"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"><!-- こちらを追加 -->
iOS
iOSシミュレータでは特に問題なく動作させることができました。
完成!
これで完成です!
Androidエミュレータで実行した様子は以下のような感じになります。
Appendix: FlutterとJavaScriptの通信
InAppWebView
はJavaScriptと通信することも、その逆を行うこともできます。
例えばゲーム終了時にインタースティシャル広告を表示させたい場合、以下のようなJavaScriptハンドラーを登録します。(ここではハンドラーの名前をendGame
としています。)
InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse('http://localhost:8080/assets/index.html'),
),
onWebViewCreated: (controller) {
// ゲーム終了
controller.addJavaScriptHandler(
handlerName: "endGame",
callback: (args) async {
// インタースティシャル広告の表示処理を書く。
},
);
},
)
実際にゲームが終了した時、ゲームのJavaScriptから以下のようにハンドラーを呼び出します。
window.flutter_inappwebview.callHandler('endGame', ...args);
まとめ
いかがだったでしょうか。
簡単にブラウザゲームをネイティブアプリ化できる、と感じていただけた方もいらっしゃるのではないでしょうか。
今回はエミュレータでの起動まででしたが、実際にAndroid / iOSアプリとしてストアにリリースするとなると、他の対応が必要になるかもしれませんが、その点はご了承ください。
最後に
現在medibaでは、メンバーを大募集しています。
募集・応募ページ
medibaってどんな会社だろう? と興味を持っていただいた方は、
カジュアル面談もやっておりますので、お気軽にお申込み頂ければと思います。