juicycleff / flutter-unity-view-widget

Embeddable unity game engine view for Flutter. Advance demo here https://github.com/juicycleff/flutter-unity-arkit-demo
BSD 3-Clause "New" or "Revised" License
2.11k stars 510 forks source link

Blank screen on initializing UnityWidget, iOS #540

Open jafitz26 opened 2 years ago

jafitz26 commented 2 years ago

On first running the UnityWidget, the Unity Game's sound plays, but a blank screen is pulled up, on iOS only. This screen is the same color as my main app's loading screen.

If I hot restart the app from this screen, the Unity game's sound continues playing behind the main app, and after navigating back to the UnityWidget, the Unity game runs as intended.

The only work around I've found is automating a pop back to the main app, then rerunning the UnityWidget, which is not a good user experience.

-- Error output:

CrashReporter: initialized Built from '2021.2/staging' branch, Version '2021.2.11f1 (e50cafbb4399)', Build type 'Release', Scripting Backend 'il2cpp' MemoryManager: Using 'Default' Allocator. [UnityMemory] Configuration Parameters - Can be set up in boot.config "memorysetup-bucket-allocator-granularity=16" "memorysetup-bucket-allocator-bucket-count=8" "memorysetup-bucket-allocator-block-size=4194304" "memorysetup-bucket-allocator-block-count=1" "memorysetup-main-allocator-block-size=16777216" "memorysetup-thread-allocator-block-size=16777216" "memorysetup-gfx-main-allocator-block-size=16777216" "memorysetup-gfx-thread-allocator-block-size=16777216" "memorysetup-cache-allocator-block-size=4194304" "memorysetup-typetree-allocator-block-size=2097152" "memorysetup-profiler-bucket-allocator-granularity=16" "memorysetup-profiler-bucket-allocator-bucket-count=8" "memorysetup-profiler-bucket-allocator-block-size=4194304" "memorysetup-profiler-bucket-allocator-block-count=1" "memorysetup-profiler-allocator-block-size=16777216" "memorysetup-profiler-editor-allocator-block-size=1048576" "memorysetup-temp-allocator-size-main=4194304" "memorysetup-job-temp-allocator-block-size=2097152" "memorysetup-job-temp-allocator-block-size-background=1048576" "memorysetup-job-temp-allocator-reduction-small-platforms=262144" "memorysetup-temp-allocator-size-background-worker=32768" "memorysetup-temp-allocator-size-job-worker=262144" "memorysetup-temp-allocator-size-preload-manager=262144" "memorysetup-temp-allocator-size-nav-mesh-worker=65536" "memorysetup-temp-allocator-size-audio-worker=65536" "memorysetup-temp-allocator-size-cloud-worker=32768" "memorysetup-temp-allocator-size-gfx=262144" -> applicationDidFinishLaunching() -> applicationWillEnterForeground() -> applicationDidBecomeActive() Unexpected node type. Unexpected node type. GfxDevice: creating device client; threaded=1; jobified=0 Initializing Metal device caps: Apple A9 GPU Initialize engine version: 2021.2.11f1 (e50cafbb4399) flutter: in onUnityCreated CrashReporter: No pending report exists at /var/mobile/Containers/Data/Application/D17E09AB-1621-4810-8A8E-EDBB1BE93C4C/Library/Caches/CrashReports/crash-pending.plcrash

Drarox commented 2 years ago

Exact same issue here, on iOS only, the UnityWidget is blank but the sound of the Unity game is playing.

I tried to pop back the screen after x seconds and then rerunning the UnityWidget and it's working then like you said, but it's not user friendly.

It was perfectly working a few weeks ago, I'm trying to figure out since when it's not working anymore because I did many upgrades.

What I have tried so far without any results :

I have also recently updated Flutter from 2.8.1 to 2.10.1, I have not tried yet if this could be related to the issue.

Drarox commented 2 years ago

