Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
372450e
Move Unreal Android library mode into UPL
juicycleff Sep 4, 2026
13e80a3
Implement the Unreal bridge for iOS and macOS
juicycleff Sep 4, 2026
bed10f6
Build Unreal as a linkable dylib on Apple, and fix what that surfaced
juicycleff Sep 4, 2026
30a3fa7
Drive the Unreal engine tick from the host
juicycleff Sep 4, 2026
4c04381
Render surface for iOS, and what a real device run taught us about em…
juicycleff Sep 4, 2026
4f6b113
Fix what a source engine build and a real cook exposed
juicycleff Sep 4, 2026
6a7e6dd
Unreal renders inside a host app on iOS
juicycleff Sep 5, 2026
ce806b5
feat(unreal/ios): drive the engine from the Flutter platform view
juicycleff Sep 5, 2026
f3b3135
feat(unreal/ios): let host apps subclass IOSAppDelegate directly
juicycleff Sep 5, 2026
f88de24
fix(unreal): deliver messages from Flutter to the actors waiting for …
juicycleff Sep 6, 2026
9719f3f
feat(example): build the Unreal demo scene in code
juicycleff Sep 6, 2026
3f134d7
fix(unreal): route wildcard handlers, which is how every actor registers
juicycleff Sep 6, 2026
10d6668
fix(unreal): run messages from Flutter on the game thread, and stream…
juicycleff Sep 6, 2026
e8db970
feat(unreal): receive every message from Flutter without naming a target
juicycleff Sep 6, 2026
16da5ba
chore(example): let the CLI own the Podfile copy phase
juicycleff Sep 6, 2026
d36c359
fix(example): report the render state only once there is one
juicycleff Sep 6, 2026
907f2ef
fix(example): frame the camera once the viewport has a size
juicycleff Sep 6, 2026
ed3584c
test(example): prove the unified handler receives what the router cannot
juicycleff Sep 6, 2026
01e9c4a
feat(unreal): pause the game for real, and let a host unload the engine
juicycleff Sep 6, 2026
08d21c5
feat(unreal/macos): give macOS an embedded path, which the engine has…
juicycleff Sep 7, 2026
1c3aae6
feat(macos): build the macOS half of the framework
juicycleff Sep 7, 2026
dae79de
chore(macos): wire the podspec and enable macOS in the example config
juicycleff Sep 7, 2026
e3b5d8f
docs(unreal): leave a note for whoever picks this up next
juicycleff Sep 7, 2026
11431ba
fix(unity/macos): make it compile, so both engines build together
juicycleff Sep 7, 2026
a7344e7
chore: untrack the Unity widget assessment
juicycleff Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ build/
# ============================================================================
# Unreal Engine Integration
# ============================================================================
# These need the extra ** because an Unreal project usually sits one level
# down, as unreal/<ProjectName>/, and plugins nest their own build output again.
**/unreal/**/Binaries/
**/unreal/**/Build/
**/unreal/**/Intermediate/
**/unreal/**/Saved/
**/unreal/**/DerivedDataCache/
**/unreal/Binaries/
**/unreal/Build/
**/unreal/Intermediate/
Expand Down Expand Up @@ -281,3 +288,13 @@ app.*.symbols

# Keep empty directories with .gitkeep
!**/.gitkeep

# Unreal export outputs. Cooked content is large and rebuilt by
# "game export unreal", so it does not belong in the repository.
engines/unreal/dart/ios/UnrealContent/
engines/unreal/dart/macos/UnrealContent/
example/unreal/demo_exports/

# Content synced into the example app by "game sync unreal", rebuilt on demand.
example/macos/UnrealContent/
example/ios/UnrealContent/
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Game Framework provides a consistent API for integrating game engines into Flutt
- **Modular Architecture** - Use only the engines you need
- **Bidirectional Communication** - Flutter ↔ Engine messaging with type safety
- **Lifecycle Management** - Automatic pause/resume/destroy handling
- **Multi-Platform** - Android, iOS, macOS, Windows, Linux support
- **Multi-Platform** - Android, iOS and web today, with macOS in progress
- **Production Ready** - Comprehensive testing and documentation

## Monorepo Structure
Expand Down Expand Up @@ -256,14 +256,43 @@ Game Engine (Unity/Unreal)

## Platform Support

| Platform | gameframework | Unity | Unreal | Status |
|----------|--------------|-------|--------|--------|
| Android | ✅ Ready | ✅ Ready | 🚧 WIP | Stable |
| iOS | ✅ Ready | ✅ Ready | 🚧 WIP | Stable |
| Web | ✅ Ready | ✅ Ready | ⏳ Planned | Stable |
| macOS | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
| Windows | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
| Linux | ✅ Ready | 🚧 WIP | ⏳ Planned | Beta |
| Platform | Unity | Unreal |
|----------|-------|--------|
| Android | Working | Built, not yet run on a device |
| iOS | Working | Working, verified on device |
| Web | Working | Not started |
| macOS | Builds; not run | Starts and reaches Metal, does not render yet |
| Windows | Stub | Stub |
| Linux | Stub | Stub |

