@@ -3,8 +3,8 @@ import 'dart:async';
33import 'dart:html' as html;
44// ignore: deprecated_member_use, avoid_web_libraries_in_flutter
55import 'dart:js' as js;
6- // ignore: deprecated_member_use, avoid_web_libraries_in_flutter
7- import 'dart:ui ' as ui ;
6+ // ignore: avoid_web_libraries_in_flutter
7+ import 'dart:ui_web ' as ui_web ;
88import 'package:flutter/foundation.dart' ;
99import 'package:gameframework/gameframework.dart' ;
1010
@@ -47,6 +47,7 @@ class UnityControllerWeb implements GameEngineController {
4747 bool _disposed = false ;
4848
4949 html.DivElement ? _container;
50+ html.CanvasElement ? _canvas;
5051 js.JsObject ? _unityInstance;
5152
5253 /// Message queue for messages sent before Unity is ready.
@@ -68,46 +69,52 @@ class UnityControllerWeb implements GameEngineController {
6869 }
6970
7071 void _setupContainer () {
72+ _canvas = html.CanvasElement ()
73+ ..id = '${_containerId }-canvas'
74+ ..style.width = '100%'
75+ ..style.height = '100%'
76+ ..style.display = 'block' ;
77+
7178 _container = html.DivElement ()
7279 ..id = _containerId
7380 ..style.width = '100%'
7481 ..style.height = '100%'
75- ..style.position = 'relative' ;
82+ ..style.position = 'relative'
83+ ..append (_canvas! );
7684 }
7785
7886 /// Register an HtmlElementView factory so Flutter Web can render the Unity container.
7987 void _registerPlatformViewFactory (int viewId) {
80- // ignore: undefined_prefixed_name, avoid_dynamic_calls
81- // ignore: deprecated_member_use
82- ui.platformViewRegistry.registerViewFactory (
88+ ui_web.platformViewRegistry.registerViewFactory (
8389 'com.xraph.gameframework/unity_$viewId ' ,
8490 (int id) => _container! ,
8591 );
8692 }
8793
8894 void _setupMessageHandler () {
89- // Register global message handler that Unity can call
90- js.context[ 'FlutterUnityReceiveMessage' ] = (
91- String target,
92- String method,
93- String data,
94- ) {
95- _handleUnityMessage (target, method, data);
96- } ;
95+ // Register global message handler that Unity can call.
96+ // Must use js.allowInterop so Dart closures are correctly wrapped for JS→Dart
97+ // parameter passing across the Wasm/JS boundary.
98+ js.context[ 'FlutterUnityReceiveMessage' ] = js. allowInterop (
99+ ( String target, String method, String data) {
100+ _handleUnityMessage (target, method, data);
101+ },
102+ ) ;
97103
98104 // Register scene load handler
99- js.context['FlutterUnitySceneLoaded' ] = (
100- String name,
101- int buildIndex,
102- ) {
103- _handleSceneLoaded (name, buildIndex);
104- };
105+ js.context['FlutterUnitySceneLoaded' ] = js.allowInterop (
106+ (String name, int buildIndex) {
107+ _handleSceneLoaded (name, buildIndex);
108+ },
109+ );
105110 }
106111
107112 void _handleUnityMessage (String target, String method, String data) {
108113 if (_disposed) return ;
109114
110115 _messageController.add (GameEngineMessage (
116+ target: target,
117+ method: method,
111118 data: data,
112119 timestamp: DateTime .now (),
113120 metadata: {
@@ -219,9 +226,12 @@ class UnityControllerWeb implements GameEngineController {
219226 _emitProgress (0.3 + (progress * 0.6 ));
220227 });
221228
222- // Call createUnityInstance with progress callback
229+ // Call createUnityInstance with the canvas element (not the div wrapper).
230+ // Unity's framework.js patches canvas.getContext (Safari WebGL2 fix) which
231+ // requires an actual <canvas> element — passing a <div> causes
232+ // "canvas.getContextSafariWebGL2Fixed is not a function".
223233 final instancePromise = createUnityInstance.apply ([
224- _container ,
234+ _canvas ,
225235 config,
226236 progressCallback,
227237 ]);
@@ -286,6 +296,27 @@ class UnityControllerWeb implements GameEngineController {
286296 }
287297
288298 /// Flush all queued messages to Unity.
299+ /// Encode a message as the JSON envelope FlutterBridge.ReceiveMessage expects.
300+ /// This matches the native iOS/Android format so MessageRouter can dispatch
301+ /// via [FlutterMethod] attributes regardless of C# method name casing.
302+ String _makeEnvelope (String target, String method, String data) {
303+ // Escape the data string for embedding in JSON.
304+ final escapedData = data
305+ .replaceAll ('\\ ' , '\\\\ ' )
306+ .replaceAll ('"' , '\\ "' )
307+ .replaceAll ('\n ' , '\\ n' )
308+ .replaceAll ('\r ' , '\\ r' )
309+ .replaceAll ('\t ' , '\\ t' );
310+ return '{"target":"$target ","method":"$method ","data":"$escapedData "}' ;
311+ }
312+
313+ /// Call unityInstance.SendMessage directly without envelope wrapping.
314+ /// Used internally for messages that are already formatted as envelopes
315+ /// (e.g. pause/resume routing to FlutterBridge.ReceiveMessage directly).
316+ void _callUnityDirect (String gameObject, String method, String data) {
317+ _unityInstance? .callMethod ('SendMessage' , [gameObject, method, data]);
318+ }
319+
289320 void _flushMessageQueue () {
290321 if (_messageQueue.isEmpty) return ;
291322
@@ -297,10 +328,12 @@ class UnityControllerWeb implements GameEngineController {
297328
298329 for (final msg in messages) {
299330 try {
331+ // Route through FlutterBridge.ReceiveMessage so MessageRouter handles
332+ // [FlutterMethod] dispatch — same path as native iOS/Android.
300333 _unityInstance? .callMethod ('SendMessage' , [
301- msg.target ,
302- msg.method ,
303- msg.data,
334+ 'FlutterBridge' ,
335+ 'ReceiveMessage' ,
336+ _makeEnvelope ( msg.target, msg.method, msg. data) ,
304337 ]);
305338 } catch (e) {
306339 debugPrint ('Failed to flush queued message ${msg .target }.${msg .method }: $e ' );
@@ -326,7 +359,15 @@ class UnityControllerWeb implements GameEngineController {
326359 }
327360
328361 try {
329- _unityInstance! .callMethod ('SendMessage' , [target, method, data]);
362+ // Route through FlutterBridge.ReceiveMessage so MessageRouter handles
363+ // [FlutterMethod] dispatch — same path as native iOS/Android.
364+ // Direct unityInstance.SendMessage(target, method, data) bypasses the
365+ // router and requires exact C# method names (case-sensitive on WebGL).
366+ _unityInstance! .callMethod ('SendMessage' , [
367+ 'FlutterBridge' ,
368+ 'ReceiveMessage' ,
369+ _makeEnvelope (target, method, data),
370+ ]);
330371 } catch (e) {
331372 throw EngineCommunicationException (
332373 'Failed to send message to Unity WebGL: $e ' ,
@@ -354,8 +395,9 @@ class UnityControllerWeb implements GameEngineController {
354395
355396 try {
356397 // Unity WebGL doesn't have direct pause API, so we send a message
357- // to the FlutterBridge in Unity which calls NativeAPI.Pause(true)
358- await sendMessage ('FlutterBridge' , 'ReceiveMessage' ,
398+ // to the FlutterBridge in Unity which calls NativeAPI.Pause(true).
399+ // Use _callUnityDirect — the JSON is already a proper envelope.
400+ _callUnityDirect ('FlutterBridge' , 'ReceiveMessage' ,
359401 '{"target":"NativeAPI","method":"Pause","data":"true"}' );
360402 _isPaused = true ;
361403
@@ -378,7 +420,7 @@ class UnityControllerWeb implements GameEngineController {
378420 if (! _isReady) return ;
379421
380422 try {
381- await sendMessage ('FlutterBridge' , 'ReceiveMessage' ,
423+ _callUnityDirect ('FlutterBridge' , 'ReceiveMessage' ,
382424 '{"target":"NativeAPI","method":"Pause","data":"false"}' );
383425 _isPaused = false ;
384426
0 commit comments