diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000..688a936
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,63 @@
+{
+ "projectName": "flutter-unity-view-widget",
+ "projectOwner": "snowballdigital",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": true,
+ "commitConvention": "eslint",
+ "contributors": [
+ {
+ "login": "juicycleff",
+ "name": "Rex Raphael",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/11243590?v=4",
+ "profile": "http://rexraphael.com",
+ "contributions": [
+ "code",
+ "doc",
+ "question",
+ "bug",
+ "review",
+ "tutorial"
+ ]
+ },
+ {
+ "login": "thomas-stockx",
+ "name": "Thomas Stockx",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1475368?v=4",
+ "profile": "https://stockxit.com",
+ "contributions": [
+ "code",
+ "doc",
+ "question",
+ "tutorial"
+ ]
+ },
+ {
+ "login": "krispypen",
+ "name": "Kris Pypen",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/156955?v=4",
+ "profile": "http://krispypen.github.io/",
+ "contributions": [
+ "code",
+ "doc",
+ "question",
+ "tutorial"
+ ]
+ },
+ {
+ "login": "lorant-csonka-planorama",
+ "name": "Lorant Csonka",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/48209860?v=4",
+ "profile": "https://github.com/lorant-csonka-planorama",
+ "contributions": [
+ "doc",
+ "video"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7
+}
diff --git a/.gitignore b/.gitignore
index d9ee87c..f6748aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,10 @@
*.iws
.idea/
+node_modules/
+./package.json
+.idea/
+
# Visual Studio Code related
.vscode/
diff --git a/2019_03_28_19_23_37.gif b/2019_03_28_19_23_37.gif
deleted file mode 100644
index daf95e7..0000000
Binary files a/2019_03_28_19_23_37.gif and /dev/null differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54780dc..63f7d18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,23 @@
+## 0.1.6+1
+
+* Adding Metal renderer support (on iOS) [@krispypen](https://github.com/krispypen)
+
+## 0.1.6
+
+* iOS support for the Unity 2019.3 new export format Unity as a Library [@krispypen](https://github.com/krispypen)
+
+## 0.1.5
+
+* Android support for the Unity 2019.3 new export format Unity as a Library [@thomas-stockx](https://github.com/thomas-stockx)
+
## 0.1.4
-* Support for AR on Android thanks to @thomas-stockx
-
+* Support for AR on Android thanks to [@thomas-stockx](https://github.com/thomas-stockx)
## 0.1.3+4
-* Change input source of Flutter touch events so they work in Unity @thomas-stockx
+* Change input source of Flutter touch events so they work in Unity [@thomas-stockx](https://github.com/thomas-stockx)
* Instructions on how to implement Vuforia AR
-* Fix postMessage throwing exceptions on Android @thomas-stockx
-* Add video tutorial, replace `unity-player` with `unity-classes` in example
-* Remove java and UnityPlayer changes to the windowmanager
+* Fix postMessage throwing exceptions on Android [@thomas-stockx](https://github.com/thomas-stockx)
+* Add video tutorial, replace `unity-player` with `unity-classes` in example [@lorant-csonka-planorama](https://github.com/lorant-csonka-planorama)
+* Remove java and UnityPlayer changes to the windowmanager [@thomas-stockx](https://github.com/thomas-stockx)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..3de396f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at rex@snowball.digital. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/README.md b/README.md
index 2ce9de0..aa1c35c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# flutter_unity_widget
+[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-)
[![version][version-badge]][package]
[![MIT License][license-badge]][license]
@@ -7,14 +8,14 @@
[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]
-Flutter unity 3D widget for embedding unity in flutter. Add a Flutter widget to show unity. Works on Android, iOS in works.
+Flutter unity 3D widget for embedding unity in flutter. Now you can make awesome gamified features of your app in Unity and get it rendered in a Flutter app both in fullscreen and embeddable mode. Works great on Android and iOS.
## Installation
First depend on the library by adding this to your packages `pubspec.yaml`:
```yaml
dependencies:
- flutter_unity_widget: ^0.1.4
+ flutter_unity_widget: ^0.1.6+1
```
Now inside your Dart code you can import it.
@@ -26,7 +27,10 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart';
## Preview
-![gif](https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/2019_03_28_19_23_37.gif?raw=true)
+30 fps gifs, showcasing communication between Flutter and Unity:
+
+![gif](https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/preview_android.gif?raw=true)
+![gif](https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/preview_ios.gif?raw=true)
@@ -57,9 +61,11 @@ Now your project files should look like this.
1. First Open Unity Project.
-2. Click Menu: File => Build Settings => Player Settings
+2. Click Menu: File => Build Settings
-3. Change `Product Name` to Name of the Xcode project, You can find it follow `ios/${XcodeProjectName}.xcodeproj`.
+Be sure you have at least one scene added to your build.
+
+3. => Player Settings
**Android Platform**:
1. Make sure your `Graphics APIs` are set to OpenGLES3 with a fallback to OpenGLES2 (no Vulkan)
@@ -71,22 +77,20 @@ Now your project files should look like this.
- ARM64 ✅
- x86 ✅
+
- **IOS Platform**:
- 1. Other Settings find the Rendering part, uncheck the `Auto Graphics API` and select only `OpenGLES2`.
+ **iOS Platform**:
+ 1. This only works with Unity version >=2019.3 because uses Unity as a library!
2. Depending on where you want to test or run your app, (simulator or physical device), you should select the appropriate SDK on `Target SDK`.
-
-
-
### Add Unity Build Scripts and Export
-Copy [`Build.cs`](https://github.com/f111fei/react-native-unity-demo/blob/master/unity/Cube/Assets/Scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/f111fei/react-native-unity-demo/blob/master/unity/Cube/Assets/Scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/`
+Copy [`Build.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/`
-Open your unity project in Unity Editor. Now you can export unity project with `Flutter/Export Android` or `Flutter/Export IOS` menu.
+Open your unity project in Unity Editor. Now you can export the Unity project with `Flutter/Export Android` (for Unity versions up to 2019.2), `Flutter/Export Android (Unity 2019.3.*)` (for Unity versions 2019.3 and up, which uses the new [Unity as a Library](https://blogs.unity3d.com/2019/06/17/add-features-powered-by-unity-to-native-mobile-apps/) export format), or `Flutter/Export IOS` menu.
@@ -99,20 +103,13 @@ IOS will export unity project to `ios/UnityExport`.
**Android Platform Only**
1. After exporting the unity game, open Android Studio and and add the `Unity Classes` Java `.jar` file as a module to the unity project. You just need to do this once if you are exporting from the same version of Unity everytime. The `.jar` file is located in the ```/android/UnityExport/lib``` folder
- 2. Next open `build.gradle` of `flutter_unity_widget` module and replace the dependencies with
-```gradle
- dependencies {
- implementation project(':UnityExport') // The exported unity project
- implementation project(':unity-classes') // the unity classes module you added from step 1
- }
-```
- 3. Next open `build.gradle` of `UnityExport` module and replace the dependencies with
+ 2. If using Unity 2019.2 or older, open `build.gradle` of `UnityExport` module and replace the dependencies with
```gradle
dependencies {
implementation project(':unity-classes') // the unity classes module you added from step 1
}
```
- 4. Next open `build.gradle` of `UnityExport` module and remove these
+ 3. If using Unity 2019.2 or older, open `build.gradle` of `UnityExport` module and remove these
```gradle
bundle {
language {
@@ -127,9 +124,31 @@ IOS will export unity project to `ios/UnityExport`.
}
```
+**iOS Platform Only**
+
+ 1. open your ios/Runner.xcworkspace (workspace!, not the project) in Xcode and add the exported project in the workspace root (with a right click in the Navigator, not on an item -> Add Files to “Runner” -> add the UnityExport/Unity-Iphone.xcodeproj file
+
+ 2. Select the Unity-iPhone/Data folder and change the Target Membership for Data folder to UnityFramework
+
+ 3. Add this to your Runner/Runner/Runner-Bridging-Header.h
+
+```c
+#import "UnityUtils.h"
+```
+ 4. Add to AppDelegate.swift before the GeneratePluginRegistrant call:
+
+```swift
+InitArgs(CommandLine.argc, CommandLine.unsafeArgv)
+```
+ 5. Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file with the key `io.flutter.embedded_views_preview` and the value `YES`.
+
-### AR Foundation (ANDROID only at the moment)
+### AR Foundation (not compatible with Unity 2019.3)
+https://github.com/Unity-Technologies/arfoundation-samples/issues/210
+
+Android only as iOS requires Unity 2019.3.
+
If you want to use Unity for integrating Augmented Reality in your Flutter app, a few more changes are required:
1. Export the Unity Project as previously stated (using the Editor Build script).
2. Check if the exported project includes all required Unity libraries (.so) files (`lib/\/libUnityARCore.so` and `libarpresto_api.so`). There seems to be a bug where a Unity export does not include all lib files. If they are missing, use Unity to build a standalone .apk of your AR project, unzip the resulting apk, and copy over the missing .lib files to the `UnityExport` module.
@@ -276,15 +295,16 @@ class _UnityDemoScreenState extends State{
## API
- `pause()` (Use this to pause unity player)
- `resume()` (Use this to resume unity player)
+ - `postMessage(String gameObject, methodName, message)` (Allows you invoke commands in Unity from flutter)
+ - `onUnityMessage(data)` (Unity to flutter bindding and listener)
## Known issues
- - no iOS support yet
- Android Export requires several manual changes
- Using AR will make the activity run in full screen (hiding status and navigation bar).
[version-badge]: https://img.shields.io/pub/v/flutter_unity_widget.svg?style=flat-square
-[package]: https://pub.dartlang.org/packages/flutter_unity_widget/versions/0.1.2
+[package]: https://pub.dartlang.org/packages/flutter_unity_widget/
[license-badge]: https://img.shields.io/github/license/snowballdigital/flutter-unity-view-widget.svg?style=flat-square
[license]: https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
@@ -293,3 +313,25 @@ class _UnityDemoScreenState extends State{
[github-watch]: https://github.com/snowballdigital/flutter-unity-view-widget/watchers
[github-star-badge]: https://img.shields.io/github/stars/snowballdigital/flutter-unity-view-widget.svg?style=social
[github-star]: https://github.com/snowballdigital/flutter-unity-view-widget/stargazers
+
+## Contributors ✨
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
diff --git a/change_target_membership_data_folder.png b/change_target_membership_data_folder.png
new file mode 100644
index 0000000..1373a57
Binary files /dev/null and b/change_target_membership_data_folder.png differ
diff --git a/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csproj.CoreCompileInputs.cache b/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csproj.CoreCompileInputs.cache
index 3d80074..a4971f5 100644
--- a/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csproj.CoreCompileInputs.cache
+++ b/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csproj.CoreCompileInputs.cache
@@ -1 +1 @@
-eb7addd6604d08d1b14bdd2f0b7568eea6aaad77
+5c787ba36b071ef6ee6a4f60499357697d53c75b
diff --git a/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csprojAssemblyReference.cache b/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csprojAssemblyReference.cache
index 9d287cb..a9d7731 100644
Binary files a/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csprojAssemblyReference.cache and b/example/unity/DemoApp/obj/Debug/Assembly-CSharp-Editor.csprojAssemblyReference.cache differ
diff --git a/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csproj.CoreCompileInputs.cache b/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csproj.CoreCompileInputs.cache
index 6a95d74..ee3ccf3 100644
--- a/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csproj.CoreCompileInputs.cache
+++ b/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csproj.CoreCompileInputs.cache
@@ -1 +1 @@
-1d70ee33aab6965c19c5636e539561c5c25ac66b
+bd1065d7afc1a8da0fc454a1d7dede98f3aaa473
diff --git a/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csprojAssemblyReference.cache b/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csprojAssemblyReference.cache
index 08da307..4521a16 100644
Binary files a/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csprojAssemblyReference.cache and b/example/unity/DemoApp/obj/Debug/Assembly-CSharp.csprojAssemblyReference.cache differ
diff --git a/ios/Classes/FlutterUnity.h b/ios/Classes/FlutterUnity.h
deleted file mode 100644
index ca66ad7..0000000
--- a/ios/Classes/FlutterUnity.h
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// Created by rex on 19/03/2019.
-//
-
-#ifndef FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H
-#define FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H
-
-
-#import
-
-@interface FlutterUnityController : NSObject
-
-- (instancetype)initWithWithFrame:(CGRect)frame
- viewIdentifier:(int64_t)viewId
- arguments:(id _Nullable)args
- binaryMessenger:(NSObject*)messenger;
-
-- (UIView*)view;
-@end
-
-@interface FlutterUnityFactory : NSObject
-- (instancetype)initWithMessenger:(NSObject*)messenger;
-@end
-
-#endif //FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H
diff --git a/ios/Classes/FlutterUnity.m b/ios/Classes/FlutterUnity.m
deleted file mode 100644
index 2eff0b8..0000000
--- a/ios/Classes/FlutterUnity.m
+++ /dev/null
@@ -1,87 +0,0 @@
-//
-// Created by rex on 19/03/2019.
-//
-
-#include "FlutterUnity.h"
-
-@implementation FlutterUnityFactory {
- NSObject* _messenger;
-}
-
-- (instancetype)initWithMessenger:(NSObject*)messenger {
- self = [super init];
- if (self) {
- _messenger = messenger;
- }
- return self;
-}
-
-
-@implementation FlutterUnityController {
- WKWebView* _webView;
- int64_t _viewId;
- FlutterMethodChannel* _channel;
-}
-
-- (instancetype)initWithWithFrame:(CGRect)frame
- viewIdentifier:(int64_t)viewId
- arguments:(id _Nullable)args
- binaryMessenger:(NSObject*)messenger {
- if ([super init]) {
- _viewId = viewId;
- _webView = [[WKWebView alloc] initWithFrame:frame];
- NSString* channelName = [NSString stringWithFormat:@"nativeweb_%lld", viewId];
- _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
- __weak __typeof__(self) weakSelf = self;
- [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
- [weakSelf onMethodCall:call result:result];
- }];
-
- }
- return self;
-}
-
-- (UIView*)view {
- return _webView;
-}
-
-- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
- if ([[call method] isEqualToString:@"postMessage"]) {
- [self postMessage:call result:result];
- } else if ([[call method] isEqualToString:@"isReady"]) {
- [self postMessage:call result:result];
- } else if ([[call method] isEqualToString:@"createUnity"]) {
- [self postMessage:call result:result];
- } else if ([[call method] isEqualToString:@"pause"]) {
- [self postMessage:call result:result];
- } else if ([[call method] isEqualToString:@"resume"]) {
- [self postMessage:call result:result];
- } else {
- result(FlutterMethodNotImplemented);
- }
-}
-
-- (void)postMessage:(FlutterMethodCall*)call result:(FlutterResult)result {
- NSString* url = [call arguments];
- if (![self postMessage:url]) {
- result([FlutterError errorWithCode:@"loadUrl_failed"
- message:@"Failed parsing the URL"
- details:[NSString stringWithFormat:@"URL was: '%@'", url]]);
- } else {
- result(nil);
- }
-}
-
-- (bool)onPostMessage:(NSString*)url {
- NSURL* nsUrl = [NSURL URLWithString:url];
- if (!nsUrl) {
- return false;
- }
- NSURLRequest* req = [NSURLRequest requestWithURL:nsUrl];
- [_webView loadRequest:req];
- return true;
-}
-
-@end
-
-
diff --git a/ios/Classes/FlutterUnityView.h b/ios/Classes/FlutterUnityView.h
new file mode 100644
index 0000000..2b1bad0
--- /dev/null
+++ b/ios/Classes/FlutterUnityView.h
@@ -0,0 +1,18 @@
+//
+// FlutterUnityView.h
+// FlutterUnityView
+//
+// Created by krispypen on 8/1/2019
+//
+
+#import
+
+#import "UnityUtils.h"
+
+@interface FlutterUnityView : UIView
+
+@property (nonatomic, strong) UIView* uView;
+
+- (void)setUnityView:(UIView *)view;
+
+@end
diff --git a/ios/Classes/FlutterUnityView.m b/ios/Classes/FlutterUnityView.m
new file mode 100644
index 0000000..ea5753e
--- /dev/null
+++ b/ios/Classes/FlutterUnityView.m
@@ -0,0 +1,37 @@
+//
+// FlutterUnityView.m
+// FlutterUnityView
+//
+// Created by krispypen on 8/1/2019
+//
+
+#import "FlutterUnityView.h"
+
+@implementation FlutterUnityView
+
+- (id)initWithFrame:(CGRect)frame
+{
+ self = [super initWithFrame:frame];
+ return self;
+}
+
+- (void)dealloc
+{
+}
+
+- (void)setUnityView:(UIView *)view
+{
+ self.uView = view;
+ [self setNeedsLayout];
+}
+
+- (void)layoutSubviews
+{
+ [super layoutSubviews];
+ [(UIView *)self.uView removeFromSuperview];
+ [self insertSubview:(UIView *)self.uView atIndex:0];
+ ((UIView *)self.uView).frame = self.bounds;
+ [(UIView *)self.uView setNeedsLayout];
+}
+
+@end
diff --git a/ios/Classes/FlutterUnityWidgetPlugin.h b/ios/Classes/FlutterUnityWidgetPlugin.h
index 0de4a00..013da00 100644
--- a/ios/Classes/FlutterUnityWidgetPlugin.h
+++ b/ios/Classes/FlutterUnityWidgetPlugin.h
@@ -1,4 +1,25 @@
+//
+// FlutterUnityWidgetPlugin.h
+// FlutterUnityWidgetPlugin
+//
+// Created by Kris Pypen on 8/1/19.
+//
+
#import
@interface FlutterUnityWidgetPlugin : NSObject
@end
+
+@interface FUController : NSObject
+
+- (instancetype)initWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ registrar:(NSObject *)registrar;
+
+- (UIView*)view;
+@end
+
+@interface FUViewFactory : NSObject
+- (instancetype)initWithRegistrar:(NSObject *)registrar;
+@end
diff --git a/ios/Classes/FlutterUnityWidgetPlugin.m b/ios/Classes/FlutterUnityWidgetPlugin.m
index 2a96730..4febb23 100644
--- a/ios/Classes/FlutterUnityWidgetPlugin.m
+++ b/ios/Classes/FlutterUnityWidgetPlugin.m
@@ -1,20 +1,106 @@
+//
+// FlutterUnityWidgetPlugin.m
+// FlutterUnityWidgetPlugin
+//
+// Created by Kris Pypen on 8/1/19.
+//
+
#import "FlutterUnityWidgetPlugin.h"
-#import
+#import "UnityUtils.h"
+#import "FlutterUnityView.h"
-+ (void)registerWithRegistrar:(NSObject*)registrar {
- FlutterNativeWebFactory* webviewFactory =
- [[FlutterNativeWebFactory alloc] initWithMessenger:registrar.messenger];
- [registrar registerViewFactory:webviewFactory withId:@"unity_view"];
-}
-
-
-/*
-#import "FlutterUnityWidgetPlugin.h"
-#import
+#include
@implementation FlutterUnityWidgetPlugin
+ (void)registerWithRegistrar:(NSObject*)registrar {
- [SwiftFlutterUnityWidgetPlugin registerWithRegistrar:registrar];
+ FUViewFactory* fuviewFactory = [[FUViewFactory alloc] initWithRegistrar:registrar];
+ [registrar registerViewFactory:fuviewFactory withId:@"unity_view"];
}
@end
-*/
\ No newline at end of file
+
+@implementation FUViewFactory {
+ NSObject* _registrar;
+}
+- (instancetype)initWithRegistrar:(NSObject*)registrar {
+ self = [super init];
+ if (self) {
+ _registrar = registrar;
+ }
+ return self;
+}
+- (NSObject*)createArgsCodec {
+ return [FlutterStandardMessageCodec sharedInstance];
+}
+
+- (NSObject*)createWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args {
+ FUController* controller = [[FUController alloc] initWithFrame:frame
+ viewIdentifier:viewId
+ arguments:args
+ registrar:_registrar];
+ return controller;
+}
+
+@end
+
+@implementation FUController {
+ FlutterUnityView* _uView;
+ int64_t _viewId;
+ FlutterMethodChannel* _channel;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ registrar:(NSObject*)registrar {
+ if ([super init]) {
+ _viewId = viewId;
+
+ NSString* channelName = [NSString stringWithFormat:@"unity_view_%lld", viewId];
+ _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:registrar.messenger];
+ __weak __typeof__(self) weakSelf = self;
+ [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
+ [weakSelf onMethodCall:call result:result];
+ }];
+
+ }
+ return self;
+}
+
+- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
+ if ([[call method] isEqualToString:@"postMessage"]) {
+ [self postMessage:call result:result];
+ } else {
+ result(FlutterMethodNotImplemented);
+ }
+}
+
+- (void)postMessage:(FlutterMethodCall*)call result:(FlutterResult)result {
+ NSString* object = [call arguments][@"gameObject"];
+ NSString* method = [call arguments][@"methodName"];
+ NSString* message = [call arguments][@"message"];
+
+ UnityPostMessage(object, method, message);
+
+ result(nil);
+}
+
+- (UIView*)view {
+ _uView = [[FlutterUnityView alloc] init];
+ if ([UnityUtils isUnityReady]) {
+ [_uView setUnityView: (UIView*)[GetAppController() unityView]];
+ } else {
+ [UnityUtils createPlayer:^{
+ [_uView setUnityView: (UIView*)[GetAppController() unityView]];
+ }];
+ }
+ return _uView;
+}
+
+@end
+
+
+
+
+
diff --git a/ios/Classes/UnityUtils.h b/ios/Classes/UnityUtils.h
index a0513cd..31fdca5 100644
--- a/ios/Classes/UnityUtils.h
+++ b/ios/Classes/UnityUtils.h
@@ -1,27 +1,23 @@
-//
-// Created by rex on 15/03/2019.
-//
-
-#ifndef FLUTTER_UNITY_WIDGET_UNITYUTILS_H
-#define FLUTTER_UNITY_WIDGET_UNITYUTILS_H
-
#import
+#ifndef UnityUtils_h
+#define UnityUtils_h
+
#ifdef __cplusplus
extern "C" {
#endif
-void InitArgs(int argc, char* argv[]);
+ void InitArgs(int argc, char* argv[]);
-bool UnityIsInited(void);
+ bool UnityIsInited(void);
-void InitUnity();
+ void InitUnity();
-void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message);
+ void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message);
-void UnityPauseCommand();
+ void UnityPauseCommand();
-void UnityResumeCommand();
+ void UnityResumeCommand();
#ifdef __cplusplus
} // extern "C"
@@ -40,5 +36,4 @@ void UnityResumeCommand();
@end
-#endif //FLUTTER_UNITY_WIDGET_UNITYUTILS_H
-
+#endif /* UnityUtils_h */
diff --git a/ios/Classes/UnityUtils.mm b/ios/Classes/UnityUtils.mm
index 8c152ee..7d59064 100644
--- a/ios/Classes/UnityUtils.mm
+++ b/ios/Classes/UnityUtils.mm
@@ -1,10 +1,8 @@
-#include "RegisterMonoModules.h"
-#include "RegisterFeatures.h"
#include
#import
-#import "UnityInterface.h"
#import "UnityUtils.h"
-#import "UnityAppController.h"
+
+#include
// Hack to work around iOS SDK 4.3 linker problem
// we need at least one __TEXT, __const section entry in main application .o files
@@ -18,6 +16,8 @@ char** g_argv;
void UnityInitTrampoline();
+UnityFramework* ufw;
+
extern "C" void InitArgs(int argc, char* argv[])
{
g_argc = argc;
@@ -29,6 +29,19 @@ extern "C" bool UnityIsInited()
return unity_inited;
}
+UnityFramework* UnityFrameworkLoad()
+{
+ NSString* bundlePath = nil;
+ bundlePath = [[NSBundle mainBundle] bundlePath];
+ bundlePath = [bundlePath stringByAppendingString: @"/Frameworks/UnityFramework.framework"];
+
+ NSBundle* bundle = [NSBundle bundleWithPath: bundlePath];
+ if ([bundle isLoaded] == false) [bundle load];
+
+ UnityFramework* ufw = [bundle.principalClass getInstance];
+ return ufw;
+}
+
extern "C" void InitUnity()
{
if (unity_inited) {
@@ -36,41 +49,30 @@ extern "C" void InitUnity()
}
unity_inited = true;
- UnityInitStartupTime();
+ ufw = UnityFrameworkLoad();
- @autoreleasepool
- {
- UnityInitTrampoline();
- UnityInitRuntime(g_argc, g_argv);
-
- RegisterMonoModules();
- NSLog(@"-> registered mono modules %p\n", &constsection);
- RegisterFeatures();
-
- // iOS terminates open sockets when an application enters background mode.
- // The next write to any of such socket causes SIGPIPE signal being raised,
- // even if the request has been done from scripting side. This disables the
- // signal and allows Mono to throw a proper C# exception.
- std::signal(SIGPIPE, SIG_IGN);
- }
+ [ufw setDataBundleId: "com.unity3d.framework"];
+ [ufw frameworkWarmup: g_argc argv: g_argv];
}
extern "C" void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message)
{
- UnitySendMessage([gameObject UTF8String], [methodName UTF8String], [message UTF8String]);
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [ufw sendMessageToGOWithName:[gameObject UTF8String] functionName:[methodName UTF8String] message:[message UTF8String]];
+ });
}
extern "C" void UnityPauseCommand()
{
dispatch_async(dispatch_get_main_queue(), ^{
- UnityPause(1);
+ [ufw pause:true];
});
}
extern "C" void UnityResumeCommand()
{
dispatch_async(dispatch_get_main_queue(), ^{
- UnityPause(0);
+ [ufw pause:false];
});
}
diff --git a/ios/flutter_unity_widget.podspec b/ios/flutter_unity_widget.podspec
index 910e54c..ea1a834 100644
--- a/ios/flutter_unity_widget.podspec
+++ b/ios/flutter_unity_widget.podspec
@@ -15,7 +15,11 @@ Flutter unity 3D widget for embedding unity in flutter
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
+ s.frameworks = 'UnityFramework'
s.ios.deployment_target = '8.0'
+ s.xcconfig = {
+ 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/../UnityExport" "${PODS_ROOT}/../.symlinks/flutter/ios-release" "${PODS_CONFIGURATION_BUILD_DIR}"',
+ 'OTHER_LDFLAGS' => '$(inherited) -framework UnityFramework ${PODS_LIBRARIES}'
+ }
end
-
diff --git a/preview_android.gif b/preview_android.gif
new file mode 100644
index 0000000..36db7e1
Binary files /dev/null and b/preview_android.gif differ
diff --git a/preview_ios.gif b/preview_ios.gif
new file mode 100644
index 0000000..e28e08a
Binary files /dev/null and b/preview_ios.gif differ
diff --git a/pubspec.yaml b/pubspec.yaml
index aaecc04..cbc51f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,8 +1,10 @@
name: flutter_unity_widget
description: Flutter unity 3D widget for embedding unity in flutter
-version: 0.1.4
+version: 0.1.6+1
authors:
- Rex Raphael
+ - Thomas Stockx
+ - Kris Pypen
homepage: https://github.com/snowballdigital/flutter-unity-view-widget/tree/master
environment:
diff --git a/scripts/Editor/Build.cs b/scripts/Editor/Build.cs
new file mode 100644
index 0000000..281d5c8
--- /dev/null
+++ b/scripts/Editor/Build.cs
@@ -0,0 +1,119 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using UnityEditor;
+using UnityEngine;
+using Application = UnityEngine.Application;
+using BuildResult = UnityEditor.Build.Reporting.BuildResult;
+
+public class Build : MonoBehaviour
+{
+ static readonly string ProjectPath = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
+
+ static readonly string apkPath = Path.Combine(ProjectPath, "Builds/" + Application.productName + ".apk");
+
+ static readonly string androidExportPath = Path.GetFullPath(Path.Combine(ProjectPath, "../../android/UnityExport"));
+ static readonly string iosExportPath = Path.GetFullPath(Path.Combine(ProjectPath, "../../ios/UnityExport"));
+
+ [MenuItem("Flutter/Export Android (Unity 2019.3.*) %&n", false, 1)]
+ public static void DoBuildAndroidLibrary()
+ {
+ DoBuildAndroid(Path.Combine(apkPath, "unityLibrary"));
+
+ // Copy over resources from the launcher module that are used by the library
+ Copy(Path.Combine(apkPath + "/launcher/src/main/res"), Path.Combine(androidExportPath, "src/main/res"));
+ }
+
+ [MenuItem("Flutter/Export Android %&a", false, 2)]
+ public static void DoBuildAndroidLegacy()
+ {
+ DoBuildAndroid(Path.Combine(apkPath, Application.productName));
+ }
+
+ public static void DoBuildAndroid(String buildPath)
+ {
+ if (Directory.Exists(apkPath))
+ Directory.Delete(apkPath, true);
+
+ if (Directory.Exists(androidExportPath))
+ Directory.Delete(androidExportPath, true);
+
+ EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
+
+ var options = BuildOptions.AcceptExternalModificationsToPlayer;
+ var report = BuildPipeline.BuildPlayer(
+ GetEnabledScenes(),
+ apkPath,
+ BuildTarget.Android,
+ options
+ );
+
+ if (report.summary.result != BuildResult.Succeeded)
+ throw new Exception("Build failed");
+
+ Copy(buildPath, androidExportPath);
+
+ // Modify build.gradle
+ var build_file = Path.Combine(androidExportPath, "build.gradle");
+ var build_text = File.ReadAllText(build_file);
+ build_text = build_text.Replace("com.android.application", "com.android.library");
+ build_text = build_text.Replace("implementation fileTree(dir: 'libs', include: ['*.jar'])", "implementation project(':unity-classes')");
+ build_text = Regex.Replace(build_text, @"\n.*applicationId '.+'.*\n", "\n");
+ File.WriteAllText(build_file, build_text);
+
+ // Modify AndroidManifest.xml
+ var manifest_file = Path.Combine(androidExportPath, "src/main/AndroidManifest.xml");
+ var manifest_text = File.ReadAllText(manifest_file);
+ manifest_text = Regex.Replace(manifest_text, @"", "");
+ Regex regex = new Regex(@"(\s|\S)+?", RegexOptions.Multiline);
+ manifest_text = regex.Replace(manifest_text, "");
+ File.WriteAllText(manifest_file, manifest_text);
+ }
+
+ [MenuItem("Flutter/Export IOS (Unity 2019.3.*) %&i", false, 3)]
+ public static void DoBuildIOS()
+ {
+ if (Directory.Exists(iosExportPath))
+ Directory.Delete(iosExportPath, true);
+
+ EditorUserBuildSettings.iOSBuildConfigType = iOSBuildType.Release;
+
+ var options = BuildOptions.AcceptExternalModificationsToPlayer;
+ var report = BuildPipeline.BuildPlayer(
+ GetEnabledScenes(),
+ iosExportPath,
+ BuildTarget.iOS,
+ options
+ );
+
+ if (report.summary.result != BuildResult.Succeeded)
+ throw new Exception("Build failed");
+ }
+
+ static void Copy(string source, string destinationPath)
+ {
+ if (Directory.Exists(destinationPath))
+ Directory.Delete(destinationPath, true);
+
+ Directory.CreateDirectory(destinationPath);
+
+ foreach (string dirPath in Directory.GetDirectories(source, "*",
+ SearchOption.AllDirectories))
+ Directory.CreateDirectory(dirPath.Replace(source, destinationPath));
+
+ foreach (string newPath in Directory.GetFiles(source, "*.*",
+ SearchOption.AllDirectories))
+ File.Copy(newPath, newPath.Replace(source, destinationPath), true);
+ }
+
+ static string[] GetEnabledScenes()
+ {
+ var scenes = EditorBuildSettings.scenes
+ .Where(s => s.enabled)
+ .Select(s => s.path)
+ .ToArray();
+
+ return scenes;
+ }
+}
\ No newline at end of file
diff --git a/scripts/Editor/XCodePostBuild.cs b/scripts/Editor/XCodePostBuild.cs
new file mode 100644
index 0000000..d9176a2
--- /dev/null
+++ b/scripts/Editor/XCodePostBuild.cs
@@ -0,0 +1,333 @@
+/*
+MIT License
+Copyright (c) 2017 Jiulong Wang
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#if UNITY_IOS
+
+using System;
+
+using System.Collections.Generic;
+using System.IO;
+
+using UnityEditor;
+using UnityEditor.Callbacks;
+using UnityEditor.iOS.Xcode;
+
+///
+/// Adding this post build script to Unity project enables the flutter-unity-widget to access it
+///
+public static class XcodePostBuild
+{
+
+ ///
+ /// The identifier added to touched file to avoid double edits when building to existing directory without
+ /// replace existing content.
+ ///
+ private const string TouchedMarker = "https://github.com/snowballdigital/flutter-unity-view-widget";
+
+ [PostProcessBuild]
+ public static void OnPostBuild(BuildTarget target, string pathToBuiltProject)
+ {
+ if (target != BuildTarget.iOS)
+ {
+ return;
+ }
+
+ PatchUnityNativeCode(pathToBuiltProject);
+
+ UpdateUnityProjectFiles(pathToBuiltProject);
+ }
+
+ ///
+ /// We need to add the Data folder to the UnityFramework framework
+ ///
+ private static void UpdateUnityProjectFiles(string pathToBuiltProject)
+ {
+ var pbx = new PBXProject();
+ var pbxPath = Path.Combine(pathToBuiltProject, "Unity-iPhone.xcodeproj/project.pbxproj");
+ pbx.ReadFromFile(pbxPath);
+
+ // Add UnityExport/Data
+ var targetGuid = pbx.TargetGuidByName("UnityFramework");
+ var fileGuid = pbx.AddFolderReference(Path.Combine(pathToBuiltProject, "Data"), "Data");
+ pbx.AddFileToBuild(targetGuid, fileGuid);
+
+ pbx.WriteToFile(pbxPath);
+ }
+
+ ///
+ /// Make necessary changes to Unity build output that enables it to be embedded into existing Xcode project.
+ ///
+ private static void PatchUnityNativeCode(string pathToBuiltProject)
+ {
+ EditUnityFrameworkH(Path.Combine(pathToBuiltProject, "UnityFramework/UnityFramework.h"));
+ EditUnityAppControllerH(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.h"));
+ EditUnityAppControllerMM(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.mm"));
+ EditUnityViewMM(Path.Combine(pathToBuiltProject, "Classes/UI/UnityView.mm"));
+ }
+
+
+ ///
+ /// Edit 'UnityFramework.h': add 'frameworkWarmup'
+ ///
+ private static void EditUnityFrameworkH(string path)
+ {
+ var inScope = false;
+
+ // Add frameworkWarmup method
+ EditCodeFile(path, line =>
+ {
+ inScope |= line.Contains("- (void)runUIApplicationMainWithArgc:");
+
+ if (inScope)
+ {
+ if (line.Trim() == "")
+ {
+ inScope = false;
+
+ return new string[]
+ {
+ "",
+ "// Added by " + TouchedMarker,
+ "- (void)frameworkWarmup:(int)argc argv:(char*[])argv;",
+ ""
+ };
+ }
+ }
+
+ return new string[] { line };
+ });
+ }
+
+ ///
+ /// Edit 'UnityAppController.h': returns 'UnityAppController' from 'AppDelegate' class.
+ ///
+ private static void EditUnityAppControllerH(string path)
+ {
+ var inScope = false;
+ var markerDetected = false;
+
+ // Add static GetAppController
+ EditCodeFile(path, line =>
+ {
+ inScope |= line.Contains("- (void)startUnity:");
+
+ if (inScope)
+ {
+ if (line.Trim() == "")
+ {
+ inScope = false;
+
+ return new string[]
+ {
+ "",
+ "// Added by " + TouchedMarker,
+ "+ (UnityAppController*)GetAppController;",
+ ""
+ };
+ }
+ }
+
+ return new string[] { line };
+ });
+
+ inScope = false;
+ markerDetected = false;
+
+ // Modify inline GetAppController
+ EditCodeFile(path, line =>
+ {
+ inScope |= line.Contains("extern UnityAppController* GetAppController");
+
+ if (inScope && !markerDetected)
+ {
+ if (line.Trim() == "")
+ {
+ inScope = false;
+ markerDetected = true;
+
+ return new string[]
+ {
+ "// }",
+ "",
+ "static inline UnityAppController* GetAppController()",
+ "{",
+ " return [UnityAppController GetAppController];",
+ "}",
+ };
+ }
+
+ return new string[] { "// " + line };
+ }
+
+ return new string[] { line };
+ });
+
+
+ }
+
+ ///
+ /// Edit 'UnityAppController.mm': triggers 'UnityReady' notification after Unity is actually started.
+ ///
+ private static void EditUnityAppControllerMM(string path)
+ {
+ var inScope = false;
+ var markerDetected = false;
+
+ EditCodeFile(path, line =>
+ {
+ if (line.Trim() == "@end")
+ {
+ return new string[]
+ {
+ "",
+ "// Added by " + TouchedMarker,
+ "static UnityAppController *unityAppController = nil;",
+ "",
+ @"+ (UnityAppController*)GetAppController",
+ "{",
+ " static dispatch_once_t onceToken;",
+ " dispatch_once(&onceToken, ^{",
+ " unityAppController = [[self alloc] init];",
+ " });",
+ " return unityAppController;",
+ "}",
+ "",
+ line,
+ };
+ }
+
+ inScope |= line.Contains("- (void)startUnity:");
+ markerDetected |= inScope && line.Contains(TouchedMarker);
+
+ if (inScope && line.Trim() == "}")
+ {
+ inScope = false;
+
+ if (markerDetected)
+ {
+ return new string[] { line };
+ }
+ else
+ {
+ return new string[]
+ {
+ " // Modified by " + TouchedMarker,
+ @" [[NSNotificationCenter defaultCenter] postNotificationName: @""UnityReady"" object:self];",
+ "}",
+ };
+ }
+ }
+
+ return new string[] { line };
+ });
+
+ inScope = false;
+ markerDetected = false;
+
+ // Modify inline GetAppController
+ EditCodeFile(path, line =>
+ {
+ inScope |= line.Contains("UnityAppController* GetAppController()");
+
+ if (inScope && !markerDetected)
+ {
+ if (line.Trim() == "}")
+ {
+ inScope = false;
+ markerDetected = true;
+
+ return new string[]
+ {
+ "",
+ };
+ }
+
+ return new string[] { "// " + line };
+ }
+
+ return new string[] { line };
+ });
+ }
+
+ ///
+ /// Edit 'UnityView.mm': fix the width and height needed for the Metal renderer
+ ///
+ private static void EditUnityViewMM(string path)
+ {
+ var inScope = false;
+
+ // Add frameworkWarmup method
+ EditCodeFile(path, line =>
+ {
+ inScope |= line.Contains("UnityGetRenderingResolution(&requestedW, &requestedH)");
+
+ if (inScope)
+ {
+ if (line.Trim() == "")
+ {
+ inScope = false;
+
+ return new string[]
+ {
+ "",
+ "// Added by " + TouchedMarker,
+ " if (requestedW == 0) {",
+ " requestedW = _surfaceSize.width;",
+ " }",
+ " if (requestedH == 0) {",
+ " requestedH = _surfaceSize.height;",
+ " }",
+ ""
+ };
+ }
+ }
+
+ return new string[] { line };
+ });
+ }
+
+ private static void EditCodeFile(string path, Func> lineHandler)
+ {
+ var bakPath = path + ".bak";
+ if (File.Exists(bakPath))
+ {
+ File.Delete(bakPath);
+ }
+
+ File.Move(path, bakPath);
+
+ using (var reader = File.OpenText(bakPath))
+ using (var stream = File.Create(path))
+ using (var writer = new StreamWriter(stream))
+ {
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ var outputs = lineHandler(line);
+ foreach (var o in outputs)
+ {
+ writer.WriteLine(o);
+ }
+ }
+ }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/workspace.png b/workspace.png
new file mode 100644
index 0000000..4115386
Binary files /dev/null and b/workspace.png differ