"Stub" means the platform directory holds a CMake file and an empty plugin
entry point. There is no controller and no platform view, so a `GameWidget`
there renders nothing. Web is Unity only; Unreal has no web target.

### Unreal on iOS

Working end to end, three commands and no hand editing:

```bash
game export unreal -p ios && game sync unreal -p ios && flutter build ios
```

Rendering, touch, messaging both ways, pause, and unload. Needs an engine
built from source: an installed engine ships its modules prebuilt, so
`BUILD_EMBEDDED_APP` never reaches them and the framework links, launches and
never boots an engine.

### Unreal on macOS

Further than the table suggests, and not finished. Unreal has no embedded mode
for Mac at all, so the target defines `BUILD_EMBEDDED_APP` itself and the
plugin supplies the startup and view path the engine provides only for iOS.
The app builds, the framework loads, the engine starts, opens the project and
initialises Metal. It then stops because nothing has been cooked for Mac and a
non-editor build cannot compile shaders at runtime.

See `engines/unreal/CONTINUE.md` for what is left and the constraints worth
knowing before changing any of it.

## Continuous Integration

Expand Down
9 changes: 7 additions & 2 deletions engines/unity/dart/ios/gameframework_unity.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ to sync your Unity export to your plugin's ios/ directory.
unity_framework_path = File.join(__dir__, 'UnityFramework.framework')
if File.exist?(unity_framework_path) || File.symlink?(unity_framework_path)
s.preserve_paths = 'UnityFramework.framework', 'UnityFramework.framework/Data'
# Don't vendor - let the consumer plugin vendor it to avoid conflicts
# s.ios.vendored_frameworks = 'UnityFramework.framework'

# Vendor it when it is sitting right here, which is the case after
# "game sync unity -p ios" with no separate game plugin in between. Without
# this nothing embeds the framework and the Swift compiler cannot find the
# module. A consumer plugin that vendors its own build syncs there instead,
# so this stays false for them and there is no duplicate.
s.ios.vendored_frameworks = 'UnityFramework.framework'
end

