IMVU API for Unity

The IMVU API for Unity is a library that allows you to load IMVU's assets (avatars and outfits) from within the Unity game engine via a set of IMVU APIs. There are two main parts to this library: The REST APIs and the AssetLoader.

1. Setup

To work with the API, you need to install the unitypackage, setup the shaders, register your app with IMVU, and setup the login settings. Even if you're just running the demos that come with the API, you still need to do all of these things. If building for Android, an extra step is required to make authorization work, which is detailed below under the Android section.

1.1 Install Package

To install the API, open the IMVUnity.unitypackage or IMVUnityWithPhoton.unitypackage files in Unity. It will add all of the files to your project. Remember, imports are additive in Unity. When upgrading to a new version of a unity package you should first delete the old one.

1.2 Setup Shaders

In order for our avatars to render properly you need to add the IMVU shaders to your Always Loaded Shaders. To do this, open Edit->Project Settings->Graphics, and add the shaders in the IMVUnity/Shaders folder that you intend to use to the Always Included Shaders list. If you're going to use Unlit materials, include the Unlit shaders. If you're going to use Lit materials, include the Lit shaders. If you're going to use PhysicallyLit materials, include the PhysicallyLit shaders. This Unity documentation should prove useful: http://docs.unity3d.com/Manual/class-GraphicsSettings.html

1.3 Register the App

In order for a user to be able to login to your app, you'll need to register the app with IMVU. To do this, go to http://imvu.com/developer . Click New to create a new app. Enter the name of the new app, and optionally provide a short description and an icon. Don't worry about making it perfect, as you can come back and change all of this information at any time. You also need to list all of the IMVU avatar accounts that will be developing or testing the app under the Allowed Users section. This includes your own IMVU account. Until the app is live these are the only accounts that will be able to login to test your app.

Once you submit the app you'll need to add at least one platform that you intend the app to run on. Click the New button next to the app and select a platform, enter a URL, enter a redirect URL and specify the capabilities your application needs in order to function. The available platforms, right now, are Web, iOS and Android. The URL of a web app is the URL where the app is hosted. For an Android or iOS app, it's the app's URL on the appropriate app store. You can put a placeholder URL here, until you have a real one.

The redirect URL is used by the login process to send the authentication token back to your app. For the web platform, you can enter any valid URL for the redirect URL. For iOS and Android you need to specify a URL with a custom protocol. The protocol is the part of the URL that goes before the colon. For normal web URLs, this is http or https. You'll want to invent something unique, because if another app on the same device is trying to use the same protocol for something, this could cause problems. It should ideally reflects the name of your game and your company. For instance, if you're Awesome Co., and your app is Super Great Ball, your custom protocol might be awesomeco-super-great-ball. The redirect URL will need to be a complete URL, so in the above example you might make it awesomeco-super-great-ball://blah/. The part after the double-slash doesn't really matter. Alongside whatever URL you want to use here, also include the URL http://localhost:8888/. This is necessary to permit you to login to this app from the Unity editor. (Note: you can use a port other than 8888, as long as you match the port number in the steps below as well.)

The capabilities specify what your app will be allowed to do with the user's account once they log in. The first time a user logs in to your app, they'll be asked to grant the app permission to perform these capabilities. A very long list of capabilities may be scary to the user, so it's a good idea to only ask for the capabilities you really need. The capabilities are:

  • user:login - You always need this capability. It grants your app permission to login as the user and read their profile data, including their profile look. See section 3.3 UserModel for the distinction between profile look and current look.
  • avatar:read - This grants your app permission to access the user's current look. Note that if you do not have this capability then when you attempt to load the logged in user's avatar, you will get their profile look instead of their current look. See section 3.3 UserModel for the distinction between profile look and current look.
  • outfit:read - This grants your app permission to access the user's saved outfits.
  • user friends:read - This grants your app permission to access the list of the user's friends.
  • feed:read - This grants your app permission to read the user's feed.
  • feed:write - This grants your app permission to post to the user's feed. Note that you will still need the user's explicit permission for each post.

1.4 Login Settings

Once the app is registered on the website, you'll need to go back to Unity and add data from the registration process into your app. From the Window menu select IMVU Settings.

For each of the platforms you registered on the website, fill in the redirect URL you invented for them. The platforms you're not using can be left blank.

