Skip to content

Commit 05bcdf6

Browse files
committed
chore: Update platform-specific handling and improve macOS build script for better clarity and functionality
1 parent 0d84157 commit 05bcdf6

39 files changed

Lines changed: 10560 additions & 169 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ Game Engine (Unity/Unreal)
260260
|----------|--------------|-------|--------|--------|
261261
| Android | ✅ Ready | ✅ Ready | 🚧 WIP | Stable |
262262
| iOS | ✅ Ready | ✅ Ready | 🚧 WIP | Stable |
263-
| Web | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
263+
| Web | ✅ Ready | ✅ Ready | ⏳ Planned | Stable |
264264
| macOS | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
265265
| Windows | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
266266
| Linux | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |

engines/unity/dart/lib/src/unity_controller_web.dart

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import 'dart:async';
33
import 'dart:html' as html;
44
// ignore: deprecated_member_use, avoid_web_libraries_in_flutter
55
import '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;
88
import 'package:flutter/foundation.dart';
99
import '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

engines/unity/plugin/Editor/FlutterBuildScript.cs

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,8 @@ private static void ConfigureWebGLSettings(bool isDevelopment)
350350
}
351351

352352
/// <summary>
353-
/// Build for macOS - exports as Xcode project (source) only. IL2CPP is required.
354-
/// The game-cli builds the Xcode project and assembles UnityFramework.framework.
353+
/// Build for macOS - exports as Xcode project (source) when IL2CPP is available,
354+
/// or as .app bundle when using Mono. The game-cli handles both cases.
355355
/// </summary>
356356
[MenuItem("Game Framework/Build macOS")]
357357
public static void BuildMacos()
@@ -378,33 +378,49 @@ public static void BuildMacos()
378378
Directory.CreateDirectory(buildPath);
379379
}
380380

381-
// Configure macOS settings; require IL2CPP for Xcode project export
381+
// Configure macOS settings (attempts IL2CPP + Xcode project)
382382
bool isIL2CPP = ConfigureMacOSSettings();
383-
if (!isIL2CPP)
383+
384+
// When IL2CPP is active, request an Xcode project export (mirrors iOS).
385+
// With Mono, Unity only produces a .app bundle; the CLI will extract
386+
// UnityFramework from it and warn the user to switch to IL2CPP.
387+
bool xcodeProjectRequested = false;
388+
if (isIL2CPP)
384389
{
385-
Debug.LogError("macOS build requires IL2CPP scripting backend.");
386-
Debug.LogError("Install Mac Build Support (IL2CPP) via Unity Hub > Installs > your version > Add Modules.");
387-
Debug.LogError("Then set Edit > Project Settings > Player > macOS > Other Settings > Scripting Backend to IL2CPP.");
388-
EditorApplication.Exit(1);
389-
return;
390+
try
391+
{
392+
EditorUserBuildSettings.SetPlatformSettings("OSXUniversal", "CreateXcodeProject", "true");
393+
xcodeProjectRequested = true;
394+
Debug.Log("Requested Xcode project export (IL2CPP + CreateXcodeProject)");
395+
}
396+
catch (System.Exception e)
397+
{
398+
Debug.LogWarning($"SetPlatformSettings failed (Unity 6 Build Profiles may override): {e.Message}");
399+
}
390400
}
391401

392-
// Request Xcode project export (mirrors iOS; game-cli assembles UnityFramework from build products)
393-
try
402+
// Determine the output path.
403+
// For Xcode project: the build path is used as the project directory.
404+
// For .app binary: Unity expects the path to end with .app.
405+
string appName = PlayerSettings.productName;
406+
if (string.IsNullOrEmpty(appName)) appName = "UnityGame";
407+
408+
string locationPath;
409+
if (xcodeProjectRequested)
394410
{
395-
EditorUserBuildSettings.SetPlatformSettings("OSXUniversal", "CreateXcodeProject", "true");
396-
Debug.Log("Requested Xcode project export (CreateXcodeProject=true)");
411+
// Xcode project mode: Unity puts the .xcodeproj inside this directory
412+
locationPath = buildPath;
397413
}
398-
catch (System.Exception e)
414+
else
399415
{
400-
Debug.LogWarning($"SetPlatformSettings failed (Unity 6 Build Profiles may override): {e.Message}");
416+
// .app mode: Unity needs a path ending in .app
417+
locationPath = Path.Combine(buildPath, appName + ".app");
401418
}
402419