As a workaround for the moment for iOS, I pop the UnityWidget on the first opening in the onUnityCreated and then relaunch it 1 second after it's closed (don't relaunch it immediately or the same issue will still be there). This causes for the user only a blinking the first time opening, but it's working then.

steven-voon commented 2 years ago

@Drarox i ran into the same problem, would you mind to share with me your solution on “pop the widget and relaunch it after 1 sec” part ? I’m from Unity and still new to flutter, I will be very appreciate if you are willing to share with me.

jafitz26 commented 2 years ago

There may be a better way to do this, but my solution to popping the widget and relaunching it was to use two Future.delayed() in onUnityCreated. So something like:

widget.isInit ? Future.delayed( Duration(milliseconds: 300), () async { Navigator.of(context).pop(); Future.delayed( Duration(milliseconds: 500), () async { widget.initCallback(); }, ); }, ) : null;

The initCallback() in the enclosing Class pushes the Class with the UnityWidget in it again:

Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => UnityClass(isInit: false, initCallback: initCallback,), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ));

I also added a loading screen at the top of a Stack that pops after another Future.delayed duration.

steven-voon commented 2 years ago

@jafitz26 thanks for the sharing, I don’t quite understand the part on widget.isinit and widget.initCallback is it possible for you to share the whole code for me to learn about it?

jafitz26 commented 2 years ago

Sure, here is the code within the context of the two classes I'm using:

class FlutterPage extends StatefulWidget {
  const FlutterPage({Key? key}) : super(key: key);

  @override
  _FlutterPageState createState() => _FlutterPageState();
}

class _FlutterPageState extends State<FlutterPage> {

  void initCallback(){
    Navigator.push(
        context,
        PageRouteBuilder(
          pageBuilder: (context, animation1, animation2) => UnityPage(isInit: false, initCallback: (){},),
          transitionDuration: Duration.zero,
          reverseTransitionDuration: Duration.zero,
        ));
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: (){
        Navigator.push(
            context,
            PageRouteBuilder(
              pageBuilder: (context, animation1, animation2) => UnityPage(isInit: true, initCallback: initCallback,),
              transitionDuration: Duration.zero,
              reverseTransitionDuration: Duration.zero,
            ));

      },
      child: Text('Launch'),
    );
  }
}

class UnityPage extends StatefulWidget {
  UnityPage({required this.initCallback, required this.isInit,});

  VoidCallback initCallback;
  bool isInit;

  @override
  _UnityPageState createState() => _UnityPageState();
}

class _UnityPageState extends State<UnityPage> {

  late UnityWidgetController unityWidgetController;

  void _onUnityCreated(controller) async {

    this.unityWidgetController = await controller;

    widget.isInit ? Future.delayed(
      Duration(milliseconds: 300),
          () async {
            await unityWidgetController.pause();
            Navigator.of(context).pop();
        Future.delayed(
          Duration(milliseconds: 500),
              () async {
            widget.initCallback();
          },
        );
      },
    ) : null;
  }

  @override
  Widget build(BuildContext context) {
    return   UnityWidget(
      onUnityCreated: _onUnityCreated,
    );
  }
}
WellingtonBipo commented 2 years ago

@jafitz26 thanks for the sharing, I don’t quite understand the part on widget.isinit and widget.initCallback is it possible for you to share the whole code for me to learn about it?

Other approach that works for me is remove the UnityWidget for the widget tree.

You can use the same time intervals and instead of navigate between pages you can call setState and with a conditional, remove from the widget tree.

Like this:

bool _maintainUnity = false;

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          if (_maintainUnity) _unityWidget,
        ],
      ),
    );
  }
steven-voon commented 2 years ago

@jafitz26 thanks for the sharing your code, I had a quick implementation just now and it does work on iPad but something went wrong when it comes to iPhone, i will check on it myself and share it here if I managed to found the cause of it.

@WellingtonBipo thanks for the generous sharing ! Will try your suggestion today,

Holosynthetic commented 2 years ago