The Editor, Windows and OS X platforms are not supported for publishing at this time, which is why they're not present on the website. They exist here purely for development purposes. For now, fill in the http://localhost:8888/ URL for them. The Windows and OS X options are primarily useful for running multiple local copies of your app, when testing networking.

For the App ID, use the large alphanumeric hash associated with the platform. For Editor, Windows and OS X, you can pick the hash from any of the platforms you've registered with your app.

For more info on these settings, see section 5. Settings below.

1.5 Android

In order to authenticate properly on Android, you need to edit the Android manifest file at Assets/Plugins/Android/AndroidManifest.xml to reflect the custom protocol used by the redirect URL. Note again that it's important that this protocol be unique. If there is another app installed on the user's Android device which uses the same protocol, it will prompt the user which app should handle the login, even though only your app will be capable of handling it correctly. Look for these lines in the manifest file:

<!-- Set your app's custom protocol on the next line -->
<data android:scheme="put-protocol-here" />

And change the put-protocol-here string to your custom protocol. So if your redirect URL is awesomeco-super-greeat-ball://blah/, as in the example above, the protocol is awesomeco-super-great-ball, and you'll want to edit it to:

<!-- Set your app's custom protocol on the next line -->
<data android:scheme="awesomeco-super-great-ball" />

2. Example Code

This is a minimal example of using the API to load an avatar, once all the setup above is done.

using IMVU;

class TestLoader : LocalAssetLoader {
    void Start() {
        Imvu.Login().Then(
            userModel => Load(userModel)
        ).Catch(
            error => Debug.LogError(error)
        );
    }
}

The login process is handled for you when you call Imvu.Login(). This returns a Promise<UserModel, Error>. Promises here function very much like JavaScript promises, and they're documented in more detail at the end of this document. Then it calls Load(), a function inherited from LocalAssetLoader, which loads an IMVU avatar from a UserModel onto this GameObject. This returns a Promise<AssetInfo, Error>. The avatar will already be setup to work with Mecanim, so if you've placed an Animator on it it'll already be animating.

If there are any actual errors at either step, it will fall through to the Catch() and log the error.

3. REST

The REST APIs, at their root, are a set of HTTP-based APIs for accessing IMVU. Inside the APIs this is hidden behind a clean, strongly-typed API. Aside from the fact that many operations are asynchronous, you don't have to worry about the fact that there are HTTP requests behind the scenes. The REST APIs are accessed through RestModel and RestCollection classes.

3.1 RestModel Class

A RestModel is a base class for representing a single data node. It is generic over the type it stores. It contains an info member variable of that type. For instance, UserModel is a subclass of RestModel<UserInfo>, and its info member variable will have type UserInfo.

Each subclass of RestModel will also contain getters for each of the other data nodes related to it. These getters will always return another RestModel.

The set of RestModels that exist right now are: * UserModel - Data about an IMVU user. * OutfitModel - Data about a saved outfit. * FeedElementModel - Data about a single feed post. * ProductModel - Data about an IMVU product.

3.2 RestCollection Class

A RestCollection is a base class for representing a list of data nodes. This looks like:

class RestCollection<T> : RestModel<CollectionInfo<T>> {
    public CollectionInfo<T> info;

    public Promise<Unit, Error> NextPage();
    public delegate void AddEventHandler(T model);
    public event AddEventHandler add;
}

The CollectionInfo class is as follows:

class CollectionInfo<T> {
    public List<Promise<T, Error>> items;
    public int totalCount;
}

It's generic over the type of RestModel it stores. Like RestModel, it contains an info member variable, with the type RestCollection, generic over the same type. For example, the FriendCollection inherits from RestColletion<UserModel>, and it's info member variable will have type CollectionInfo<UserModel>.

Note that collections are paginated. The totalCount field represents the total size of the collection, which may be larger than the number currently loaded, which is why it needs to be a separate field instead of just using items.Count. Calling NextPage() will load the next page. The promise it returns will be accepted when the request for the next page is complete and populated into the collection, or rejected if the request fails. There is also the add event, which will be called each time a new element is added to the collection. Currently this only happens during loading. So you can respond to a new page loading either by using the promise returned by NextPage, or one element at a time by using the add event.

