왜 WebGL + WebView인가
Unity 게임을 Flutter 앱 안에서 돌리는 방법은 여러 가지예요. flutter_unity_widget 같은 네이티브 임베딩도 있지만, 플랫폼별 빌드와 호환성 문제가 많습니다.
WebGL 빌드 + WebView 조합은 한 번 빌드하면 iOS/Android 모두에서 작동해요. 배포도 서버만 업데이트하면 되니 앱 심사 없이 게임을 갱신해요.
WebGL 빌드 + WebView 조합은 한 번 빌드하면 iOS/Android 모두에서 작동해요. 배포도 서버만 업데이트하면 되니 앱 심사 없이 게임을 갱신해요.
Unity WebGL 빌드 설정
Unity에서 WebGL 빌드 시 압축 포맷과 메모리 설정이 중요합니다. Brotli 압축이 기본인데 모바일에서는 Gzip이 더 호환성이 좋거든요.
bash
// ProjectSettings > Player > WebGL 설정
// Publishing Settings
Compression Format: Gzip // 모바일 호환성
Decompression Fallback: true
// Memory Settings
Initial Memory Size: 32 // MB, 모바일은 낮게
Maximum Memory Size: 256
Memory Growth Mode: Linear
// 빌드 커맨드 (CI용)
Unity -batchmode -nographics \
-projectPath ./MyGame \
-buildTarget WebGL \
-executeMethod BuildScript.BuildWebGL \
-quitFirebase Hosting 배포
WebGL 빌드 결과물을 Firebase Hosting에 올립니다. CDN이 기본 포함이라 로딩 속도가 빠릅니다.
json
# Firebase CLI 설치 및 초기화
npm install -g firebase-tools
firebase init hosting
# firebase.json 설정
{
"hosting": {
"public": "Build/WebGL",
"headers": [
{
"source": "**/*.@(js|wasm)",
"headers": [
{ "key": "Content-Encoding", "value": "gzip" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
]
}
]
}
}
# 배포
firebase deploy --only hostingFlutter WebView 연동
webview_flutter 패키지로 WebGL 게임을 로드합니다. 핵심은 JavaScript 활성화와 viewport 메타 태그입니다.
Dart
import 'package:webview_flutter/webview_flutter.dart';
class GameWebView extends StatefulWidget {
@override
State<GameWebView> createState() => _GameWebViewState();
}
class _GameWebViewState extends State<GameWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (_) => _injectViewport(),
))
..loadRequest(Uri.parse(
'https://my-game.web.app/index.html',
));
}
// 모바일 viewport 스케일링
void _injectViewport() {
_controller.runJavaScript('''
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.head.appendChild(meta);
''');
}
@override
Widget build(BuildContext context) {
return WebViewWidget(controller: _controller);
}
}JS Channel: Flutter ↔ Unity 통신
점수 전송, 게임 오버 이벤트 같은 데이터를 Flutter와 Unity 사이에 주고받아야 해요. JavaScript Channel이 다리 역할을 해요.
Dart
// Flutter 쪽 - JS Channel 등록
_controller
..addJavaScriptChannel(
'GameBridge',
onMessageReceived: (message) {
final data = jsonDecode(message.message);
switch (data['event']) {
case 'gameOver':
_handleGameOver(data['score'] as int);
break;
case 'purchase':
_handlePurchase(data['itemId'] as String);
break;
}
},
);JavaScript
// Unity C# 쪽 - Flutter로 메시지 전송
// jslib 플러그인 (Assets/Plugins/WebGL/bridge.jslib)
mergeInto(LibraryManager.library, {
SendToFlutter: function(msgPtr) {
var msg = UTF8ToString(msgPtr);
// Flutter의 GameBridge 채널로 전송
if (window.GameBridge) {
window.GameBridge.postMessage(msg);
}
}
});iOS WKWebView 제약사항
iOS의 WKWebView는 WebGL을 지원하지만 몇 가지 제약이 있거든요. SharedArrayBuffer가 기본 비활성이라 멀티스레드 Unity 빌드가 안 되죠.
COOP/COEP 헤더를 설정해도 WKWebView에서는 무시돼요. Unity 빌드 시 Player Settings > Threading Support를 끄세요.
COOP/COEP 헤더를 설정해도 WKWebView에서는 무시돼요. Unity 빌드 시 Player Settings > Threading Support를 끄세요.
성능 최적화 팁
WebGL은 네이티브 대비 성능이 60-70% 수준입니다. 모바일에서는 더 떨어지니 최적화가 필수입니다.
텍스처 크기를 절반으로 줄이고, Draw Call을 50 이하로 유지하세요. 프레임 예산은 30fps 기준으로 잡는 게 현실적예요.
텍스처 크기를 절반으로 줄이고, Draw Call을 50 이하로 유지하세요. 프레임 예산은 30fps 기준으로 잡는 게 현실적예요.