This is an issue related to the flutter_unity_widget iOS framework. If you take a look at the UnityPlayerUtils.swift file, you'll notice the createPlayer method never invokes the callback with the UnityFramework's rootView on first load. It's expecting the UnityReady notification to perform the callback, but it's never posted.

For now, I've changed the method body to the following and it works:

if unityIsInitiallized() && _isUnityReady {
    completed(controller?.rootView)
    return
}

DispatchQueue.main.async {
    // Always keep Flutter window on top
    let flutterLevel = UIWindow.Level.normal.rawValue + 1.0
    UIApplication.shared.keyWindow?.windowLevel = UIWindow.Level(flutterLevel)

    self.initUnity()
    self._isUnityReady = true
    completed(controller?.rootView)
    self.listenAppState()
}
WellingtonBipo commented 2 years ago

@Holosynthetic thanks very much. It's works fine. The only problem is that this file is inside flutter_unity_widget and if you delete the package and run flutter pub get you have to change the code again.

And to make easy to find the file, follow the path ios/.symlinks/plugins/flutter_unity_widget/ios/Classes/UnityPlayerUtils.swift (flutter_unity_widget folder is a link) or .pub-cach/host/pub.dartlang.org/flutter_unity_widget/ios/Classes/UnityPlayerUtils.swift

bonfry commented 2 years ago

Hi, I've a similar problem but with Android. I would like understand if solutions writed in this issue work also on Android. Thanks and sorry for my english

jamesncl commented 2 years ago

@Holosynthetic thanks for the fix, had the exact same issue and this resolved it for me. The code has changed a bit in the latest version, so it's now:

// Create new unity player
    func createPlayer(completed: @escaping (_ view: UIView?) -> Void) {
        if self.unityIsInitiallized() && self._isUnityReady {
            completed(controller?.rootView)
        }

        NotificationCenter.default.addObserver(forName: NSNotification.Name("UnityReady"), object: nil, queue: OperationQueue.main, using: { note in
            self._isUnityReady = true
            completed(controller?.rootView)
        })

        DispatchQueue.main.async {
            if (sharedApplication == nil) {
                sharedApplication = UIApplication.shared
            }

            // Always keep Flutter window on top
            let flutterUIWindow = sharedApplication?.keyWindow
            flutterUIWindow?.windowLevel = UIWindow.Level(UIWindow.Level.normal.rawValue + 1) // Always keep Flutter window in top
            sharedApplication?.keyWindow?.windowLevel = UIWindow.Level(UIWindow.Level.normal.rawValue + 1)

            self.initUnity()
            unity_warmed_up = true
            completed(controller?.rootView)    <--------- ADD THIS LINE
            self.listenAppState()
        }

        if unity_warmed_up == true {
            self._isUnityReady = true
            self._isUnityLoaded = true
            completed(controller?.rootView)
        }
    }

@WellingtonBipo I've done a pull request for this change so hopefully @juicycleff can merge it into master and publish a new version, so that you don't have to manually edit the files each time you pub get. Alternatively, just create your own fork and make the change there, then point to your fork in pubspec.yaml. Or you could just use my fork until the PR is merged:

  # TODO switch back to the main flutter_unity_widget repository when a new version published with issue #540 fixed
  # Use fork which includes fix issue #540 (iOS widget not showing on first load)
  flutter_unity_widget:
    git:
      url: https://github.com/jamesncl/flutter-unity-view-widget.git
      # Pin the commit which fixes issue #540 to prevent tracking master which may introduce unexpected changes
      ref: 3acea6c42afb2663c1e306693458d96141587aba
jamesncl commented 2 years ago

Hi, I've a similar problem but with Android. I would like understand if solutions writed in this issue work also on Android. Thanks and sorry for my english