The set of RestCollections that exist right now are:

  • FriendCollection - A collection of UserModels, obtained by calling UserModel.GetFriends().
  • OutfitCollection - A collection of OutfitModels, obtained by calling UserModel.GetOutfits().
  • FeedElementCollection - A collection of FeedElementModels, obtained by calling UserModel.GetPersonalFeed(), UserModel.GetSubscribedFeed() or UserModel.GetRecommendedFeed().
  • ProductCollection - A collection of ProductModels, obtained by calling OutfitModel.GetProducts() or AvatarModel.GetProducts().

3.3 UserModel Class

This model represents a user:

class UserModel : RestModel<UserInfo> {
    public UserInfo info;

    // Get the user's friends
    public Promise<FriendCollection, Error> GetFriends(int limit = 0);
    // Get the user's current look
    public Promise<ProductCollection, Error> GetCurrentLook();
    // Get the user's profile look
    public Promise<ProductCollection, Error> GetProfileLook();
    // Get the user's saved outfits
    public Promise<OutfitCollection, Error> GetOutfits();
    // Get the user's personal feed posts
    public Promise<FeedElementCollection, Error> GetPersonalFeed();
    // Get the user's subscribed feed, which is the aggregate of their posts and their friends' posts
    public Promise<FeedElementCollection, Error> GetSubscribedFeed();
    // Get the user's recommended feed, which is a collection of whitelisted posts
    public Promise<FeedElementCollection, Error> GetRecomendedFeed();
}

The limit argument to GetFriends determines the page size of the resulting FriendCollection. The default value of 0 means to use the server's default page size.

The difference between the current look and the profile look is that current look should be used whenever the user is actually present, and the profile look when they're not. The current look reflects what they last put on while using dress up in the IMVU client, website or mobile app, while the profile look is the look they've saved to their profile. Note that the current look can only be accessed on the UserModel for the logged in user. If called on any other user, it will return an error. The only way to access the current look of another user is through the PhotonAssetLoader or UNetAssetLoader, which will synchronize that user's current look when they connect to a synchronous multiplayer experience.

The UserInfo data is as follows:

struct UserInfo {
    public string gender;
    public string country;
    public string state;
    public Uri thumbnailUrl;
    public bool online;
    public string username;
    public int id;
}

You can use the thumbnailUrl URL to load the user's profile image. It's recommended to use the TextureLoader for this purpose.

3.4 OutfitModel class

This model represents an outfit, which is a look plus a name and an image:

class OutfitModel : RestModel<OutfitInfo> {
    // Get the list of products making up the outfit's look
    public Promise<ProductCollection, Error> GetProducts();

    public OutfitInfo info;
}

The OutfitInfo data is as follows:

public struct OutfitInfo {
    public string name;
    public Uri image;
    public List<int> pids;
}

You can use the image URL to load an image of the outfit. It's recommended to use the TextureLoader for this purpose.

3.5 ProductModel class

This model represents an IMVU product:

class ProductModel : RestModel<ProductInfo> {
    public ProductInfo info;
}

The ProductInfo data is a follows:

public struct ProductInfo {
    public int pid;
    public string name;
    public string creatorName;
    public string rating;
    public Uri productPage;
    public Uri creatorPage;
    public string gender;
    public List<string> categories;
    public List<string> types;
    public Uri previewImage;
}

The rating field is one of GA or AP, which determines whether the product is appropriate for all audiences (GA or General Audience), or require an access pass (AP for Access Pass).

The gender and categories fields are set by the creator. They will usually reflect the proper nature of the product, but they aren't guaranteed to be correct.

The types field is a list of additional categories which are set programatically, and these are reliably accurate.

3.6 FeedElementModel Class

This model represents a feed post:

class FeedElementModel : RestModel<FeedElementInfo> {
    public FeedElementInfo info;

    // Get the user who made this post
    public Promise<UserModel, Error> GetUser();
}

The FeedElementInfo is as follows:

struct FeedElementInfo {
    public string time;
    public string type;
    public FeedElementPayloadInfo payload;
}

public struct FeedElementPayloadInfo {
    public int height;
    public int width;
    public Uri url;
    public Uri thumbnailUrl;
    public string message;
}

The type field is one of message or photo. If it's a message post, then the only field of the payload that will be populated is message. If it's a photo post, then all the fields will be populated. You can use the url and thumbnailUrl URLs to load the attached image for a photo post. It's recommended to use the TextureLoader for this purpose.

3.7 FriendCollection Class

