Add Edge Support to Unity Ping Pong Demo App

Version: 1.0
Last Modified: 7/30/2020

The Ping Pong application demonstration and server show that the total time of interaction on a fast-paced game may be far below the network latency time to the server.

How-To Goals

  • Add edge support to the mobile ping pong app. This task is located in the NetworkManager.cs file.
  • Understand suggested developer workflow to connect to application backend.
  • Sync and interpolate the ball and paddles to have it act natural.
  • Compare with edge , with no interpolation or hacks.

Requirements & Prerequisites

Note: This document provides instruction for users of a MacOS host development platform. However, the steps described do apply to other development platforms as well.

  • Basic development experience with Unity and C#.
  • Xcode for iOS.
  • Unity installation, with Desktop. Including Android and iOS build targets.
  • Obtain an Apple ID here. Click on Accounts (for the current website).
  • Install Unity Hub here. Use version 2019.2.13f1, or the latest version available)
    • A personal license is provided when the user meets specific conditions. If conditions are not met, a license from Unity will be required.
    • Access the MobiledgeX Unity SDK GitHub repo here
    • Access the empty version of the networked Ping Pong Game here.
      • Access to the completed MobiledgeX enabled Ping Pong Game here.
    • Use Git version control management on the command line.
    • Node.js experience is required if you're working on the WebSocket server code.
      • SSH access to a VM: [email protected]
      • Docker experience is required when working on the server code. Your team has to deploy locally on your machine or upload a new version to your edge server.

About the Ping Pong Application

The Ping Pong Application is built on Unity, and explained in the tutorial found here. It is recommended to review the tutorial if the original code is desired, or if the user is inexperienced with Unity.

Note: The Ping Pong Application does not offer network multiplayer capabilities. Nonetheless, the Ping Pong Application does permit two players to play, where state synchronization operates through a WebSocket server that maintains a minimal game lobby. The Web Socket server supports the timing of the opposing player's ball, and paddles within the rounds played.

The Ping Pong Application WebSocket edge cloudlet server is provided and maintained within a Docker Container. The WebSocket Edge cloudlet server resides with the MatchingEngine SDK, where the WebSocket cloud server locates the nearest cloudlet with FindCloudlet(). With the nearest cloudlet determined, the WebSocket cloud server returns the information to the application for synchronization of play. The Ping Pong Application uses this cloudlet URI to connect to the server.

Tip: The Ping Pong Application possesses an uncomplicated game mechanic, which gives the user the option to play using the original code and also return to the aforementioned multiplayer version.

Example screen 1

In the demonstration application, the green paddles and ball represent the server and player lag that exist within the multiplayer environment operating on an edge server or public cloud.

Example screen 2

Step 1: Get the code

The MobiledgeX edge-cloud-sampleapps repo can be found here.

To retrieve the code:

  1. git clone https://github.com/mobiledgex/edge-cloud-sampleapps.git
  2. cd edge-cloud-sampleapps/unity/PingPongGameExample/PingPongGameSkeleton.

Here is the basic code structure:

PingPongApplicationExample

\__PingPongGameSkeleton - Pong game, without MobiledgeX SDK integration. Open Unity in this directory

  \__Assets\Scripts
      GameManager.cs - Handles game setup and logic. 
      NetworkManager.cs - Handles Integration with MobiledgeX Unity SDK.

  \__ ProjectSettings - This project's Unity specific metadata and project files

\__PingPongGame - Client code completed MobiledgeX SDK integration. (If you skimmed and want to skip the copy and paste sections here).

\__PingPongServer - Server code, this is already installed in the MobiledgeX infrastructure
   \__ yawsps.js - Yet Another WebSocket Pong Server, Dockerfile, Makefile, updated from standard practice to respond to /, which isn't very different from default. Also available if the hackathon is true to its title, and brings the server down from new features added.
   \__ wsclient.js - Just a dummy test client in case there's a shortage of test devices/SIM cards.  
  1. Open this project folder in Unity: edge-cloud-sampleapps/unity/PingPongGameExample/PingPongGameSkeleton.

  2. Load the PingPongApplication scene under Assets>Scenes.