# Configure framework search paths to find UnityFramework from sibling pods
Expand Down
62 changes: 53 additions & 9 deletions engines/unity/dart/macos/Classes/UnityEngineController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,36 @@ import Cocoa
* - Proper NSView lifecycle management for embedding Unity in Flutter
* - Error handling with descriptive error events
*/
public class UnityEngineController: NSObject, FlutterPlatformView {
/// The part of UnityFramework this controller uses.
///
/// The type itself is not available when building: UnityFramework.framework is
/// assembled per game and loaded from the app bundle at runtime, so naming the
/// class here would make the plugin impossible to compile without a Unity build
/// on hand. The instance arrives through the bundle's principal class and is
/// messaged through this, which is what Objective-C was doing anyway.
///
/// Keep the selectors exact. A mismatch here compiles and then fails as an
/// unrecognised selector at runtime, which is a much worse place to find out.
@objc protocol UnityFrameworkInterface {
@objc func setDataBundleId(_ bundleId: String)

@objc func runEmbedded(withArgc argc: Int32,
argv: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?,
appLaunchOpts: [AnyHashable: Any]?)

@objc func appController() -> NSViewController?

@objc func pause(_ paused: Bool)

@objc func sendMessageToGO(withName name: String,
functionName: String,
message: String)

@objc optional func unloadApplication()
@objc optional func quitApplication(_ exitCode: Int32)
}

public class UnityEngineController: NSObject {

// MARK: - Static Active Controller Tracking

Expand Down Expand Up @@ -198,7 +227,12 @@ public class UnityEngineController: NSObject, FlutterPlatformView {
}

// Register the Unity framework with the FlutterBridgeRegistry
FlutterBridgeRegistry.register(unityFramework: unityFramework)
// The registry stores it as an NSObject, which every Unity
// framework instance is; the protocol is only how this file talks
// to it.
if let asObject = unityFramework as? NSObject {
FlutterBridgeRegistry.register(unityFramework: asObject)
}

// Set up Unity framework
unityFramework.setDataBundleId("com.unity3d.framework")
Expand All @@ -211,8 +245,7 @@ public class UnityEngineController: NSObject, FlutterPlatformView {
)

// Get Unity's root view
if let appController = unityFramework.appController(),
let rootView = appController.rootViewController?.view {
if let rootView = unityFramework.appController()?.view {
self.unityView = rootView

// Embed Unity view in our container
Expand Down Expand Up @@ -325,7 +358,9 @@ public class UnityEngineController: NSObject, FlutterPlatformView {
// MARK: - Unity Message Handling (called from C bridge)

/// Called from Unity when a message is sent to Flutter
@objc public func onUnityMessage(target: String, method: String, data: String) {
/// Swift-side convenience. Deliberately not @objc: it would carry the same
/// selector as the method below, and two of those on one class is an error.
public func onUnityMessage(target: String, method: String, data: String) {
onUnityMessageWithTarget(target, method: method, data: data)
}

Expand Down Expand Up @@ -400,9 +435,9 @@ public class UnityEngineController: NSObject, FlutterPlatformView {
// - Data/ = game data
// Pre-load GameAssembly.dylib so UnityPlayer can resolve IL2CPP symbols when the bundle loads.

private func loadUnityFramework() -> UnityFramework? {
private func loadUnityFramework() -> UnityFrameworkInterface? {
// Try to get from cache first
if let cached = FlutterBridgeRegistry.sharedUnityFramework as? UnityFramework {
if let cached = FlutterBridgeRegistry.sharedUnityFramework as? UnityFrameworkInterface {
return cached
}

Expand Down Expand Up @@ -431,8 +466,17 @@ public class UnityEngineController: NSObject, FlutterPlatformView {
return nil
}

let getInstance = principalClass.getInstance()
return getInstance as? UnityFramework
// getInstance is Unity's own class method, so it has to be sent
// dynamically too rather than called on a type nothing here declares.
let selector = NSSelectorFromString("getInstance")
guard let unityClass = principalClass as? NSObject.Type,
unityClass.responds(to: selector) else {
NSLog("UnityEngineController [macOS]: principal class has no getInstance")
return nil
}

let instance = unityClass.perform(selector)?.takeUnretainedValue()
return instance as? UnityFrameworkInterface
}

// MARK: - Cleanup
Expand Down
85 changes: 85 additions & 0 deletions engines/unreal/CONTINUE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Where the Unreal work stands

Written 2026-09-07, at the end of a long session. Everything here was seen
working or seen failing on this machine, not inferred.

## iOS: working, verified on device

Unreal renders inside a `GameWidget` on an iPhone 16 Pro at full native
portrait resolution. Drag orbits, pinch zooms, the HUD controls drive the
cube, and camera state streams back. Pause freezes the scene, unload releases
the view and reload brings it back mid-scene.

Three commands, no hand editing:

game export unreal -p ios && game sync unreal -p ios && flutter build ios

`flutter run` times out installing 565MB over wireless. USB is fine.

## macOS: builds, starts, stops at shaders

Further than it looks, and not finished. The app builds, the framework loads,
the engine starts, reads its command line, opens the project and initialises
Metal. It then fails:

LogShaderLibrary: Error: Failed to initialize ShaderCodeLibrary ...
part of the Global shader library is missing

Nothing has been cooked for Mac, and a non-editor build cannot compile shaders
at runtime.

**Next step, and it is a long one.** Build the Mac editor from source, cook the
project for Mac, then re-export. The engine build alone took 78 minutes for the
game target; the editor is bigger. After that the framework should have what it
needs, and the next unknown is whether reparenting the engine's `FCocoaWindow`
content view into the Flutter platform view actually renders. That has never
run, so treat it as unproven rather than merely untested.

Two things a macOS host needs, which the export prints but nothing enforces:

- The app must not be sandboxed for an uncooked run, because it reads the
project from a path outside its container. Cooked content in the bundle
removes this.
- The engine finds its own content relative to the executable, so it needs
`-basedir=<engine>/Engine/Binaries/Mac`. That is read from the process argv,
not from `uecommandline.txt`, so it cannot be set from inside the library.
A staged layout beside the app would avoid it.

## Android

The UPL migration is in and the library-mode Java is injected at build time.
Not run on a device in this session, so treat it as built but unverified.

## Things worth knowing before changing anything

- `BUILD_EMBEDDED_APP` is defined only by `UEBuildIOS.cs`. The Mac target
defines it itself and takes `TargetBuildEnvironment.Unique` so it reaches
Core, which is why the Mac build rebuilds the engine.
- The host's tick is a display link on the **main thread**, not Unreal's game
loop. `TickGameThread` drains its queue on whoever calls it, so anything
touching the renderer must not run there. `FTSTicker` is the way onto the
real game thread; `RunOnGameThread` is not.
- Unreal's log file is buffered and mostly shows startup. Do not conclude
anything from a missing runtime line. Route diagnostics back over the bridge
instead; `flutter.TraceMessages 1` turns the message trace on.
- A flat iOS framework must not contain `Resources/`. A versioned macOS one
must carry `Versions/A/Resources/Info.plist`. These are opposite rules and
both are enforced by tooling that blames something else.

## Unity's macOS controller

It did not compile, which broke the macOS build of any app depending on both
engines. Fixed to the point where both build together, and no further: Unity
could not be run here, so treat macOS Unity as compiling rather than working.

What was wrong: it conformed to `FlutterPlatformView`, which does not exist on
macOS and which its factory never needed, since that already returns an
`NSView`. It named the `UnityFramework` type, which is not available when
building because the framework is assembled per game and loaded from the
bundle at runtime. And it declared two methods carrying the same Objective-C
selector.

The framework is reached through an `@objc protocol` now, so the selectors are
declared in one place. Get one wrong and it compiles and then fails at runtime
as an unrecognised selector, so they are worth checking against Unity's own
header before trusting them.
Loading
Loading