This collection represents a user's friend list:

class FriendCollection : RestCollection<UserModel> {
    public CollectionInfo<UserModel> info;
}

3.8 OutfitCollection class

This collection represents a set of saved outfits:

class OutfitCollection : RestCollection<OutfitModel> {
    public CollectionInfo<OutfitModel> info;
}

3.9 FeedElementCollection Class

This collection represents a set of feed posts, generally just called a feed:

class FriendElementCollection : RestCollection<FeedElementModel> {
    public CollectionInfo<FeedElementModel> info;

    // Post an image to this feed
    public Promise<FeedElementModel, Error> PostImage(Texture2D image);
}

Note that PostImage() is only valid on the personal feed of the currently logged-in user. If you call it on any other feed, it will fail. This call will pop up a confirmation dialog showing the user the image you intend to post, so you can not post to the user's feed without their consent. If they choose not to post it, the promise will resolve to an error.

3.10 ProductCollection class

This collection represents the set of products in a particular look:

class ProductCollection {
    public CollectionInfo<ProductModel> info;
}

3.11 Imvu Class

The entry point to the whole API is the Imvu class. For the moment, this has one method:

class Imvu {
    public static Promise<UserModel, Error> Login(bool storeToken = false);
    public static void Logout();
}

The Login() method will handle the whole login process for you. By default, the login token will be stored, so the next time the app is run it will automatically be logged in to the same account. If you don't want this behavior, then pass in false for storeToken.

If you call Login() multiple times, it will always return the same promise, unless the user cancelled or the login failed. This means that if you need the logged-in user's UserModel in multiple places, you can call Login() from each of those places independently. When the user cancels or the login fails, the stored promise is cleared, which allows you to attempt to login again.

The Logout() method will clear the login token. All stored RestModels will stop functioning, and should no longer be used. The next time Login() is called, it will go through the login process again, and return a new UserModel.

4. Asset Loading

In order to load an IMVU asset, you need to create a GameObject and put a LocalAssetLoader on it. If you want this loading to synchronize properly in a multiplayer experience, you'll also need a UNetAssetLoader or a PhotonAssetLoader, depending on whether you're using UNet or Photon for your network layer.

4.1 LocalAssetLoader

The LocalAssetLoader is a MonoBehaviour. If it's placed on a GameObject, you can then call its Load() method to load that asset onto that GameObject. Its signature is:

class LocalAssetLoader : MonoBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

While the LocalAssetLoader can be used directly, it's also common to subclass it, like so:

class MyAvatar : IMVU.LocalAssetLoader {
    void Start() {
        Imvu.Login().Then(
            userModel => Load(userModel, Setup)
        ).Then(
            assetInfo => {
                // do stuff with the avatar
            }
        );
    }
}

4.2 UNetAssetLoader

The UNetAssetLoader has the same API as the LocalAssetLoader:

class UNetAssetLoader : NetworkBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

The main feature of the UnetAssetLoader is that if Load() is called while connected to a UNet server, it will cause the same asset to be loaded on the matching GameObject on all clients.

4.3 PhotonAssetLoader

The PhotonAssetLoader has the same API as the LocalAssetLoader:

class PhotonAssetLoader : Photon.MonoBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

The main feature of the PhotonAssetLoader is that if Load() is called while connected to a Photon server, it will cause the same asset to be loaded on the matching GameObject on all clients.

NOTE: When using the Photon demo you need to apply your Photon AppID to the demo and you need to turn on "Auto-Join Lobby". Go to window->Photon Unity Networking->PUN Wizard->Locate Photon Server Settings. Ensure your Photon AppID is here and "Auto-Join Lobby" is turned on. "Auto-Join Lobby" can also be turned on programmatically. This setting is important to be aware of.

4.4 AssetInfo

The AssetInfo class contains basic information about the asset:

class AssetInfo {
    public List<SkeletonData> skeletons;
}

The skeletons field is a list of the skeletons on the asset. The first is always the root skeleton. After that come the skeletons of each of the attachments. The SkeletonData provides the following:

public class SkeletonData {
    public Transform root;
    public Transform[] bones;

    // Check if a bone exists, by name
    public bool HasBone(string bname);
    // Get a bone, by name
    public Transform GetBone(string bname);
    // Get the index of a bone, by name
    public int GetBoneIndex(string bname);
}