Assets: Scenes

Step 2: Get the MobiledgeX Unity SDK

To get the MobiledgeX Unity SDK, follow this guide here

  1. When you get to the SetUp portion. Use the following credentials (These will be used by MobiledgeXIntegration functions to enable you to connect to your application backend):

    • Organization Name: MobiledgeX-Samples
    • App Name: PingPong
    • App Version: 2.0

    Unity SDK: Credentials

Step 3: Configure Unity

With the MobiledgeX Unity SDK package acquired:

  1. Choose a device build target.
  2. View the device target app signaling details.

Choose a device build target

  1. In the Unity Editor, go to File>Build Settings.
  2. Select Android or iOS, corresponding with the Operating System in use.

Device Build Target

  1. Select Switch Platform to swap device toolchains underneath Unity:

Switch Platform

Device Target App Signing details

  1. Select File>Build Settings. The build settings window will open and in the bottom left click the button Player Settings.

File: Build Settings

  1. Change the bundle identifier by navigating to Project Settings>Player>Other Settings>Identification>Bundler Identifier.

Bundle Identifier

  1. For application signing purposes, create a unique name for the Bundle Identifier for each of the iOS and Android targets. For example: com. <yourCompanyName>.pingponggame.

Add a Location Permission Request

Location services are available for extraction on many target platforms within Unity. Location services are provided in the skeleton app code. Upon first use, a system dialog will appear once the user provides text. Further customization for suitable apps should be applied.

To add a location permissions request:

  1. Navigate to Build settings (Shift + Command + B on macOS).
  2. Go to Player>Other Settings>Configuration>Location Usage Description.

Location Usage Description

Location Services

The LocationService class provides developers easy functions to request permissions, and grab Location objects ready to be used in other MobiledgeX functions. MobiledgeXIntegration functions use this class to grab gps location data to find the nearest cloudlet.

In PingPongSkeleton/Assets/Scripts/NetworkManager.cs in IEnumerator Start() Find and replace:

 yield return  new Exception("LocationService NOT IMPLEMENTED");

With:

 yield return StartCoroutine(MobiledgeX.LocationService.EnsureLocation());

The above code requests location permissions from the user and waits until LocationService is running before moving on.

Note: MobiledgeX.LocationService uses the device Location once during the application lifetime.

Several MobiledgeX functions require gps location:
* Retrieves the device version of GPS. FindCloudlet() is used to locate the app inst on the closest/lowest latency cloudlet.
* Verifies the location (by using the MobiledgeX MatchingEngine SDK ) using Carrier provided location services, in case the location is mocked, either for test and development purposes, or malicious reasons on the device. The MobiledgeX api for that is VerifyLocation.

MobiledgeX Integration

MobiledgeXIntegration is a class made for Unity that wraps the most relevant C# DistributedMatchEngine Namespace functions. It allows the user to specify developer-specific application details as a lookup keyset + Geolocation to pass to the MatchingEngine SDK. It also retrieves information that allows the application to communicate with the appropriate edge server. We will explore how the MobiledgeXIntegration class can help you connect your client side application to your backend.

We will be working in Assets/Scripts/NetworkManager.cs.

At the top of NetworkManager.cs, you will find the following Namespaces:

using MobiledgeX;
using DistributedMatchEngine; 
using DistributedMatchEngine.PerformanceMetrics;

These three namespaces will give you access to the MobiledgeXIntegration class as well as any additional functions/classes in the C# DistributedMatchEngine & DistributedMatchEngine.PerformanceMetrics that may be useful.

We will be making use of two DME (Distributed Matching Engine) APIs: RegisterClient and FindCloudlet. Then we will go through the recommended workflow to connect to your backend and additional useful functions.

