Unity & Game Integration Guide
Complete guide for integrating Rivellum blockchain into Unity games and interactive applications.
Table of Contents
- Overview
- Installation
- Quick Start
- Client Configuration
- Wallet & Signing
- Querying Balances
- Building Intents
- Simulating Intents
- Sending Intents
- Event Subscriptions
- Mobile Build Settings
- Sample Scenes
- Best Practices
- Troubleshooting
Overview
The Rivellum Unity SDK (Rivellum.Unity) provides game developers with:
- ā Cross-platform support - Works on Windows, macOS, iOS, Android
- ā Simple async/await APIs - Modern C# patterns for Unity 6.x+
- ā Intent simulation - Test transactions before sending
- ā Event streaming - React to blockchain events in real-time
- ā Flexible signing - Dev keys for testing, external wallet for production
- ā NFT support - Mint, transfer, and query NFTs directly from games
- ā Type-safe builders - Fluent API for constructing intents
Installation
Option 1: Source Integration (Recommended)
- Copy the
rivellum-unity-sdk/src/folder into your Unity project'sAssets/Plugins/directory:
YourUnityProject/
āāā Assets/
ā āāā Plugins/
ā āāā Rivellum.Unity/
ā āāā Models/
ā āāā Signing/
ā āāā Transport/
ā āāā RivellumClient.cs
ā āāā IntentBuilder.cs
ā āāā ...
-
Ensure your Unity project targets .NET Standard 2.1 or .NET 4.x:
- Edit ā Project Settings ā Player ā Other Settings
- Set Api Compatibility Level to
.NET Standard 2.1
-
Install Newtonsoft.Json via Unity Package Manager:
- Window ā Package Manager
- Click + ā Add package from git URL
- Enter:
com.unity.nuget.newtonsoft-json
Option 2: DLL Import
- Build the SDK as a DLL:
cd rivellum-unity-sdk
dotnet build -c Release
- Copy
bin/Release/netstandard2.1/Rivellum.Unity.dllto your Unity project'sAssets/Plugins/folder.
Quick Start
1. Create a RivellumClient
using Rivellum.Unity;
using Rivellum.Unity.Models;
using System.Threading.Tasks;
using UnityEngine;
public class RivellumManager : MonoBehaviour
{
private RivellumClient _client;
async void Start()
{
// Configure client
var config = new RivellumClientConfig
{
NodeUrl = "https://devnet.rivellum.io", // Or your local node
TimeoutMs = 15000,
AutoSimulateBeforeSend = true
};
_client = new RivellumClient(config);
// Fetch balance
var balance = await _client.GetBalanceAsync("0x...");
Debug.Log($"Balance: {balance.GetRivlBalance()} RIVL");
}
void OnDestroy()
{
_client?.Dispose();
}
}
2. Send a Simple Transfer
using Rivellum.Unity.Signing;
async Task SendTransfer()
{
// Create dev signer (testing only!)
var signer = await LocalDevSigner.CreateNewAsync("password");
var senderAddress = signer.GetAddress();
// Get current nonce
var balance = await _client.GetBalanceAsync(senderAddress);
// Build intent
var intent = new IntentBuilder()
.ForSender(senderAddress)
.WithNonce(balance.Nonce)
.Transfer("0x_recipient_address_", 1000000) // 1 RIVL
.Build();
// Send (auto-simulates first if configured)
var result = await _client.SendIntentAsync(intent, signer);
Debug.Log($"Intent sent! ID: {result.IntentId}");
}
Client Configuration
RivellumClientConfig Properties
var config = new RivellumClientConfig
{
// Node RPC endpoint
NodeUrl = "http://localhost:8080",
// Request timeout (ms)
TimeoutMs = 10000,
// Retry attempts for network errors
MaxRetries = 3,
// Initial retry delay (ms), doubles each retry
RetryBackoffMs = 500,
// Auto-simulate before sending
AutoSimulateBeforeSend = true,
// Only send if simulation predicts success
RequireSuccessPrediction = true
};
Custom Logger
public class UnityLogger : IRivellumLogger
{
public void Info(string message) => Debug.Log($"[Rivellum] {message}");
public void Warn(string message) => Debug.LogWarning($"[Rivellum] {message}");
public void Error(string message, Exception ex = null)
=> Debug.LogError($"[Rivellum] {message}\n{ex}");
}
var client = new RivellumClient(config, logger: new UnityLogger());
Wallet & Signing
IIntentSigner Interface
All signing operations use the IIntentSigner interface:
public interface IIntentSigner
{
Task<byte[]> SignIntentAsync(byte[] serializedIntent);
string GetAddress();
}
LocalDevSigner (Development Only)
ā ļø WARNING: LocalDevSigner stores private keys locally. Never use for production player wallets.
// Create new random keypair
var signer = await LocalDevSigner.CreateNewAsync("secure_password");
// Or import from mnemonic
var signer = await LocalDevSigner.ImportFromMnemonicAsync(
"your twelve word mnemonic phrase here...",
"secure_password"
);
string address = signer.GetAddress();
byte[] signature = await signer.SignIntentAsync(intentBytes);
// Lock when done to clear memory
signer.Lock();
ExternalWalletSigner (Production Recommended)
For production games, integrate with natos-wallet:
// Mobile deep link flow (future implementation)
var signer = new ExternalWalletSigner(playerAddress);
// This will open natos-wallet app for signing
var result = await _client.SendIntentAsync(intent, signer);
Integration Flow (Mobile):
- Serialize intent to base64
- Create deep link:
rivellum://sign?intent=<base64>&callback=<your_scheme> - Open link with
Application.OpenURL(deepLink) - Handle callback in Unity with signed intent
- Submit signed intent to network
Status: Stub implementation. Full natos-wallet integration pending API spec.
Querying Balances
Get Account Balance
async Task<BalanceResponse> GetPlayerBalance(string address)
{
var balance = await _client.GetBalanceAsync(address);
// Get RIVL balance
ulong rivl = balance.GetRivlBalance();
// Get specific asset
ulong tokens = balance.GetAssetBalance("0x_custom_token_id_");
// Current nonce (for sending intents)
ulong nonce = balance.Nonce;
return balance;
}
Display in UI
public Text balanceText;
public Text addressText;
async void RefreshBalance()
{
var balance = await GetPlayerBalance(playerAddress);
addressText.text = $"Address: {ShortenAddress(playerAddress)}";
balanceText.text = $"Balance: {FormatRivl(balance.GetRivlBalance())} RIVL";
}
string ShortenAddress(string addr)
=> $"{addr.Substring(0, 6)}...{addr.Substring(addr.Length - 4)}";
string FormatRivl(ulong microRivl)
=> (microRivl / 1_000_000.0).ToString("F2");
Building Intents
IntentBuilder Fluent API
var intent = new IntentBuilder()
.ForSender(senderAddress)
.WithNonce(currentNonce)
.WithMaxFee(2_000_000) // 2 RIVL max fee
// Add actions
.Transfer(recipientAddress, 1_000_000, "RIVL")
.CallContract("GameRewards", "claim_daily", new { player_id = 123 })
.MintNft("my_collection", new Dictionary<string, object>
{
["name"] = "Legendary Sword",
["image"] = "https://cdn.example.com/sword.png",
["attributes"] = new { power = 100, rarity = "legendary" }
})
.Build();
Common Intent Patterns
Transfer Tokens
var intent = new IntentBuilder()
.ForSender(playerAddress)
.WithNonce(nonce)
.Transfer(merchantAddress, 5_000_000, "RIVL") // 5 RIVL
.Build();
Call Game Contract
var intent = new IntentBuilder()
.ForSender(playerAddress)
.WithNonce(nonce)
.CallContract("GameRewards", "claim_reward", new
{
quest_id = "dragon_slayer_01",
reward_type = "gold",
amount = 1000
})
.Build();
Mint Player NFT
var intent = new IntentBuilder()
.ForSender(playerAddress)
.WithNonce(nonce)
.MintNft("player_items", new Dictionary<string, object>
{
["name"] = "Epic Shield",
["description"] = "Forged in dragon fire",
["image"] = "ipfs://QmAbCdEf...",
["attributes"] = new
{
defense = 75,
durability = 100,
enchantment = "fire_resistance"
}
})
.Build();
Transfer NFT
var intent = new IntentBuilder()
.ForSender(playerAddress)
.WithNonce(nonce)
.TransferNft("nft_id_here", recipientAddress)
.Build();
Simulating Intents
Why Simulate?
Simulation predicts:
- ā Will the intent succeed or fail?
- ā How much will it cost in fees?
- ā What state changes will occur?
Simulate Before Sending
async Task<bool> TrySimulate(Intent intent)
{
var simulation = await _client.SimulateIntentAsync(intent);
if (simulation.Success)
{
Debug.Log($"Simulation passed! Estimated fee: {simulation.EstimatedFee} micro-RIVL");
return true;
}
else
{
Debug.LogWarning($"Simulation failed: {simulation.ErrorMessage}");
return false;
}
}
Show Fee Estimate to Player
public Text feeEstimateText;
async void ShowFeeEstimate(Intent intent)
{
var simulation = await _client.SimulateIntentAsync(intent);
if (simulation.Success)
{
float feeRivl = simulation.EstimatedFee / 1_000_000f;
feeEstimateText.text = $"Estimated fee: {feeRivl:F4} RIVL";
}
}
Sending Intents
Method 1: Auto-Simulation (Recommended)
// Config has AutoSimulateBeforeSend = true by default
var result = await _client.SendIntentAsync(intent, signer);
// Automatically simulates first, throws if prediction is failure
Method 2: Manual Simulation + Send
var simulation = await _client.SimulateIntentAsync(intent);
if (simulation.Success)
{
var result = await _client.SendIntentAsync(intent, signer);
Debug.Log($"Sent! Intent ID: {result.IntentId}");
}
Method 3: Combined SimulateAndSend
var result = await _client.SimulateAndSendAsync(
intent,
signer,
requireSuccessPrediction: true
);
if (result.WasSent)
{
Debug.Log($"Intent sent! ID: {result.SendResult.IntentId}");
Debug.Log($"Fee: {result.Simulation.EstimatedFee} micro-RIVL");
}
else
{
Debug.LogWarning($"Not sent: {result.Simulation.ErrorMessage}");
}
Error Handling
try
{
var result = await _client.SendIntentAsync(intent, signer);
}
catch (RivellumRpcException ex)
{
Debug.LogError($"RPC Error: {ex.Message}");
Debug.LogError($"Error Code: {ex.ErrorCode}");
Debug.LogError($"Node Message: {ex.NodeMessage}");
}
Event Subscriptions
Subscribe to Events
using Rivellum.Unity.Models;
void SubscribeToPlayerEvents(string playerAddress)
{
var filter = new EventFilter
{
Addresses = new List<string> { playerAddress },
EventTypes = new List<string> { "transfer", "nft_minted" },
FromHeight = 0 // Start from latest
};
_subscription = _client.SubscribeEvents(filter, OnEvent);
}
void OnEvent(RivellumEvent evt)
{
Debug.Log($"Event: {evt.Type} at height {evt.Height}");
if (evt.Type == "transfer")
{
var amount = evt.Data["amount"];
ShowNotification($"Received {amount} RIVL!");
}
else if (evt.Type == "nft_minted")
{
var nftId = evt.Data["nft_id"];
RefreshPlayerInventory();
}
}
void OnDestroy()
{
_subscription?.Dispose(); // Stop receiving events
}
Filter by Event Type
// Only subscribe to NFT events
var filter = new EventFilter
{
EventTypes = new List<string>
{
"nft_minted",
"nft_transferred",
"nft_burned"
}
};
_subscription = _client.SubscribeEvents(filter, evt =>
{
Debug.Log($"NFT Event: {evt.Type}, NFT ID: {evt.Data["nft_id"]}");
});
Cancel Subscription
// Option 1: Dispose
_subscription.Dispose();
// Option 2: Cancel
_subscription.Cancel();
// Option 3: Using statement (auto-disposes)
using (var sub = _client.SubscribeEvents(filter, OnEvent))
{
// Subscription active within this scope
await Task.Delay(60000); // Listen for 1 minute
} // Auto-cancelled here
Mobile Build Settings
iOS
- Network Permissions: Add to
Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
- URL Scheme (for external wallet integration):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourgame</string>
</array>
</dict>
</array>
Android
- Network Permission: Add to
AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- Clear Text Traffic (for local devnet testing):
<application android:usesCleartextTraffic="true" ... >
- Deep Links: Add intent filter to main activity:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourgame" />
</intent-filter>
Unity Player Settings
- Api Compatibility Level:
.NET Standard 2.1 - Scripting Backend: IL2CPP (recommended for mobile)
- Target Architectures: ARM64 (iOS), ARM64 + ARMv7 (Android)
- Minimum iOS Version: 13.0+
- Minimum Android API: 24+
Sample Scenes
1. Wallet Card Demo
Location: rivellum-unity-sdk/examples/Unity/RivellumWalletDemo/
Features:
- Display player address (shortened)
- Show RIVL balance with refresh button
- List recent intents/transactions
- Simple send RIVL UI
Key Scripts:
WalletCardController.cs- Main controllerBalanceDisplay.cs- Format and update balance UITransactionHistoryItem.cs- Recent activity list item
Setup:
- Open scene in Unity 6.x
- Configure node URL in WalletCardController inspector
- Play in editor or build to device
- Connect to devnet and view wallet
2. Game Rewards Demo
Location: rivellum-unity-sdk/examples/Unity/RivellumGameRewardsDemo/
Features:
- Query GameRewards contract for player points
- Display earned rewards
- Claim rewards via contract call
- Simulate before claiming
Key Scripts:
GameRewardsController.cs- Contract interactionRewardClaimButton.cs- Claim with simulationPointsDisplay.cs- Show player points
Setup:
- Open scene in Unity 6.x
- Deploy GameRewards contract to devnet (see contract examples)
- Configure contract address in inspector
- Play and interact with rewards system
Best Practices
1. Always Use Async/Await Properly
// ā
Good - proper async handling
async void Start()
{
try
{
var balance = await _client.GetBalanceAsync(address);
UpdateUI(balance);
}
catch (Exception ex)
{
Debug.LogError($"Failed to fetch balance: {ex}");
}
}
// ā Bad - blocking the Unity thread
void Start()
{
var balance = _client.GetBalanceAsync(address).Result; // Blocks!
}
2. Cache Balance Queries
private BalanceResponse _cachedBalance;
private float _lastBalanceUpdate;
private const float BALANCE_CACHE_SECONDS = 5f;
async Task<BalanceResponse> GetBalanceWithCache(string address)
{
if (_cachedBalance != null &&
Time.time - _lastBalanceUpdate < BALANCE_CACHE_SECONDS)
{
return _cachedBalance;
}
_cachedBalance = await _client.GetBalanceAsync(address);
_lastBalanceUpdate = Time.time;
return _cachedBalance;
}
3. Handle Network Errors Gracefully
async Task<BalanceResponse?> SafeGetBalance(string address)
{
try
{
return await _client.GetBalanceAsync(address);
}
catch (RivellumRpcException ex)
{
ShowErrorToPlayer($"Network error: {ex.Message}");
return null;
}
}
4. Dispose Resources Properly
public class RivellumManager : MonoBehaviour
{
private RivellumClient _client;
private EventSubscription _subscription;
void OnDestroy()
{
_subscription?.Dispose();
_client?.Dispose();
}
}
5. Never Store Private Keys in PlayerPrefs
// ā BAD - insecure!
PlayerPrefs.SetString("private_key", privateKey);
// ā
GOOD - use external wallet for production
var signer = new ExternalWalletSigner(playerAddress);
// ā
OK - dev signer with warning labels
#if UNITY_EDITOR
var signer = await LocalDevSigner.CreateNewAsync("dev_password");
// NOTE: LocalDevSigner is DEV ONLY, use external wallet in production
#endif
Troubleshooting
Build Errors
Error: The type or namespace 'Newtonsoft' could not be found
Solution: Install Newtonsoft.Json via Package Manager:
com.unity.nuget.newtonsoft-json
Error: Task not found
Solution: Ensure Api Compatibility Level is set to .NET Standard 2.1 or .NET 4.x.
Runtime Errors
Error: RivellumRpcException: Connection refused
Solution: Verify node URL is correct and node is running:
var config = new RivellumClientConfig
{
NodeUrl = "http://localhost:8080" // Check this matches your node
};
Error: Simulation failed: Insufficient balance
Solution: Ensure sender has enough RIVL for amount + fees:
var balance = await _client.GetBalanceAsync(senderAddress);
if (balance.GetRivlBalance() < amount + estimatedFee)
{
Debug.LogWarning("Insufficient balance for transaction");
}
Error: Intent signature verification failed
Solution: Ensure nonce is correct and signer address matches intent sender:
var balance = await _client.GetBalanceAsync(senderAddress);
var intent = new IntentBuilder()
.ForSender(senderAddress)
.WithNonce(balance.Nonce) // Use current nonce
.Transfer(...)
.Build();
Mobile Issues
iOS: App crashes when making HTTP requests
Solution: Add App Transport Security exception to Info.plist (see Mobile Build Settings).
Android: Network calls timeout
Solution: Add INTERNET permission to AndroidManifest.xml (see Mobile Build Settings).
Next Steps
- š Read NFT Standard Documentation
- š Read Account Auth Documentation
- š® Explore sample Unity scenes in
examples/Unity/ - š§ Deploy devnet node for testing: Devnet Guide
- š¬ Join Discord for game dev support: https://discord.gg/rivellum
Questions or Issues?
- GitHub Issues: https://github.com/rivellumlabs/rivellum/issues
- Discord: #game-dev channel
- Email: developers@rivellum.io