403-
// Always export to build path as Xcode project directory
404420
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions
405421
{
406422
scenes = GetScenes(),
407-
locationPathName = buildPath,
423+
locationPathName = locationPath,
408424
target = BuildTarget.StandaloneOSX,
409425
options = BuildOptions.None
410426
};
@@ -414,7 +430,9 @@ public static void BuildMacos()
414430
buildPlayerOptions.options |= BuildOptions.Development;
415431
}
416432

417-
Debug.Log($"Building to: {buildPath}");
433+
Debug.Log($"Building to: {locationPath}");
434+
Debug.Log($"Xcode project requested: {xcodeProjectRequested}");
435+
Debug.Log($"IL2CPP: {isIL2CPP}");
418436
Debug.Log($"Development: {isDevelopment}");
419437
Debug.Log($"Streaming Enabled: {streamingEnabled}");
420438

@@ -425,7 +443,7 @@ public static void BuildMacos()
425443
{
426444
Debug.Log($"macOS build succeeded: {summary.totalSize} bytes");
427445

428-
// Verify Xcode project was produced; fail if not
446+
// Verify output type
429447
bool hasXcodeProj = false;
430448
foreach (string dir in Directory.GetDirectories(buildPath))
431449
{
@@ -439,11 +457,10 @@ public static void BuildMacos()
439457

440458
if (!hasXcodeProj)
441459
{
442-
Debug.LogError("Build did not produce an Xcode project. A .app bundle was produced instead.");
443-
Debug.LogError("Enable 'Create Xcode Project' in Build Profiles (Edit > Project Settings > Build) for macOS.");
444-
Debug.LogError("Then re-run the build.");
445-
EditorApplication.Exit(1);
446-
return;
460+
Debug.LogWarning("Build produced .app instead of Xcode project.");
461+
Debug.LogWarning("This can happen when using Mono or when CreateXcodeProject is overridden by Build Profiles.");
462+
Debug.LogWarning("For best results, set IL2CPP as the scripting backend and enable 'Create Xcode Project' in Build Profiles.");
463+
Debug.Log("The game-cli will extract UnityFramework from the .app bundle as a fallback.");
447464
}
448465

449466
EditorApplication.Exit(0);

example/lib/main.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class _UnityExampleScreenState extends State<UnityExampleScreen>
153153

154154
// UI State
155155
bool _isPanelExpanded = false;
156-
bool _showMiniHud = true;
156+
final bool _showMiniHud = true;
157157
late AnimationController _panelAnimationController;
158158
late Animation<double> _panelAnimation;
159159

@@ -294,7 +294,7 @@ class _UnityExampleScreenState extends State<UnityExampleScreen>
294294
child: Row(
295295
mainAxisSize: MainAxisSize.min,
296296
children: [
297-
Icon(
297+
const Icon(
298298
Icons.speed,
299299
color: Colors.amber,
300300
size: 18,
@@ -420,7 +420,7 @@ class _UnityExampleScreenState extends State<UnityExampleScreen>
420420
padding: const EdgeInsets.symmetric(horizontal: 20),
421421
child: Row(
422422
children: [
423-
Icon(Icons.speed, color: Colors.amber, size: 20),
423+
const Icon(Icons.speed, color: Colors.amber, size: 20),
424424
const SizedBox(width: 8),
425425
Expanded(
426426
child: SliderTheme(
@@ -602,7 +602,7 @@ class _UnityExampleScreenState extends State<UnityExampleScreen>
602602
const SizedBox(width: 4),
603603
Text(
604604
label,
605-
style: TextStyle(color: Colors.white54, fontSize: 11),
605+
style: const TextStyle(color: Colors.white54, fontSize: 11),
606606
),
607607
],
608608
),

0 commit comments

Comments
 (0)