Two thin APIs wrappers customized for Unity developers.
* RegisterClient() - Identifies the application details and user, and allows the use of MobiledgeXIntegration functions. Returns true if Organization Name, App Name, and App Version provided in MobiledgeX Package Setup matches an app deployed via MobiledgeX Console.
* FindCloudlet() - Finds the nearest edge application server to communicate with. Returns true if there exists an app instance deployed to a cloudlet. Stores the Cloudlet information in the FindCloudletReply class variable. FindCloudlet can be run in either Proximity (default) or Performance mode. Proximity mode finds the nearest cloudlet based on gps location, while Performance mode tests the latency of each cloudlet to find the app instance with the lowest latency. Performance mode will take a longer time to return. FindCloudlet mode can configured using the UseFindCloudletPerformanceMode(bool performanceMode) function.

For this tutorial, if the user desires a different server that FindCloudlet() is unaware of, change the IP address inside the NetworkManager.cs file and set useAltServer=true. If the user desires a private server to compare how the Ping Pong Application operates with minimum hops between the client and server, the user may run the node.js server directly. Also, the user may install the server inside a Docker container to run locally.

RegisterClient

In the function MobiledgeXAPICalls(), find and replace:

gameManager.clog("RegisterClient NOT IMPLEMENTED");
return;

With:

// Register Client
bool registered = false;
try
{
  registered = await integration.Register();
}
catch (RegisterClientException rce)
{
  gameManager.clog("RegisterClientException: " + rce.Message + ". Make sure OrgName, AppName, and AppVers are correct.");
  return;
}
catch (DmeDnsException de)
{
  // This app should fallback to public cloud, as the DME doesn't exist for your
  // SIM card + carrier.
  gameManager.clog("Cannot register to DME host: " + de.Message + ", Stack: " + de.StackTrace);
  if (de.InnerException != null)
  {
    gameManager.clog("Original Exception: " + de.InnerException.Message);
  }
  // Handle fallback to public cloud application server.
  return;
}
catch (Exception e)
{
  gameManager.clog("Unexpected Exception: " + e.StackTrace);
  return;
}
if (!registered)
{
  gameManager.clog("Unable to Register Client");
  return;
}
gameManager.clog("RegisterClient Successful!!!");

Build and run. The user may use the UnityEditor/Unity Player on the PC development machine to run this by clicking the play button:

Play Button

Click the play button in Unity Editor:

Play Button

In your console logs, you should see: "RegisterClient Successful!!!"

FindCloudlet

Find and replace:

gameManager.clog("FindCloudlet NOT IMPLEMENTED");
return;

With:

// FindCloudlet
bool foundCloudlet = false;
try
{
  foundCloudlet = await integration.FindCloudlet();
}
catch (FindCloudletException fce)
{
  gameManager.clog("FindCloudletException: " + fce.Message + ". Make sure you have an app instance deployed to your region and carrier network");
  return;
}
catch (DmeDnsException de)
{
  // This app should fallback to public cloud, as the DME doesn't exist for your
  // SIM card + carrier.
  gameManager.clog("Cannot register to DME host: " + de.Message + ", Stack: " + de.StackTrace);
  if (de.InnerException != null)
  {
    gameManager.clog("Original Exception: " + de.InnerException.Message);
  }
  // Handle fallback to public cloud application server.
  return;
}
catch (Exception e)
{
  gameManager.clog("Unexpected Exception: " + e.StackTrace);
  return;
}
if (!foundCloudlet)
{
  gameManager.clog("Unable to Find Cloudlet");
  return;
}
gameManager.clog("FindCloudlet Successful!!!");

Click the play button in Unity Editor
In your console logs, you should see: "FindCloudlet Successful!!!"

Connect to Your Edge Server and GetConnection Workflow

The above are examples of how to use individual MobiledgeXIntegration MatchingEngine API calls as needed.

However, we also suggest a simple workflow to easily integrate and connect to your application backend (deployed through MobiledgeX). This will be broken up into 3 steps: RegisterAndFindCloudlet, GetAppPort, and GetConnection.

1. RegisterAndFindCloudlet