This allows you to attach geometries or additional behaviours to bones. For instance, it's used heavily by the Equip demo, to attach equipment to bones, or the Ragdoll demo, to attach physics to bones.

4.5 LoadOptions

The LoadOptions class can be provided as an optional argument to all of the various Load functions on LocalAssetLoader, UNetAssetLoader and PhotonAssetLoader, in order to override the normal default load settings. It's constructor looks like:

public LoadOptions(MaterialType? materialType = null, List<LodSetting> lodSettings = null);

Since these are both optional arguments, you can specify only the subset you wish to change, as so:

LoadOptions(materialType: MaterialType.Unlit);
LoadOptions(lodSettings: new List<LodSetting> { new LodSetting(LodLevel.Low, 100) });
LoadOptions(
    materialType: MaterialType.Unlit,
    lodSettings: new List<LodSetting> { new LodSetting(LodLevel.Low, 100) });

4.6 Level of Detail

The API supports loading asset meshes at several different levels of detail and can set up loaded GameObjects to automatically switch between them depending on the percentage of screen space that they occupy. This lets you display assets with dense meshes when close up, when detail is needed, but then switch to meshes with fewer triangles when at a distance, to improve drawing performance.

By default, they are configured to load each GameObject with several levels of detail and automatically switch between them, which has obvious benefits. However, there are cases when you may want to specify custom level-of-detail settings:

  • If you specify fewer levels of detail than the default, the load will make fewer network requests for mesh data, saving some loading time.
  • If you specify just one level of detail, the load can omit the LodComponent that does the automated switching between levels, saving some per-frame CPU overhead.
  • Custom settings also let you tune the default transition points for display quality or performance.

You can customize the default level of detail settings in the IMVU Settings window, or override the defaults at run time, as detailed in section 5.3 Load Settings. You can also use the LoadOptions described in section 4.5 LoadOptions to override the defaults on a specific asset.

Custom level-of-detail settings are specified by a list of LodSetting objects. Each LodSetting object specifies a LodLevel and a maximum screen coverage percentage. There are four LodLevels available:

  • LodLevel.Maximum - The most detailed mesh, as originally authored.
  • LodLevel.Medium - An automatically generated mesh with lower detail (about 50%).
  • LodLevel.Low - An automatically generated mesh with very low detail (about 10%).
  • LodLevel.Hidden - Displays no meshes at all.

Note that the actual amount of detail reduction depends on the geometry of the mesh. For some models, reduction may not be possible, in which case the original mesh is used for all levels.

Screen coverage percentage is expressed as a float from 0-100. At runtime, the screen coverage is estimated from the bounding boxes of the meshes, and the level with the smallest coverage value greater than the object's current coverage (capped to 100%) is used. If you do not specify levels up to 100% coverage, the maximum level will be automatically included.

For example, if we were loading an asset that we know will never be displayed in detail, we could save some network bandwidth by omitting the maximum level when loading, using these LoD settings:

new List<LodSetting>() {
    new LodSetting(LodLevel.Hidden, 0.01f),
    new LodSetting(LodLevel.Low, 1.0f),
    new LodSetting(LodLevel.Medium, 100.0f),
}

Or, if we were loading an asset to only be displayed in detail, we could save some network requests and runtime CPU by loading just the maximum level, with these LoD settings:

new List<LodSetting>() {
    new LodSetting(LodLevel.Maximum, 100.0f),
}

4.7 Material Types

There are three different types of shaders available for the avatars:

  • Unlit - These are the simplest shaders. They have no lighting and no shadows. They will tend to be the fastest.
  • Lit - These shaders use the Unity legacy lighting system. They are lit and recieve and cast shadows.
  • PhysicallyLit - These use Unity 5's new lighting system. They are more advanced, but IMVU avatars can't take advantage of most of the additional features they provide, and there are some bugs in how some avatars render on certain platforms with these shaders. This option is not currently recommended.

You can customize the default material type in the IMVU Settings window, or override the defaults at run time, as detailed in section 5.3 Load Settings. You can also use the LoadOptions described in section 4.5 LoadOptions to override the defaults on a specific asset.

4.8 TextureLoader

The TextureLoader facilitates the loading images over HTTP into a Texture2D. This is easy to do if you use the WWW class directly, but if you use TextureLoader you'll get the benefits of our network stack, which is much more reliable. The TextureLoader API is as follows:

