Creating a WatchOS companion for your Unity3d Game

Creating a WatchOS companion for your Unity3d Game

Apple Watch is an awesome device, I did not buy one when it first came out in 2015 but waited about one year to buy it and honestly used to say to myself why would I need an Apple Watch if I have my iPhone device with me at all times, but honestly you don't know what you don't know until you experience it which is why I find myself needing my Watch as much as I need my phone now.

So why making this topic, well I'm a hardware guy and of course super passionate about coding, I love every aspect of communicating with hardware from code, and right before I decided to launch Stickman World for iOS there was something inside of me telling me how awesome and geeky would be to launch a companion game app that could just do few simple things for my game. The next day I launched Unity and went to "Build Settings" to find out if there was a WatchOS platform available or some kind of support available for WatchOS, well sadly there wasn't one and there wasn't any documentation anywhere about how I could easily create a companion app with Unity which became somehow my new priority. I tend to try to avoid to do scope creeping and have learned to manage it well but once in a while I get distracted with things like this and not until I find a solution I can rest and sleep very well.

The next day I decided to take a day off from my day job, I went to the library and heads down started reviewing how to create a WatchOS app within XCode. I also remember going through github and downloading few samples WatchOS apps people had created and reviewing their code to find out exactly how the project was organized, what frameworks were used, and how the Storyboard worked for WatchOS which turned out to be very similar in comparison with iOS apps - Storyboard -> ViewController -> Outlets, after realizing how everything worked and pushing few sample apps to my own Apple Watch I was ready for the next step.

Creating a prototype game in Unity plus a WatchOS Bridge (Plugin)

In this phase I started reading about bridging Unity with Objective-C, how was I going to run code that lived in my WatchOS from within Unity. The key here was to be able to play the Unity game and in certain areas of the game communicate with my watch about what was happening in the game. Well it wasn't as easy, the bridge wasn't complicated at all but what every article in Apple recommended to use App Groups to share data, the idea is that your iOS App or game can share its data to all apps that have the same app group capability enabled and the same app group registration in the form of com.mycompany.myappgroupidentifier and any app that registers with that same app group can share its data.

Well that sounded pretty straight forward and I said to myself let's just create a new Unity project that all it had was a button on the screen, this button could generate a GUID when pressed and the it would be displayed on the watch app by using shared data saved by PlayerPrefs........honestly not as easy as I thought it to be and after doing all the steps I mentioned I couldn't ever get the shared data to come across and spent far too many hours trying to make this work which is why I moved on and found something very interesting on a different and more elegant approach.

Refactoring the WatchOS Bridge (Plugin)

When I say elegant I mean using not the saved data as I attempted to use it before, instead what if I could somehow use networking frameworks to pass information back and forth between Unity and the Watch App, well it turns out WatchOS provides a framework called WatchConnectivity which provides a two-way communications between apps and WatchOS apps, so I decided to create a new version of the bridge as shown in the source code below:

Example Source Code used for Bridge (Plugin) between Unity3d and WatchOS Companion App

UnityWatchConnectivity.h

#import <Foundation/Foundation.h>
#import <WatchConnectivity/WatchConnectivity.h>

@interface SDKWatchSessionDelegate : NSObject <WCSessionDelegate>

@end

UnityWatchConnectivity.mm

#import "UnityWatchConnectivity.h"

@implementation SDKWatchSessionDelegate

@end

static SDKWatchSessionDelegate *delegateObject = nil;
static WCSession *session = nil;

// Converts C style string to NSString
NSString* CreateNSString (const char* string)
{
    if (string)
        return [NSString stringWithUTF8String: string];
    else
        return [NSString stringWithUTF8String: ""];
}

extern "C" {

    void _SendMessage (const char* content)
    {
        if (delegateObject == nil){
            delegateObject = [[SDKWatchSessionDelegate alloc] init];
        }

        if(WCSession.isSupported){
            session = WCSession.defaultSession;
            session.delegate = delegateObject;
            [session activateSession];
        }

        [session sendMessage:@{CreateNSString("json") : CreateNSString(content) } replyHandler:nil errorHandler:nil];
    }
    void _RecievedMessage (const char* content)
    {
        NSLog(@"_ReceivedMessage");
    }
}

WatchSDK.cs

using UnityEngine;
using System;
using System.Runtime.InteropServices;

public class WatchSDK
{
    // import a single C-function from our plugin
    [DllImport ("__Internal")]
    private static extern void _SendMessage(string content);

    [DllImport ("__Internal")]
    private static extern void _RecievedMessage(string content);

    // wrap imported C-function to C# method
    public static void SendMessage(string content) {
        #if UNITY_IOS && !UNITY_EDITOR
        _SendMessage(content);
        #endif
    }

    public static void RecievedMessage(string content) {
        #if UNITY_IOS && !UNITY_EDITOR
        _RecievedMessage(content);
        #endif
    }
}