MobiledgeXIntegration contains a wrapper function for both RegisterClient and FindCloudlet. Instead of calling each one separately, you can just use RegisterAndFindCloudlet:

// RegisterAndFindCloudlet
bool registeredAndFoundCloudlet
try
{
  registeredAndFoundCloudlet = await integration.RegisterAndFindCloudlet();
}
catch (RegisterClientException rce)
{
  gameManager.clog("RegisterClientException: " + rce.Message + ". Make sure OrgName, AppName, and AppVers are correct.");
  return;
}
catch (FindCloudletException fce)
{
  gameManager.clog("FindCloudletException: " + fce.Message + ". Make sure you have an app instance deployed to your region and carrier network");
  return;
}
catch (DmeDnsException de)
{
  // This app should fallback to public cloud, as the DME doesn't exist for your
  // SIM card + carrier.
  gameManager.clog("Cannot register to DME host: " + de.Message + ", Stack: " + de.StackTrace);
  if (de.InnerException != null)
  {
    gameManager.clog("Original Exception: " + de.InnerException.Message);
  }
  // Handle fallback to public cloud application server.
  return;
}
catch (Exception e)
{
  gameManager.clog("Unexpected Exception: " + e.StackTrace);
  return;
}
if (!registeredAndFoundCloudlet)
{
  gameManager.clog("Unable to Register and Find Cloudlet");
  return;
}
gameManager.clog("RegisterAndFindCloudlet Successful!!!");

If RegisterAndFindCloudlet is successful, MobiledgeXIntegration will have stored FindCloudletReply. This contains information about your backend server: fqdn, ports, etc.

(Note: If you are curious, to inspect this object call "FindCloudletReply reply = integration.FindCloudletReply" and access fields like fqdn, status, etc.)

2. GetAppPort

Next, we need to find a specific port exposed on our backend.
Find and Replace:

clog("GetAppPort NOT IMPLEMENTED");
return;

With:

// GetAppPort
AppPort appPort;
try
{
  appPort = integration.GetAppPort(LProto.L_PROTO_TCP);
}
catch (AppPortException ape)
{
  gameManager.clog("Unable to get AppPort. AppPortException: " + ape.Message);
  return;
}
if (appPort == null)
{
  gameManager.clog("GetAppPort returned null");
  return;
}

Note: This application only has one TCP port exposed, so we did not specify a port number (GetAppPort grabs the first AppPort it finds). If you have multiple ports, you must specify the port number and protocol specific to the connection you want. (eg. appPort = integration.GetAppPort(LProto.L_PROTO_UDP, 5555)). This allows you to create as many connections as needed.

3. GetConnection

Finally, we have all the information needed to get a connection. This application uses the custom MobiledgeX Websocket Client to send and receive data, so all we need is the L7 url of our application backend.

Find and Replace:

gameManager.clog("GetUrl NOT IMPLEMENTED");
return;

With:

// GetUrl
try
{
  edgeCloudletStr = integration.GetUrl("ws");
  gameManager.clog("Found Cloudlet from DME result: [" + edgeCloudletStr + "]");
}
catch (GetConnectionException gce)
{
  gameManager.clog("Unabled to get url. GetConnectionException " + gce.Message);
  return;
}

The edgeCloudletStr is used in ConnectToServerWithRoomId() function to connect to the PingPong backend.

In our case, this pong server is not just a TCP server, but a WebSocket server. You can append an unencrypted WebSocket protocol in front "ws://" to the desired TCP app port registered with MobiledgeX. For this tutorial, you will get something that looks like this constructed app port: ws://ponggame1-tcp.krakow-main.tmpl.mobiledgex.net:3000]

