Universal contract-based backend communication system for Unity supporting REST APIs, WebGL JavaScript Bridge, Nakama, Mock providers, and runtime texture loading.
- 🔌 Multi-Provider Architecture - REST API, Nakama, JavaScript Bridge, Mock, Web Texture
- 📝 Type-Safe Contracts - Strongly typed request/response handling
- 🔄 Auto Code Generation - Generate C# contracts from Swagger/OpenAPI specs
- 🔀 Runtime Provider Switching - Switch backends dynamically (production/staging/mock)
- 🛠️ Editor Tools - Real-time contract monitoring and OpenAPI generator UI
- 🎯 UniTask Integration - Modern async/await patterns with cancellation support
{
"dependencies": {
"com.unity.addressables": "2.6.0",
"com.unigame.metaservice": "https://github.com/UnioGame/unity.meta.backend.git",
"com.unigame.addressablestools": "https://github.com/UnioGame/unigame.addressables.git",
"com.unigame.contextdata": "https://github.com/UnioGame/unigame.context.git",
"com.unigame.unicore": "https://github.com/UnioGame/unigame.core.git",
"com.unigame.localization": "https://github.com/UnioGame/unigame.localization.git",
"com.unigame.rx": "https://github.com/UnioGame/unigame.rx.git",
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity"
}
}graph TB
A[Your Game Code] --> B[IBackendMetaService]
B --> C[Contract Registry]
B --> D[Provider Manager]
D --> E[Web Provider]
D --> F[Nakama Provider]
D --> G[Mock Provider]
D --> H[JS Bridge Provider]
D --> I[Web Texture Provider]
E --> J[REST API]
F --> K[Nakama Server]
G --> L[Local Mock Data]
H --> M[Browser JavaScript]
I --> N[CDN/Web Images]
C --> O[Contract Metadata]
O --> P[ContractsConfigurationAsset]
style B fill:#4a9eff
style E fill:#66bb6a
style F fill:#66bb6a
style G fill:#66bb6a
style H fill:#66bb6a
style I fill:#66bb6a
Menu: Assets/UniGame/Meta Service/Create Configuration
This creates a ContractsConfigurationAsset that manages all backend contracts and providers.
using Game.Modules.WebProvider.Contracts;
using UniGame.MetaBackend.Runtime.WebService;
[Serializable]
public class GetUserProfileContract : RestContract<UserProfileRequest, UserProfileResponse>
{
public override string Path => "api/user/profile";
public override WebRequestType RequestType => WebRequestType.Get;
}
[Serializable]
public class UserProfileRequest
{
public string userId;
}
[Serializable]
public class UserProfileResponse
{
public string name;
public string email;
public int level;
}using Extensions;
public async UniTask LoadUserProfile()
{
var contract = new GetUserProfileContract
{
input = new UserProfileRequest { userId = "12345" }
};
var result = await contract.ExecuteAsync<UserProfileResponse>();
if (result.success)
{
Debug.Log($"Welcome, {result.data.name}!");
}
else
{
Debug.LogError($"Failed: {result.error}");
}
}Contracts define the communication interface between your game and backend services.
public interface IRemoteMetaContract
{
object Payload { get; } // Request data
string Path { get; } // Endpoint path
Type OutputType { get; } // Response type
Type InputType { get; } // Request type
}public abstract class RemoteMetaContract<TInput, TOutput> : IRemoteMetaContract
{
public virtual Type InputType => typeof(TInput);
public virtual Type OutputType => typeof(TOutput);
public abstract object Payload { get; }
public abstract string Path { get; }
}// Full request/response
public class SimpleMetaContract<TInput, TOutput> : RemoteMetaContract<TInput, TOutput>
{
[SerializeField] public TInput inputData;
[SerializeField] public string path;
}
// Input only (response is string)
public class SimpleInputContract<TInput> : SimpleMetaContract<TInput, string>
// Output only (request is string)
public class SimpleOutputContract<TOutput> : SimpleMetaContract<string, TOutput>Providers handle the actual communication with different backend systems.
Main service interface managing all providers:
public interface IBackendMetaService : ILifeTimeContext
{
// Execute contract
UniTask<ContractDataResult> ExecuteAsync(IRemoteMetaContract contract, CancellationToken cancellationToken = default);
// Switch active provider
void SwitchProvider(int providerId);
// Get specific provider
IRemoteMetaProvider GetProvider(int id);
// Observable stream of all results
Observable<ContractDataResult> DataStream { get; }
// Current configuration
IRemoteMetaDataConfiguration MetaDataConfiguration { get; }
}// Using BackendMetaSource asset
[CreateAssetMenu(menuName = "UniGame/MetaBackend/Backend Meta Source")]
public class BackendMetaSource : DataSourceAsset<IBackendMetaService>
{
// Configure in inspector
}
// Or manually
var service = new BackendMetaService(
useDefaultProvider: true,
historySize: 100,
defaultMetaProvider: BackendTypeIds.WebProvider,
metaProviders: providerDictionary,
metaDataConfiguration: configuration
);Switch between different providers at runtime:
// Switch to production
backendService.SwitchProvider(BackendTypeIds.ProductionProvider);
// Switch to mock for testing
backendService.SwitchProvider(BackendTypeIds.MockProvider);
// Get specific provider
var webProvider = backendService.GetProvider(BackendTypeIds.WebProvider);HTTP/HTTPS REST API communication.
[Serializable]
public class LoginContract : RestContract<LoginRequest, LoginResponse>
{
public override string Path => "auth/login";
public override WebRequestType RequestType => WebRequestType.Post;
}public enum WebRequestType
{
None,
Get,
Post,
Put,
Delete,
Patch
}public class WebMetaProviderSettings
{
public string defaultUrl = "https://api.example.com";
public string defaultToken = "";
public int requestTimeout = 30;
public bool debugMode = false;
public List<WebApiEndPoint> contracts = new();
}var webProvider = backendService.GetProvider(BackendTypeIds.WebProvider) as IWebMetaProvider;
webProvider.SetToken("your-bearer-token");// Contract with typed error
public class UserContract : RestContract<UserRequest, UserResponse, ErrorResponse>
{
// Implementation
}
// Execute with error handling
var result = await contract.ExecuteAsync<UserResponse, ErrorResponse>();
if (!result.success)
{
var error = result.errorData; // Typed ErrorResponse
Debug.LogError($"Error: {error.message}");
}Load Web Provider configuration from StreamingAssets in builds:
// In WebMetaProviderSettings
settings.useStreamingSettings = true;
settings.useStreamingUnderEditor = false; // Only in builds
// Save to StreamingAssets
webProviderAsset.SaveSettingsToStreamingAsset();
// Settings loaded from: StreamingAssets/web_meta_provider_settings.jsonIntegration with Nakama server.
public interface INakamaService : IRemoteMetaProvider, IGameService
{
UniTask<NakamaConnectionResult> SignInAsync(
INakamaAuthenticateData authenticateData,
CancellationToken cancellationToken = default);
UniTask<bool> SignOutAsync();
}For testing without real backend.
[Serializable]
public class MockBackendData
{
public string Method; // Contract method name
public bool Success; // Mock success/failure
public string Result; // Mock response (JSON)
public string Error; // Mock error message
}
[Serializable]
public class MockBackendDataConfig
{
public bool allowConnect = true;
public List<MockBackendData> mockBackendData = new();
}var mockData = new MockBackendData
{
Method = "GetUserProfile",
Success = true,
Result = JsonConvert.SerializeObject(new UserProfile
{
name = "Test User",
level = 10
}),
Error = ""
};For WebGL builds to communicate with browser JavaScript.
public class JsApiContract : IRemoteMetaContract
{
public string apiMethod;
public object requestData;
public object Payload => requestData;
public string Path => apiMethod;
}window.JsBridge_Agent = {
SendMessage: function(contractId, message) {
console.log('From Unity:', contractId, message);
// Process and respond
var response = { result: "success" };
gameInstance.SendMessage('JsBridge_Agent', 'InvokeReceiveMessage',
JSON.stringify(response));
}
};Load textures and sprites from URLs at runtime.
var textureContract = new WebTexture2DContract
{
name = "profile-avatar",
lifeTime = this.GetAssetLifeTime()
};
var result = await textureContract.ExecuteAsync<Texture2D>();
if (result.success)
{
myRenderer.material.mainTexture = result.data;
}var spriteContract = new WebSpriteContract
{
name = "ui-icon",
lifeTime = this.GetAssetLifeTime()
};
var result = await spriteContract.ExecuteAsync<Sprite>();
if (result.success)
{
myImage.sprite = result.data;
}[Serializable]
public class WebTextureSettings
{
public string url = "";
public bool useCache = true;
public List<WebTexturePath> textures = new();
}
[Serializable]
public class WebTexturePath
{
public string name;
public string url;
}Automatically generate C# contracts and DTOs from OpenAPI 3.0 or Swagger 2.0 specifications with full support for composition schemas (allOf/anyOf/oneOf), multiple content types, and servers.
Menu: UniGame > Meta Backend > OpenApi Generator
The visual editor provides:
- Settings asset management
- API specification configuration (supports both Swagger 2.0 and OpenAPI 3.0)
- Output folder configuration
- Generation options
- One-click generation
- Servers Array - Automatic base URL extraction from servers configuration
- Multiple Content Types - Handles application/json, application/xml, multipart/form-data
- requestBody - Full support for OpenAPI 3.0 request bodies
- components/schemas - Modern schema references
- allOf - Schema inheritance and merging
- anyOf - Union types
- oneOf - Discriminated unions with discriminator support
- Polymorphism - Type discrimination for complex hierarchies
- Extended Formats - email, uri, hostname, ipv4, ipv6, uuid, date-time
- Enum Generation - C# enums from OpenAPI enum values
- Deprecated Attributes - [Obsolete] for deprecated properties and operations
- Nullable Types - Proper handling of nullable properties
- Swagger 2.0 - Full support for definitions, basePath, consumes/produces
- Automatic Detection - Seamlessly works with both specifications
[Serializable]
public class OpenApiSettings
{
public string apiJsonPath; // Path to OpenAPI JSON
public string contractsOutFolder; // Contracts output folder
public string dtoOutFolder; // DTOs output folder
public string contractNamespace; // Generated code namespace
public string apiTemplate = "api/{0}"; // URL template
public string[] apiAllowedPaths; // Filter endpoints
public bool cleanUpOnGenerate = false; // Clean before generate
public bool useResponseDataContainer; // Handle wrapped responses
public string responseDataField = "data";
}var settings = new OpenApiSettings
{
apiJsonPath = "path/to/openapi.json",
contractsOutFolder = "Assets/Generated/Contracts/",
dtoOutFolder = "Assets/Generated/DTOs/",
contractNamespace = "Game.Generated.WebContracts",
apiTemplate = "api/{0}",
cleanUpOnGenerate = true
};
WebApiGenerator.GenerateContracts(settings);// Generated from OpenAPI spec
namespace Game.Generated.WebContracts
{
[Serializable]
public class GetUserByIdContract : RestContract<GetUserByIdRequest, UserDTO>
{
public override string Path => "api/users/{id}";
public override WebRequestType RequestType => WebRequestType.Get;
}
}namespace Game.Generated.WebContracts.DTO
{
[Serializable]
public class UserDTO
{
[JsonProperty("id")]
[field: SerializeField]
public int Id { get; set; }
[JsonProperty("name")]
[field: SerializeField]
public string Name { get; set; }
[JsonProperty("email")]
[field: SerializeField]
public string Email { get; set; }
}
}[Serializable]
public class GetStoreItemsContract : RestContract<StoreRequest, StoreResponse>
{
public string categoryId = "weapons";
public string itemId = "sword_01";
public override string Path => "api/store/{categoryId}/items/{itemId}";
// Resolves to: "api/store/weapons/items/sword_01"
}For APIs that wrap responses:
{
"data": {
"userId": 123,
"name": "Player"
},
"success": true
}Enable in settings:
settings.useResponseDataContainer = true;
settings.responseDataField = "data";Generated contract:
public class GetUserContract : RestContract<GetUserRequest, ResponseDataDTO<UserDTO>>
{
// Container automatically unwrapped
}Subscribe to all backend errors:
backendService.DataStream
.Where(result => !result.success)
.Subscribe(result =>
{
Debug.LogError($"Backend Error: {result.error}");
ShowErrorPopup(result.error);
})
.AddTo(lifeTime);Add custom processing for contracts:
public interface IMetaContractHandler
{
UniTask<bool> HandleAsync(MetaContractData data, CancellationToken cancellationToken);
}
// Add handler
backendService.AddContractHandler(myCustomHandler);
// Remove handler
backendService.RemoveContractHandler<MyCustomHandler>();Menu: UniGame > Meta Backend > Meta Editor Window
Real-time monitoring and debugging of contract execution:
- Configuration Panel - View all registered contracts and settings
- Execution History - Full history with search and filtering
- Real-time Stream - Live updates as contracts execute
- Export Functionality - Export history to JSON
Features:
- Auto-refresh every 500ms
- Success/failure indicators
- Search by meta ID, type, or error
- Copy contract results
- Connection status indicator
Menu: UniGame > Meta Backend > OpenApi Generator
Visual interface for generating contracts:
- Settings asset management
- API specification browser
- Output configuration
- Generation options
- One-click contract generation
Both windows work together seamlessly:
- Generate contracts with OpenAPI Generator
- Monitor execution in Contract Monitor
- Update contract list in
ContractsConfigurationAsset
// Group contracts by feature
namespace Game.Contracts.User
{
public class LoginContract : RestContract<...> { }
public class GetProfileContract : RestContract<...> { }
}
namespace Game.Contracts.Store
{
public class GetItemsContract : RestContract<...> { }
public class PurchaseContract : RestContract<...> { }
}// Always handle errors
var result = await contract.ExecuteAsync<ResponseType>();
if (!result.success)
{
// Log error
Debug.LogError($"Contract failed: {result.error}");
// Show user-friendly message
ShowErrorMessage("Connection failed. Please try again.");
return;
}
// Process successful result
ProcessData(result.data);private CancellationTokenSource _cts;
public async UniTask LoadData()
{
_cts = new CancellationTokenSource();
try
{
var result = await contract.ExecuteAsync<DataType>(_cts.Token);
ProcessResult(result);
}
catch (OperationCanceledException)
{
Debug.Log("Operation cancelled");
}
}
public void CancelOperation()
{
_cts?.Cancel();
}// Use different providers per environment
#if DEVELOPMENT
var providerId = BackendTypeIds.MockProvider;
#elif STAGING
var providerId = BackendTypeIds.StagingProvider;
#else
var providerId = BackendTypeIds.ProductionProvider;
#endif
backendService.SwitchProvider(providerId);// Set up mock data for testing
var mockConfig = new MockBackendDataConfig
{
allowConnect = true,
mockBackendData = new List<MockBackendData>
{
new MockBackendData
{
Method = "GetUserProfile",
Success = true,
Result = JsonConvert.SerializeObject(testProfile)
},
new MockBackendData
{
Method = "PurchaseItem",
Success = false,
Error = "Insufficient funds"
}
}
};- Verify
BackendMetaServiceis initialized - Check that contract is registered in
ContractsConfigurationAsset - Ensure the correct provider supports the contract type
- Review logs in Contract Monitor window
- Verify URL/endpoint configuration
- Check authentication token if required
- Test with Mock provider to isolate network issues
- Review provider-specific settings
- Validate OpenAPI specification (use Swagger Editor)
- Check namespace configuration
- Verify output folders are writable
- Review OpenAPI Generator settings
- Ensure Play Mode is active
- Verify
BackendMetaService.EditorInstanceis assigned - Check that contracts are actually executing
- Enable Auto Refresh in monitor window
using System;
using Cysharp.Threading.Tasks;
using Extensions;
using Game.Modules.WebProvider.Contracts;
using UniGame.MetaBackend.Runtime.WebService;
using UnityEngine;
namespace Game.Features.Auth
{
// Contract definition
[Serializable]
public class LoginContract : RestContract<LoginRequest, LoginResponse>
{
public override string Path => "auth/login";
public override WebRequestType RequestType => WebRequestType.Post;
}
[Serializable]
public class LoginRequest
{
public string username;
public string password;
}
[Serializable]
public class LoginResponse
{
public string token;
public string userId;
public string displayName;
}
// Service implementation
public class AuthService : MonoBehaviour
{
public async UniTask<bool> LoginAsync(string username, string password)
{
var contract = new LoginContract
{
input = new LoginRequest
{
username = username,
password = password
}
};
var result = await contract.ExecuteAsync<LoginResponse>();
if (result.success)
{
// Store token
PlayerPrefs.SetString("auth_token", result.data.token);
// Set token for future requests
var webProvider = BackendMetaServiceExtensions.RemoteMetaService
.GetProvider(BackendTypeIds.WebProvider) as IWebMetaProvider;
webProvider?.SetToken(result.data.token);
Debug.Log($"Logged in as {result.data.displayName}");
return true;
}
else
{
Debug.LogError($"Login failed: {result.error}");
return false;
}
}
}
}MIT License
Documentation Version: 2025.0.20
Package Version: 2025.0.20
Unity Version: 2023.1+