No this is a fix for iOS only. I suggest making sure you're using the latest version of the package, use Unity 2022.1.0 and double-check you have followed all the setup steps. Also, try using the runImmediately: true flag on UnityWidget, and if you're using a placeholder, try not using a placeholder (I had a problem on Android where the placeholder wasn't removed after load). If problem still persists, open a separate issue with details.

VivianKuKu commented 2 years ago

Hello @jamesncl & @WellingtonBipo , Can I understand where the UnityPlayerUtils.swift is? I have the same error but couldn't find this file inside my Flutter and Unity folders. Thank you.

Screenshot 2022-06-04 at 23 08 55
timbotimbo commented 2 years ago

I know this has been fixed in package version 2022.1.1 but I noticed something interesting.

This bug popped up for me on changing a minor Unity version for an older project.

Given that this issue was known when using Unity 2020 or 2021, I guess some change got backported to Unity 2019 LTS. So this bug was actually caused by a Unity change.

@VivianKuKu UnityPlayerUtils.swift is in the cache for installed packages (where changes will be overridden by pub get, pub clean etc. ).

FLUTTER-INSTALL-LOCATION\.pub-cache\hosted\pub.dartlang.org\flutter_unity_widget-XXX\ios\classes

AmazeFPig commented 1 year ago

Is this fixed? I still have this issue after I tried 2022.1.7f1, 2022.1.1, 2022.1.1-v2 unitypackages

AmazeFPig commented 1 year ago

Ok...I checked UnityPlayerUtils.swift file in my project. Somehow some lines are commented and I edited it exact the same as @jamesncl did, and everything works fine.

alicanyimaz commented 1 year ago

Here is my solution

change UnityPlayerUtils.swift -> createPlayer function

func createPlayer(completed: @escaping (_ view: UIView?) -> Void) {

    if self.unityIsInitiallized() && self._isUnityReady {

        completed(controller?.rootView)

        return
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

        DispatchQueue.main.async {
            // Always keep Flutter window on top
            let flutterLevel = UIWindow.Level.normal.rawValue + 1.0
            UIApplication.shared.keyWindow?.windowLevel = UIWindow.Level(flutterLevel)

            self.initUnity()
            self._isUnityReady = true
            completed(controller?.rootView)
            self.listenAppState()
        }

        NotificationCenter.default.addObserver(forName: NSNotification.Name("UnityReady"), object: nil, queue: OperationQueue.main, using: { note in
            self._isUnityReady = true
            completed(controller?.rootView)
        })
    } 

}

kamami commented 1 year ago

Here is my solution

change UnityPlayerUtils.swift -> createPlayer function

func createPlayer(completed: @escaping (_ view: UIView?) -> Void) {

    if self.unityIsInitiallized() && self._isUnityReady {

        completed(controller?.rootView)

        return
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

        DispatchQueue.main.async {
            // Always keep Flutter window on top
            let flutterLevel = UIWindow.Level.normal.rawValue + 1.0
            UIApplication.shared.keyWindow?.windowLevel = UIWindow.Level(flutterLevel)

            self.initUnity()
            self._isUnityReady = true
            completed(controller?.rootView)
            self.listenAppState()
        }

        NotificationCenter.default.addObserver(forName: NSNotification.Name("UnityReady"), object: nil, queue: OperationQueue.main, using: { note in
            self._isUnityReady = true
            completed(controller?.rootView)
        })
    } 

}

This works, but the widget seems to overlap the AppBar of the Scaffold now. The back button is missing. Without the fix, the back button is there, but also the white screen.

J-Dark commented 2 months ago

I'm still experiencing this bug with version 2022.2.1 ( tried with unity 2020, 2021 and 2022). My unity app has multiple scenes:

Now, if I load a normal scene, everything works; but if I push an AR scene, I get the blank screen.

The AR scene is actually running: I can hear the correct audio when I point to an AR marker. If I pop and re-push the page without reverting to the init scene, everything starts working fine.

I tried @jamesncl solution, but as @kamami said in the last message, Flutter UI is now below the unity widget and there's no way to go back.