Build and test (or look at the Unity editor's console tab for Debug.Log output):

Debug.log Output

If you do not have a custom communication client, we recommend using one of the GetConnection functions. MobiledgeXIntegration supports a GetWebsocketConnection function, which connects a WebSocketClient object to the developer's backend and hands back the client ready to send and receive data. Other GetConnection functions (GetTCPConnection, GetUDPConnection, and GetHTTPClient are available in the DistributedMatchEngine namespace: eg. integration.matchingEngine.GetTCPConnection())

Additional Classes and Functions

NetTest

The DistributedMatchEngine namespace contains a useful class to test latency of different cloudlets or your own servers.

The user may add the servers to the netTest object's site list in NetworkManager.cs. Once servers are added, periodic tests of the roundtrip time may be performed between each server during game play. Additionally, application focus is available in the UnityEditor. And example of how to use NetTest is in the RegisterAndFindCloudlet() function in GameManager.cs.

In NetworkManager.cs, below the GetUrl code, see the following code for how to use NetTest:

// NetTest
netTest = new NetTest(integration.matchingEngine);
foreach (AppPort ap in integration.FindCloudletReply.ports)
{
  gameManager.clog("Port: proto: " + ap.proto + ", prefix: " + ap.fqdn_prefix + ", path_prefix: " + ap.path_prefix + ", port: " + ap.public_port);

  NetTest.Site site;
  // We're looking for one of the TCP app ports:
  if (ap.proto == LProto.L_PROTO_TCP)
  {
    // Add to test targets.
    if (ap.path_prefix == "")
    {
      site = new NetTest.Site {
        host = integration.GetHost(ap),
        port = integration.GetPort(ap)
      };
      site.testType = NetTest.TestType.CONNECT;
    }
    else
    {
      site = new NetTest.Site {
        L7Path = integration.GetUrl("", ap)
      };
      site.testType = NetTest.TestType.CONNECT;
    }
    if (useAltServer)
    {
      site.host = host;
    }
    l7Path = site.L7Path;
    netTest.sites.Enqueue(site);
  }
}
netTest.doTest(true);  

UseWifiOnly

UseWifiOnly is for testing only. In order to have a true edge connection, the device has to run connections over a cellular connection. This function allows developers to test connections in Editor. However, when deployed to a device, the MobiledgeXIntegration will not allow connections to be run over wifi.

Step 4: Build to your target device

To build your target device:

  1. Go to File>Build Settings>Build and Run. (Shift + Command + B).
    The game is not playable until a second player connects to the pong game edge cloudlet, and joins the game match with the same Room ID (any string name).
  2. Type in a string, such as Game1, and it should wait for the other player to join.The minimally viable game lobby on the server will pair the client with another network player.
    The UnityEditor serves as a second client device. The user may play Ping Pong between devices and the development PC if the server deems possible.

Appendix A: Tips and Tricks

  1. There is no physics prediction in the app to aid lower bandwidth or high latency networks.
    1. Try to mask the latency as much as possible
  2. At some point, latency is high enough such that real-time interaction is impossible. The max velocity is too high, or the latency is too high, and the other player will not get an update before the game round is over. Find a gameplay workaround or predictive solution to reduce event notification delays.
  3. It is possible to install the docker image on the local machine running Docker, and adjust the server simulated latency, if not packet delivery consistency:
    yawsps.js on the deployed demo pong server: const SIMULATELAGTIMEINMS = 0
  4. Compare edge to higher latency networks.
  5. Make the game pretty.
  6. Integrate MobiledgeX MatchingEngine SDK into your own app.
  7. Stuff that hasn't been developed or tested (may require developing unimplemented server features)
    1. Change the score keeping scheme: +1 point if you pass to a team mate, -1 if you pass to an opponent.
    2. Add a third or fourth player
    3. Change the paddle shape (curve, broken pattern, air hockey, alter rigidbody2d, etc.)
    4. Create a warp zone in the middle of field that introduces random motion
    5. Progressively accelerate the speed of the ball
    6. Make ball sticky (and double tap to release)
    7. Balls can change properties and break through paddles
    8. Enable the ball to shatter into an array
    9. Add obstacles to the playing field (with possibility to break them)
    10. Leverage AR glasses as an endpoint rather than handsets
    11. A multitouch screen ought to allow setting the paddle angle, right?