class TextureLoader {
    public Promise<Texture2D, Error> Load(Uri url);
}

5. Settings

The IMVU Settings window can be accessed by going to Window -> IMVU Settings. This window is divided vertically into three sections: the clear buttons, the oauth settings and the load settings.

5.1 Clear Buttons

  • Logout - If you run your app from within the editor, and login, then your login token will be stored in the local store, via Unity's PlayerPrefs. This is normally a convenience, as it means you don't have to login again each time you play. However, if you need to clear that login data, you can come here and press the Logout button.
  • Clear Load Settings - There are calls you can make at runtime, which are detailed below, which allow you to change the default load settings. When you do so, you can choose to have those detaults stored in the local store, via Unity's PlayerPrefs, to override the defaults configured in this window. That still works when running the app in the editor. This button will clear those defaults.

5.2 OAuth Settings

This lists a set of platforms, and for each platform you can provide an App ID and a Redirect URL.

  • App ID - The ID of the app you wish to identify as. This can be garnered from the developer page, as detailed in Setup above. Note that there is a different App ID for each platform. For iOS, Android or WebGL, be sure to use App IDs from those platforms. The Editor, Windows and OS X builds exist only for development and debugging purposes, and should get the App ID associated with whichever platform you're focusing on developing for at the time.
  • Redirect URL - The URL that the OAuth token will be forwarded to after the user logs in. This needs to be one of the URLs configured on for the app on the Developer page. For the Editor, Windows and OS X, the redirect URL needs to be http://localhost:XXXX, where the XXXX is a port number. We use 8888, but any number which is not in use on your development machine can work, provided it's consistent with the URL entered on the developer page.

5.3 Load Settings

This specifies the default settings used to load an IMVU asset. Any of these can be overriden when loading specific assets, as described in section 4.5 LoadOptions. The defaults can also be changed at run time, and these changes can optionally be stored into the local store via Unity's PlayerPrefs. This allows you to make these settings configurable by the user, if desired.

See sections 4.6 Level of Detail and 4.7 Material Types for detailed descriptions of what the Default LOD Settings and Default Material Type settings mean.

To change the settings at runtime, these functions are provided in the Settings class:

public static void SetDefaultLodSettings(List<LodSetting> lodSettings, bool save = false);
public static void SetDefaultMaterialType(MaterialType matType, bool save = false);

In both of these, the optional save argument determines whether the change with be saved to the local store. If it's not specified, the change will not be saved.

6. Common Classes

These classes make heavy use of lambdas, which are an important feature of modern C# that is not often used in Unity. To learn more about lambdas in C#, read this: https://msdn.microsoft.com/en-us/library/bb397687.aspx

6.1 Error

The Error class just encodes a human-readable error message with a machine-readable error code, like so:

class Error {
    public string message;
    public string code;

    public Error(string message, string code);
}

The error code is typically in the form <SYSTEM>-<NUM>, where <SYSTEM> is the system the error occurred in, and <NUM> is a number that will uniquely identify the error within that system. For instance, errors coming from the LocalAssetLoader are in the form LAL-01, LAL-02, etc. If the same error occurs in multiple places, they should still have unique numbers, in order to aid debugging.

6.2 Result

A Result represents the result of an operation that can fail. It's generic over two types: a success type and a failure type (called Ok and Err). In order to maintain type safety, the contents of the Result are private. They're accessed through lambdas with the Match(), IfOk() or IfErr() methods.

In proper computer science terms: Result is a sum type, with simulated pattern matching through the Match(), IfOk() and IfErr() methods.

The Match() method, for example, works like this:

IMVU.Result<int, Error> result = FunctionThatMightFail();
result.Match(
    ok => Success(ok),
    error => Debug.LogError(error)
);

The Match() method will call the appropriate lambda, depending on which type of value the Result actually stores. This means you can't accidentally access the wrong value.

There is also a variant of the Match() method which returns a value, like so:

IMVU.Result<int, Error> result = FunctionThatMightFail();
int output = result.Match(
    ok => ok,
    error => { Debug.LogError(error); return 0; }
);

Note that the return types of the lambdas passed to Match() must be the same, because Match() itself can only have one return type.

If you only care about one branch of the Match() you can use IfOk() or IfErr() instead. The first is given a lambda that will only be called if there is an Ok value, and the second a lambda that will only be called if there is an Err value. For example:

IMVU.Result<int, Error> result = FunctionThatMightFail();
result.IfOk(ok => Success(ok));
result.IfErr(error => Debug.LogError(error));

Like with the Match() method, these have variants that will return a value. However, since they're not complete (that is, there isn't a lambda for every case), they also need to provide a default value, which will be returned if the lambda isn't called. Of course, the type of the default value must be the same as the return type of the lambda. For example:

IMVU.Result<int, Error> result = FunctionThatMightFail();
int output = result.IfOk(ok => ok, 0);

You might be wondering why we would use this to return errors, instead of just throwing exceptions. The reason is that exceptions greatly increase the code size of Unity's WebGL builds. When we turn off exceptions we still needed a way to propogate errors. Having adopted Result to do this, we found it a very useful pattern.

The Result class also contains IsOk() and IsErr(), but it's recommended that these not be used outside of tests.

If you find yourself wanting to generate a Result, it has two static factory functions, one that takes an Ok and one that takes an Err. Examples of creating a Result:

var ok = IMVU.Result<int, Error>.Ok(42);
var err = IMVU.Result<int, Error>.ErrError(new Error("Error!", "ERR-01"));

6.3 Promise

A Promise represents a value that may not exist yet. This is designed to function very much like a JavaScript promise, even mirroring its API as closely as possible. Think of it as an asynchronous Result. The difference is that when you give it lambdas to be called for the different types, those lambdas won't be called until the promise is resolved. If the promise is already resolved, they'll be called immediately.

If you want to give the promise just a success callback, you can use the Then() method. For example:

Imvu.Login().Then(
    userModel => doStuffWithUserModel(userModel)
);

The Then() method also has a form that accepts both success and failure callbacks, like so:

Imvu.Login().Then(
    userModel => doStuffWithUserModel(),
    error => Debug.LogError(error)
);

If you only want an error callback, you can use the Catch() method. For example:

Imvu.Login().Catch(
    error => Debug.LogError(error)
);

Note that Then() and Catch() always return another promise. This allows you to chain promises, like so:

Imvu.Login().Then(
    userModel => Debug.Log(userModel.info.username)
).Catch(
    error => Debug.LogError(error)
);

Note that if any of the promises in this chain fail, they will fall through to the Catch() at the end. Successes will likewise fall through if there are multiple Catch() calls chained with a Then() at the end. The rules for what kind of promise is created are a bit complicated, but the results are generally fairly intuitive.

Promise allbacks must return one of three things:

  • Nothing
  • A Result
  • A Promise

If only an Accept callback is provided, then the Reject type of the new Promise is the same as the old Promise. This allows a Reject to cascade through to the next Reject callback. Likewise, if only a Reject callback is provided, then the Accept type of the new Promise is the same as the old Promise. This allows an Accept to cascade through to the next Accept callback. If both callbacks are provided to Then(), then their return types must match, and that type returns the full type of the resulting Promise.

When nothing or a Result is returned, the resulting promise is resolved as soon as the callback completes. If a Promise is returned, then the resulting Promise is resolved whenever this promise resolves. When nothing is returned, the appropriate type in the new promise is a special type called Unit, which will always be null.

Promise also provides a function called Branch, which is useful when you want to do multiple things in one step of a promise chain, and if either of them fail you want to fall through to a common Catch. It's usage is as follows:

Imvu.Login().Branch(
    userModel => Debug.Log("My name is " + userModel.info.username)
    userModel =>
        userModel.GetFriends().Then(
            friendCollection => Debug.Log("I have " + friendCollection.info.TotalCount + " friends")
        )
).Catch(
    error => Debug.LogError(error)
);

Both of the lambdas in Branch will be called with the same value. The promise returned by Branch has a success type of Unit.

Finally, Promise provides a static function called All. This takes a list of Promises of the same type, and returns a Promise of a list of that type. If any one of the passed-in Promises is rejected, the new Promise is rejected with the first rejection value it gets. If they're all accepted, the new Promise is accepted with a List of all their values. For instance, you can take a List<Promise<int, Error>> and turn it into a Promise<List<int>, Error>, like so:

Promise<int, Error>.All(promiseList);

If this all seems complicated, don't worry too much about it. It works out to be pretty intuitive. See the FriendListLoader class used by the Friends demo for an example of all of these features working together.