There are three parts to the source code, UnityWatchConnectivity.mm and UnityWatchConnectivity.h files are files that get packaged and are stored inside the Assets/Plugin/iOS directory within Unity, this is the code that provides additional functionality from within Objective-C to the generated XCode Unity project. WatchSDK.cs is the interface between the Objective-C plugin functionality with your Unity source code. A good example on how to use this:

WatchSDK_UI

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

[Serializable]
public class Statistics
{
    public string GUID = "";
}

public class WatchSDK_UI : MonoBehaviour {

    public Rect ButtonDimension;
    public Rect LabelDimension;
    public GUISkin skin;

    private string lastGUIDGenerated = "No Set";

    void OnGUI(){
        if (GUI.Button (ButtonDimension, "Generate GUID", skin.button)) {
            lastGUIDGenerated = System.Guid.NewGuid ().ToString ();
            var s = new Statistics (){ GUID = lastGUIDGenerated };
            var content = JsonUtility.ToJson (s);
            WatchSDK.SendMessage(content);
        }

        GUI.Label (LabelDimension, lastGUIDGenerated, skin.label);
    }
}

WatchSDK_UI demonstrates how to use the WatchSDK to send a message from within Unity to your bridge code and in this case all I do is generate an object called "Data" which has a GUID property, then I serialize the object to JSON format and pass its contents to the SendMessage method, which it then passes its contents back to the bridge and WatchConnectivity WCSession object gets the information and sends a message to the WatchOS session currently connected.

How you would go about setting up Unity and creating your WatchOS Companion

  1. Generate files for UnityWatchConnectivity.mm and UnityWatchConnectivity.h and place them under your Unity Project / Assets / Plugins / iOS
  2. Click on UnityWatchConnectivity.mm and UnityWatchConnectivity.h within Unity and select iOS as the platform - then click on "Rarely used frameworks" and scroll down until you find WatchConnectivity and enable it.
  3. Generate a file for WatchSDK.cs and WatchSDK_UI.cs and place them under a Scripts folder within Unity.
  4. Create a new scene, add a game object, and add WatchSDK_UI script to it.
  5. Build your game in iOS by going to Build Settings and selecting iOS.
  6. Open the XCode generated project / click on file / new / target / and select watchOS / WatchKit App / click on next / name your new Watch App something like "My Game Watch Companion" / uncheck "include notification scene" then hit finish.
  7. On your watch app make sure the Build # version matches the version of your game by going to Build Settings in XCode targets.
  8. Under Build Settings in XCode select watchos under the Supported Platforms for your Watch App and Watch Extension.
  9. At this point you have a Watch App so feel free to add all your UI components to the story board generated and to interact with your Unity game you will need something like this in one of your Watch app view controllers.

GUIDInfoController.m

#import "GUIDInfoController.h"
#import <WatchConnectivity/WatchConnectivity.h>

@interface GUIDInfoController()<WCSessionDelegate>
@property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceLabel *GuidLabel;

@end


@implementation GameInfoController

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

    if(WCSession.isSupported){
        WCSession* session = WCSession.defaultSession;
        session.delegate = self;
        [session activateSession];
    }
}

-(void)sessionWatchStateDidChange:(nonnull WCSession *)session
{
    if(WCSession.isSupported){
        WCSession* session = WCSession.defaultSession;
        session.delegate = self;
        [session activateSession];

    }
}

-(void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error{

}

-(void)sessionDidDeactivate:(WCSession *)session{

}

- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message {

    if(message){
        NSString *contents = [message objectForKey:@"json"];
        NSLog(@"%@",contents);


        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:[contents dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    }
}

- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{

    if(message){
        NSDictionary* response = @{@"response" : [NSString stringWithFormat:@"Message received."]} ;


        if (replyHandler != nil) replyHandler(response);

    }

}

- (void)willActivate {
    // This method is called when watch view controller is about to be visible to user
    [super willActivate];
}

- (void)didDeactivate {
    // This method is called when watch view controller is no longer visible
    [super didDeactivate];
}

@end

Well that summarizes all I had to do to get Unity to communicate with my WatchOS app, and if you have any questions or problems let me know as I will be more than happy to assist you as much as I can.  

Thank you for your time and don't forget to subscribe to my newsletter, also be sure to find me at @dilmerv and stop by and say hi as I love to chat with everyone.

Pingbacks are closed.

Comments
  1. Ravinder Saxena Ravinder Saxena on 07/24/2017 7:26 a.m. #

    Hi Dilmer,

    Thank you so much for this article helping fellow developers like me to understand and make a build for Apple Watch. I followed each and every step mentioned in your article and solved problems related to architectures since XCode was unable to make a build and now I am stuck at only one error and don't know how to resolve it. It is:

    Semantic Issue:
    UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
    }
    Use of undeclared identifier
    UIApplicationMain

    It is in main.mm under int main(int argc, char* argv[])

    If you could help me in resolving the issue I shall be really grateful of you.

    Thank you
    Ravinder

Post your comment