Skip to main content

Persistent Storage: Database

Introduction

UnityIcu provides an innovative persistent storage solution that combines blockchain technology with centralized storage.

How It Works

When player data is stored, it is simultaneously written to both a decentralized blockchain network and centralized servers. This dual-storage approach ensures the highest level of data security and recoverability.

With just one line of code, developers can store and retrieve player data at any time. The decentralized storage guarantees that even in the event of catastrophic backend database failure, player data remains secure and fully restorable.

Example Code

1.Get Address

Obtain the server address where storage can be used

What is implemented here is pseudo-code. You can refer to the complete and integrated example on GitHub for learning URL: GITHUB

    private static string _sqlUrl;
private static bool _gettingUrl;

/// <summary>Ensure that _sqlUrl is ready. If not, fetch it from Router once</summary>
private static async Task EnsureSqlUrlAsync()
{
if (!string.IsNullOrEmpty(_sqlUrl) || _gettingUrl)
return;

_gettingUrl = true;
try
{
_sqlUrl = await ServerRouterClient.GetServiceUrlAsync("sql");
Debug.Log($"[Router] sqlUrl = {_sqlUrl}");
}
catch (Exception e)
{
Debug.LogError($"Failed to obtain the SQL URL:{e.Message}");
throw;
}
finally { _gettingUrl = false; }
}
public static class ServerRouterClient
{
private static readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(4) };
private const string RouterBase = "https://main.unity.icu";
private const string Version = "0.1.6";

// Simple memory cache service -> url
private static readonly Dictionary<string, string> cache = new();

public static async Task<string> GetServiceUrlAsync(string service)
{
if (cache.TryGetValue(service, out var url))
return url;

string req = $"{RouterBase}/{service}?version={Version}";
using var resp = await http.GetAsync(req);
if (!resp.IsSuccessStatusCode)
throw new Exception($"Router {resp.StatusCode}");

var json = await resp.Content.ReadAsStringAsync();
var obj = JsonUtility.FromJson<ServerResponse>(json);
if (string.IsNullOrEmpty(obj.targetServer))
throw new Exception("Router returned empty targetServer");

cache[service] = obj.targetServer;
return obj.targetServer;
}

[Serializable] private class ServerResponse { public string targetServer; }
}

2.C# Implementation of SQL Remote Storage Client

    private async Task<string> SendAsync(string jsonData, HttpMethod method)
{
await EnsureSqlUrlAsync(); // key!

var request = new HttpRequestMessage(method, _sqlUrl)
{
Content = new StringContent(jsonData, Encoding.UTF8, "application/json")
};

var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}

//------------------- Public API for storing, retrieving, and deleting-------------------
public IEnumerator StoreData(string key, string value)
{
var payload = JsonConvert.SerializeObject(new
{
apikey = ApiKey,
userId = Main.main.PlayerID,
action = "store",
key,
value
});

var task = SendAsync(payload, HttpMethod.Post);
yield return new WaitUntil(() => task.IsCompleted);

if (task.IsCompletedSuccessfully)
Debug.Log($"StoreData -> {task.Result}");
else
Debug.LogError("StoreData error!");
}

public IEnumerator RetrieveData(string key,
Action<string> onSuccess,
Action onNotFound = null,
Action<string> onError = null)
{
var payload = JsonConvert.SerializeObject(new
{
apikey = ApiKey,
userId = Main.main.PlayerID,
action = "retrieve",
key
});

var task = SendAsync(payload, HttpMethod.Post);
yield return new WaitUntil(() => task.IsCompleted);

if (!task.IsCompletedSuccessfully)
{
onError?.Invoke("HTTP error");
yield break;
}

try
{
var result = JsonConvert.DeserializeObject<RetrieveResult>(task.Result);
if (result.success)
{
if (result.value != null) onSuccess?.Invoke(result.value);
else onNotFound?.Invoke();
}
else onError?.Invoke("Server response success = false");
}
catch (Exception ex)
{
onError?.Invoke(ex.Message);
}
}

public IEnumerator DeleteData(string key, Action onDone = null)
{
var payload = JsonConvert.SerializeObject(new
{
apikey = ApiKey,
userId = Main.main.PlayerID,
action = "delete",
key
});

var task = SendAsync(payload, HttpMethod.Delete);
yield return new WaitUntil(() => task.IsCompleted);

if (task.IsCompletedSuccessfully)
Debug.Log($"DeleteData -> {task.Result}");

onDone?.Invoke();
}

3.Testing Remote Storage Functionality

//save
yield return gameObject.GetComponent<SQLHttpClient>().StoreData(key, value);
//read
yield return sqlClient.RetrieveData(
key,
onSuccess: (value) =>
{
Debug.Log($"Successfully obtained JSON:{value}");

try
{

SaveData save = JsonUtility.FromJson<SaveData>(value);
if (string.IsNullOrEmpty(value))
{
Debug.LogWarning("The returned data is empty and cannot be parsed JSON");
return;
}
Debug.Log($"Archive information":TextName = {save.textName}, Line = {save.line}");

}
catch (Exception ex)
{
Debug.LogError($"Failed to parse JSON:{ex.Message}");
}
},
onNotFound: () =>
{
Debug.LogWarning("No save file found for this key");
},
onError: (err) =>
{
Debug.LogError($"Error:{err}");
}
);


//delete
var task = SendAsync(payload, HttpMethod.Delete);
yield return new WaitUntil(() => task.IsCompleted);

if (task.IsCompletedSuccessfully)
Debug.Log($"DeleteData -> {task.Result}");


Future Updates

We are continuously improving this feature to provide a better experience for developers. Stay tuned for more updates!