SDK Migration: v0.0.1 → v0.9.0
Beamable SDK migration
Overview
The purpose of this guide is to demonstrate everything needed for game makers to migrate an existing Beamable project to the latest version of the "Beamable SDK for Unity".
Ensure that the Unity project is version 2019 LTS to 2021 LTS.
Steps
Follow these steps to get started:
The majority of the effort will be in the following steps.
Upgrade Beamable SDK
Backup Your Project!
Ensure that you have a backup of your project, either through source control or otherwise
Step | Detail |
---|---|
| ![]() ![]() |
| ![]() ![]() |
| • Open your Packages/manifest.json file located in the project directory |
"scopedRegistries": [
{
"name": "Beamable",
"url": "https://nexus.beamable.com/nexus/content/repositories/unity",
"scopes": [ "com.beamable" ]
}
],
Step | Detail |
---|---|
| ![]() ![]() |
Do not attempt to log in to Beamable yet!
Step | Detail |
---|---|
| ![]() ![]() |
Using Assembly Definitions?
If you're using assembly definitions, make sure to reference the following Beamable assemblies:
- Unity.Beamable.Runtime.Common
- Unity.Beamable
- Beamable.SmallerJSON
- Beamable.Platform
- Beamable.Service
- Beamable.Config
Step | Detail |
---|---|
| ![]() ![]() |
DisruptorEngine used to expose most of its functionality through the DisruptorEngine.Instance singleton. The style is the same, but now we’ve refactored the namespace to Beamable.API.Instance. Most of the time you’ll need to include a using statement for Beamable, and you’ll be able to write it as API.Instance instead. Either will work.
Old Way | New Way |
---|---|
|
-- or --
|
The DisruptorBeam namespace has been replaced with the namespace Beamable. In most cases, you’ll want two using statements at the top of each file that used to reference DisruptorEngine, and now references Beamable.
Old Way | New Way |
---|---|
|
|
If you are using a Subscribe() call on a Beamable service like Inventory, Announcements, Calendars, Mail, Content, Commerce, etc, you may see an error in the form of:
b.InventoryService.Subscribe(_ => {});
'InventoryService' does not contain a definition for 'Subscribe' and no accessible extension method 'Subscribe' accepting a first argument of type 'InventoryService' could be found (are you missing a using directive or an assembly reference?)
In this case, Make sure you’ve added the Beamable using statement to the top of the broken file. The Subscribe method has been moved to an extension method available in the Beamable namespace:
Old Way | New Way |
---|---|
// Not applicable |
|
Any namespace references to DisruptorBeam.X.Y.Z have now been refactored to Beamable.X.Y.Z. In some cases, like content objects, the namespace has moved into a Common sub namespace. It is highly recommended that you use an IDE to assist in finding the new using statements.
Old Way | New Way |
---|---|
|
|
Some classes have been renamed from DisruptorEngineXYZ to BeamableXYZ, or more likely, just XYZ.
Old Way | New Way |
---|---|
|
|
If you are using the Beamable Commerce feature, you’ll have had to include a custom Payment Delegate file in your code. You can replace that file with this Gist or see the code block below:
using Beamable.Common;
using Debug = UnityEngine.Debug;
#if UNITY_PURCHASING
using System;
using Beamable.Api;
using Beamable.Coroutines;
using Beamable.Platform.SDK;
using Beamable.Api.Payments;
using Beamable.Service;
using Beamable.Spew;
using UnityEngine;
using UnityEngine.Purchasing;
public class PaymentDelegateUnityIAP : MonoBehaviour
{
private static IStoreController m_StoreController; // The Unity Purchasing system.
private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
public void Awake()
{
if (!ServiceManager.Exists<PaymentDelegate>())
{
Debug.Log("Registering UnityIAP Payment Delegate");
ServiceManager.ProvideWithDefaultContainer<PaymentDelegate>(new PaymentDelegateImpl());
}
}
private class PaymentDelegateImpl : IStoreListener, PaymentDelegate, IServiceResolver<PaymentDelegate>
{
static readonly int[] RETRY_DELAYS = { 1, 2, 5, 10, 20 };
private long _txid;
private Action<CompletedTransaction> _success;
private Action<ErrorCode> _fail;
private Action _cancelled;
private Promise<Unit> _initPromise = new Promise<Unit>();
#if SPEW_IAP || SPEW_ALL
/* Time, in seconds, when initialization started */
private float _initTime;
#endif
public Promise<Unit> Initialize()
{
var platform = ServiceManager.Resolve<PlatformService>();
platform.Payments.GetSKUs().Then(rsp =>
{
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach (var sku in rsp.skus.definitions)
{
builder.AddProduct(sku.name, ProductType.Consumable, new IDs()
{
{ sku.productIds.itunes, AppleAppStore.Name },
{ sku.productIds.googleplay, GooglePlay.Name },
});
}
#if SPEW_IAP
_initTime = Time.time;
#endif
// Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
// and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
UnityPurchasing.Initialize(this, builder);
});
return _initPromise;
}
private void ClearCallbacks()
{
_success = null;
_fail = null;
_cancelled = null;
_txid = 0;
}
public string GetLocalizedPrice(string skuSymbol)
{
var product = m_StoreController?.products.WithID(skuSymbol);
return product?.metadata.localizedPriceString ?? "???";
}
public void startPurchase(
string listingSymbol,
string skuSymbol,
Action<CompletedTransaction> success,
Action<ErrorCode> fail,
Action cancelled
)
{
StartPurchase(listingSymbol, skuSymbol)
.Then(tx => success?.Invoke(tx))
.Error(err => fail?.Invoke(err as ErrorCode));
if (cancelled != null) _cancelled += cancelled;
}
public Promise<CompletedTransaction> StartPurchase(string listingSymbol, string skuSymbol)
{
var result = new Promise<CompletedTransaction>();
_txid = 0;
_success = result.CompleteSuccess;
_fail = result.CompleteError;
if (_cancelled == null) _cancelled = () =>
{ result.CompleteError(
new ErrorCode(400, GameSystem.GAME_CLIENT, "Purchase Cancelled"));
};
ServiceManager.Resolve<PlatformService>().Payments.BeginPurchase(listingSymbol).Then(rsp =>
{
_txid = rsp.txid;
m_StoreController.InitiatePurchase(skuSymbol, _txid.ToString());
}).Error(err =>
{
Debug.LogError($"There was an exception making the begin purchase request: {err}");
_fail?.Invoke(err as ErrorCode);
});
return result;
}
// Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
// Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
public void RestorePurchases()
{
// If we are running on an Apple device ...
if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer)
{
// ... begin restoring purchases
InAppPurchaseLogger.Log("RestorePurchases started ...");
// Fetch the Apple store-specific subsystem.
var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
// Begin the asynchronous process of restoring purchases. Expect a confirmation response in
// the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
apple.RestoreTransactions(result => {
// The first phase of restoration. If no more responses are received on ProcessPurchase then
// no purchases are available to be restored.
InAppPurchaseLogger.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
});
}
// Otherwise ...
else
{
// We are not running on an Apple device. No work is necessary to restore purchases.
InAppPurchaseLogger.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
}
}
//
// --- IStoreListener
//
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
// Purchasing has succeeded initializing. Collect our Purchasing references.
InAppPurchaseLogger.Log("OnInitialized: PASS");
// Overall Purchasing system, configured with products for this application.
m_StoreController = controller;
// Store specific subsystem, for accessing device-specific store features.
m_StoreExtensionProvider = extensions;
_initPromise.CompleteSuccess(PromiseBase.Unit);
RestorePurchases();
#if SPEW_IAP || SPEW_ALL
var elapsed = Time.time - _initTime;
InAppPurchaseLogger.LogFormat("Initialization complete, after {0:#,0.000} seconds.", elapsed);
#endif
}
public void OnInitializeFailed(InitializationFailureReason error)
{
// Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
InAppPurchaseLogger.Log("OnInitializeFailed InitializationFailureReason:" + error);
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
string rawReceipt;
if (args.purchasedProduct.hasReceipt)
{
var receipt = JsonUtility.FromJson<UnityPurchaseReceipt>(args.purchasedProduct.receipt);
rawReceipt = receipt.Payload;
InAppPurchaseLogger.Log($"UnityIAP Payload: {receipt.Payload}");
InAppPurchaseLogger.Log($"UnityIAP Raw Receipt: {args.purchasedProduct.receipt}");
}
else
{
rawReceipt = args.purchasedProduct.receipt;
}
var transaction = new CompletedTransaction(
_txid,
rawReceipt,
args.purchasedProduct.metadata.localizedPrice.ToString(),
args.purchasedProduct.metadata.isoCurrencyCode,
"",
""
);
FulfillTransaction(transaction, args.purchasedProduct);
return PurchaseProcessingResult.Pending;
}
private void FulfillTransaction(CompletedTransaction transaction, Product purchasedProduct)
{
ServiceManager.Resolve<PlatformService>().Payments.CompletePurchase(transaction).Then(_ =>
{
m_StoreController.ConfirmPendingPurchase(purchasedProduct);
_success?.Invoke(transaction);
ClearCallbacks();
}).Error(ex =>
{
Debug.LogError($"There was an exception making the complete purchase request: {ex}");
var err = ex as ErrorCode;
if (err == null)
{
return;
}
var retryable = err.Code >= 500 || err.Code == 429 || err.Code == 0; // Server error or rate limiting or network error
if (retryable)
{
ServiceManager.Resolve<CoroutineService>().StartCoroutine(RetryTransaction(transaction, purchasedProduct));
}
else
{
m_StoreController.ConfirmPendingPurchase(purchasedProduct);
_fail?.Invoke(err);
ClearCallbacks();
}
});
}
// Copied from TransactionManager.cs
private System.Collections.IEnumerator RetryTransaction(CompletedTransaction transaction, Product purchasedProduct)
{
// This block should only be hit when the error returned from the request is retryable. This lives down here
// because C# doesn't allow you to yield return from inside a try..catch block.
var waitTime = RETRY_DELAYS[Math.Min(transaction.Retries, RETRY_DELAYS.Length - 1)];
InAppPurchaseLogger.Log($"Got a retryable error from platform. Retrying complete purchase request in {waitTime} seconds.");
// Avoid incrementing the backoff if the device is definitely not connected to the network at all.
// This is narrow, and would still increment if the device is connected, but the internet has other problems
if (Application.internetReachability != NetworkReachability.NotReachable)
{
transaction.Retries += 1;
}
yield return new WaitForSeconds(waitTime);
FulfillTransaction(transaction, purchasedProduct);
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
// A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
// this reason with the user to guide their troubleshooting actions.
InAppPurchaseLogger.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
var platform = ServiceManager.Resolve<PlatformService>();
var reasonInt = (int) failureReason;
if (failureReason == PurchaseFailureReason.UserCancelled)
{
platform.Payments.CancelPurchase(_txid);
_cancelled?.Invoke();
}
else
{
platform.Payments.FailPurchase(_txid, product.definition.storeSpecificId + ":" + failureReason);
var errorCode = new ErrorCode(reasonInt, GameSystem.GAME_CLIENT, failureReason.ToString() + $" ({product.definition.storeSpecificId})");
_fail?.Invoke(errorCode);
}
ClearCallbacks();
}
#region ServiceResolver
void IServiceResolver.OnTeardown()
{
m_StoreController = null;
m_StoreExtensionProvider = null;
}
bool IServiceResolver<PaymentDelegate>.CanResolve() => true;
bool IServiceResolver<PaymentDelegate>.Exists() => true;
PaymentDelegate IServiceResolver<PaymentDelegate>.Resolve() => this;
#endregion
}
}
[Serializable]
public class UnityPurchaseReceipt
{
public string Store;
public string TransactionID;
public string Payload;
}
#endif // UNITY_PURCHASING
If you are using the Beamable Facebook sign-in feature, you’ll have had to create a Facebook wrapper class to connect Facebook and Beamable. You can replace that file with this Gist or see the code block below:
using System.Collections.Generic;
using Beamable.AccountManagement;
using Beamable.Api;
using Beamable.Common.Api.Auth;
using Facebook.Unity;
using UnityEditor;
using UnityEngine;
public class FacebookWrapper : MonoBehaviour
{
private AccountManagementSignals _signals;
private bool _hasAttachedListener;
private void Update()
{
if (!_hasAttachedListener && _signals.ThirdPartyLoginAttempted != null)
{
_signals.ThirdPartyLoginAttempted.AddListener(StartFacebookLogin);
_hasAttachedListener = true;
}
}
// Awake function from Unity's MonoBehavior
void Awake ()
{
_signals = gameObject.AddComponent<AccountManagementSignals>();
if (!FB.IsInitialized) {
// Initialize the Facebook SDK
FB.Init(InitCallback, OnHideUnity);
} else {
// Already initialized, signal an app activation App Event
FB.ActivateApp();
}
}
private void InitCallback ()
{
if (FB.IsInitialized) {
// Signal an app activation App Event
FB.ActivateApp();
// Continue with Facebook SDK
// ...
Debug.Log("FB Didnt die");
} else {
Debug.Log("Failed to Initialize the Facebook SDK");
}
}
private void OnHideUnity (bool isGameShown)
{
if (!isGameShown) {
// Pause the game - we will need to hide
Time.timeScale = 0;
} else {
// Resume the game - we're getting focus again
Time.timeScale = 1;
}
}
private void AuthCallback (ThirdPartyLoginPromise promise, ILoginResult result) {
if (!string.IsNullOrEmpty(result.Error))
{
promise.CompleteError(new ErrorCode(0, GameSystem.GAME_CLIENT, "Facebook Error", result.Error));
return;
}
if (FB.IsLoggedIn) {
// AccessToken class will have session details
var aToken = PlayerSettings.Facebook.Unity.AccessToken.CurrentAccessToken;
// Print current access token's User ID
Debug.Log(aToken.UserId);
// Print current access token's granted permissions
foreach (string perm in aToken.Permissions) {
Debug.Log(perm);
}
promise.CompleteSuccess(new ThirdPartyLoginResponse()
{
AuthToken = aToken.TokenString
});
} else {
Debug.Log("User cancelled login");
promise.CompleteSuccess(ThirdPartyLoginResponse.CANCELLED);
}
}
public void StartFacebookLogin(ThirdPartyLoginPromise promise)
{
if (promise.ThirdParty != AuthThirdParty.Facebook) return;
var perms = new List<string>(){"public_profile", "email"};
FB.LogInWithReadPermissions(perms, result => AuthCallback(promise, result));
}
}
Calendars Service
The Calendars service has been moved to an Experimental namespace
A few content field names have changed.
The Icon field on CurrencyContent and ItemContent has been renamed to icon.
Old Way | New Way |
---|---|
|
|
|
|
|
|
|
|
|
|
Step | Detail |
---|---|
| • In order to move all the data from the old folder to the new one, we’ve created a helper script to automate the process below: |
Use Our Script!
Do not attempt to manually move the contents of the old folder
Create a temporary code file in your /Assets folder called BeamableMigrationHelper.cs. Paste the code from this Gist or see the code block below:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.VersionControl;
using UnityEngine;
public class BeamableMigrationHelper : EditorWindow
{
[MenuItem(
"Window/Beamable/Migration Tool",
priority = 0)]
public static void RunTool(){
Debug.Log($"Starting migration tool.... {BEAMABLE_FOLDER}");
RenameDisruptorEngineToBeamable();
FixContentScriptGuids();
AssetDatabase.Refresh();
}
private static readonly string DISRUPTOR_ENGINE_FOLDER = Path.Combine(Application.dataPath, "DisruptorEngine");
private static readonly string BEAMABLE_FOLDER = Path.Combine(Application.dataPath, "Beamable");
private static readonly ContentGuidFixData[] GuidFixes = new[]
{
new ContentGuidFixData
{
Name = "Item",
NewGuid = "4e65df06d8f8e499788b3ed5b359d54a",
OldGuid = "6e27788e2a314236a140dc4a409b9249"
},
new ContentGuidFixData
{
Name = "Currency",
NewGuid = "7071e886d4789435d9ad361e916b2207",
OldGuid = "5531181e6a08e4d6882d1e0bf64aeb5e"
},
new ContentGuidFixData
{
Name = "Announcements",
NewGuid = "8b8612187b1a246e5b691c90486deb34",
OldGuid = "ec1a162a275224c81b4dc66174e0abfb"
},
new ContentGuidFixData
{
Name = "Calendars",
NewGuid = "0c222bf6b195348e5be37e5cf0169f08",
OldGuid = "485745a1357640d0999ad440992934fb"
},
new ContentGuidFixData
{
Name = "Listings",
NewGuid = "fd68497a79b464de0b4e09ab2d5a5bbf",
OldGuid = "840eefedb06f44a35ba9ffd558ba8840"
},
new ContentGuidFixData
{
Name = "SKU",
NewGuid = "b5bfeeca7cf3246b3a89418e98719de7",
OldGuid = "8813c741626bd459797986cbf75812db"
},
new ContentGuidFixData
{
Name = "Store",
NewGuid = "a658558ea86c749979b310e828e3892e",
OldGuid = "4e4344ba97e6d4cbeb1a7d24ad6f778b"
},
new ContentGuidFixData
{
Name = "Leaderboard",
NewGuid = "ebf776d7c945048daab1e231d27dcdeb",
OldGuid = "7bd5890ede5a415ba93437f0d0ce7914"
},
new ContentGuidFixData
{
Name = "Tournaments",
NewGuid = "6da5f82a5df104fd4b67a842fafa5c5a",
OldGuid = "1d36333db684547d09c3cbbfff05d3e1"
},
new ContentGuidFixData
{
Name = "Events",
NewGuid = "11f962489e5a44d7c877b4a5b71f4d0e",
OldGuid = "e62f0767b71f47740b2ab2b9a03aa03f"
},
new ContentGuidFixData
{
Name = "GameType",
NewGuid = "404f8a85a397642f5b56ce1f2d8cebea",
OldGuid = "3e764f5e52bc4814ad80326864ba4aff"
},
new ContentGuidFixData
{
Name = "Email",
NewGuid = "b5d38e7e955cc47f9933ec9a3a1e2c5a",
OldGuid = "3b9f329a7c08143f19cad1d65673e105"
}
};
static void RenameDisruptorEngineToBeamable()
{
if (!Directory.Exists(DISRUPTOR_ENGINE_FOLDER))
{
Debug.Log("folder already renamed");
return;
}
Debug.Log("renaming folder...");
try
{
DirectoryCopy(DISRUPTOR_ENGINE_FOLDER, BEAMABLE_FOLDER, true);
}
catch (Exception ex)
{
Debug.LogError($"migration failed at rename folder step. error=[{ex.Message}] stack=[{ex.StackTrace}]");
Debug.LogError(ex);
throw;
}
}
static void FixContentScriptGuids()
{
Directory.CreateDirectory(BEAMABLE_FOLDER);
Debug.Log("checking content files");
var filePaths = Directory.GetFiles(BEAMABLE_FOLDER, "*.asset", SearchOption.AllDirectories);
Debug.Log($"found {filePaths.Length} content files");
var modifiedFiles = 0;
foreach (var filePath in filePaths)
{
var text = File.ReadAllText(filePath);
var modifications = new HashSet<string>();
foreach (var fix in GuidFixes)
{
if (text.Contains(fix.OldGuid))
{
text = text.Replace(fix.OldGuid, fix.NewGuid);
modifications.Add(fix.Name);
}
}
if (modifications.Count > 0)
{
modifiedFiles++;
File.WriteAllText(filePath, text);
Debug.Log($"Fixed content file fixes=[{string.Join(",",modifications)}] file=[{filePath}]");
}
}
Debug.Log($"modified {modifiedFiles} files");
}
struct ContentGuidFixData
{
public string Name,OldGuid, NewGuid;
}
private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
{
// Get the subdirectories for the specified directory.
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new DirectoryNotFoundException(
"Source directory does not exist or could not be found: "
+ sourceDirName);
}
DirectoryInfo[] dirs = dir.GetDirectories();
// If the destination directory doesn't exist, create it.
Directory.CreateDirectory(destDirName);
// Get the files in the directory and copy them to the new location.
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(destDirName, file.Name);
var isUsingVersionControl = Provider.enabled && Provider.isActive;
if (isUsingVersionControl)
{
var checkoutTargetFileTask = Provider.Checkout(tempPath, CheckoutMode.Both);
checkoutTargetFileTask.Wait();
if (checkoutTargetFileTask.success)
{
Debug.LogWarning($"Unable to checkout file {tempPath}");
}
var checkoutSourceFileTask = Provider.Checkout(file.Name, CheckoutMode.Both);
checkoutSourceFileTask.Wait();
if (checkoutSourceFileTask.success)
{
Debug.LogWarning($"Unable to checkout file {file.Name}");
}
}
file.CopyTo(tempPath, true);
}
// If copying subdirectories, copy them and their contents to new location.
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(destDirName, subdir.Name);
DirectoryCopy(subdir.FullName, tempPath, copySubDirs);
}
}
}
}
Now, in Unity, you should see a menu option called Migration Tool. Click it. Monitor the logs to make sure no errors are thrown, except for errors of the form “Asset could not be unloaded”.


Use our migration tool to migrate data between folders
Verify Success
Step | Detail |
---|---|
| • Log in to the Beamable Toolbox with your existing credentials |
| • Verify that your content is intact from the Content Manager window |
Delete Those Unnecessary Files
Once you’ve verified that everything is working again, please delete the BeamableMigrationHelper.cs file, and also delete the /Assets/DisruptorEngine folder.
Having Issues?
If you have migration issues, please reach out to us on whatever channel makes the most sense (i.e. Slack, Discord, or email [email protected]). We also have a diagnostic tool that generates a diagnostic file which you can then send to us that will greatly assist us in debugging any issues that arise


Send us diagnostic info to help us debug any issues that arise
Updated about 1 year ago