Cloud Save SDK 3.1 - Queries and Access Classes

Last month we released version 3.1 of the Unity Cloud Save SDK which added support for running Queries directly from a game client and for reading and writing to data in different Access Classes.

We just expanded the Unity SDK tutorial for Cloud Save to add examples of how to use Queries and Access Classes with both Player Data and Game Data (using Custom Items).

Queries and Indexes

Cloud Save Queries allows you to define indexes on Player Data and Game Data stored in Cloud Save so you can search / query data using Cloud Code. You must create an index before you save data that can be queried.

You can create and manage indexes in the Unity Cloud Dashboard.

Player Data: Access Classes

There are 3 different Access Classes for Cloud Save Player Data:

  • Default: Readable and writable by the player the data corresponds to
  • Public: Readable by any player, writable by the player the data corresponds to
  • Protected: Readable by the player the data corresponds to, only writeable from a server

Game Data: Access Classes

Cloud Save supports 2 different Access Class levels for Custom Items - these can be for Game Data that does not have to be bound to a player:

  • Default: Readable by any player, writeable only from a server
  • Private: Readable and writeable only from a server

Example

If you want to create a system to allow players to find other players - for example for async matchmaking - you could first create an index for the names of the properties you want to be able to match on (e.g. language, location, etc).

Player 1:

In your game, you could then write data needed for matching to the Public Access Class for the Player, where it can be read (and queried) by other players. In this case, language and a location.

using SaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;

var data = new Dictionary<string, object> { { "language", "FR" }, { "location", "Paris" } };
await CloudSaveService.Instance.Data.Player.SaveAsync(data, new SaveOptions(new PublicWriteAccessClassOptions()));

Player 2:

You can then query from another client to get back a list of Player IDs for players that match a query:

var query = new Query(
  new List<FieldFilter> {
    new FieldFilter("language", "FR", FieldFilter.OpOptions.EQ, true),
    new FieldFilter("location", "Paris", FieldFilter.OpOptions.EQ, true)
  },
  { "location", "name", "avatar" } // List of Public data keys to return along with results
);
var results = await CloudSaveService.Instance.Data.Player.QueryAsync(query);

Debug.Log($"Number of players returned {results.Count}");
results.ForEach(r => {
  Debug.Log($"Player ID: {r.Id}");
  r.Data.ForEach(d => Log($"Key: {d.Key}, Value: {d.Value.GetAsString()}"));
});

You can also use Custom Items to Query Game Data in the same way, or use Unity Cloud Code (or a game server) to query across data in any Access Class, and for any Player or Custom Item.

You can use client side methods to quickly and easily to unlock new functionality without adding any server side logic and you can combine the functionality in Cloud Save with Cloud Code to create full featured systems for guilds, quests, community goals, NPCs, auction houses, inventory management, item trading, mailboxes, loot tables, etc

2 Likes

Hi lainUnity3D,
I’m currently facing a challenge as my game has almost reached the limit of 20 Indexes in Unity Cloud Save. I’m exploring ways to optimize the usage of Indexes, but I’m wondering if there’s a possibility to increase the maximum number of Indexes beyond 20. Is there any guidance or solution available for managing a higher number of Indexes efficiently?

Hi @tranpd ,

Apologies for the delay in following up.

I don’t have much of an answer to this yet but we appreciate the feedback and that the currently limit might be overly restrictive for some use cases - particularly games with complex behaviours - and are reviewing what we can do to provide guidance and on our end to either increase the limit or provide ways to work with it that make running into it less likely.

Having generic names for indexes (e.g. ā€˜Type’, ā€˜ID’, ā€˜Name’) and then using them across multiple objects of a different type can help reduce the number of indexes required when working with Game Data (Custom Items).

If you would like to share any details about the sorts of things you are modelling (Game Data, Player Data, etc) I’d be happy to take a look to either make suggestions or to inform our own thinking about how we can better support the sorts of things you would like to be able to do.

Best regards,

Iain

1 Like

It might be handy to have an option beyond EQ, NE etc for ā€œcontainsā€ or ā€œdoesn’t containā€ for strings. Then I can Serialize a more complex class under a key ā€œGameDataā€ for example and query contains ā€œopponent: nullā€ to find empty games for a lobby.

At the moment, it seems I need to separate our an ā€œopponentā€ key and have its value as empty to do the same job.

@IainUnity3D

Do you have any examples anywhere for querying from within CloudCode? i am having troubles trying to piece it together without documentation. I was trying

_gameApiClient.CloudSaveData.QueryDefaultPlayerDataAsync(context, context.ServiceToken, context.ProjectId, queryIndexBody, default);

But I can’t work out the queryIndexBody.

EDIT -------
I managed to have some success, even if it can be improved. Sharing for other newbies.
Annoyance 1: A limit of 1000 breaks the query. I can’t find documentation to the max limit, just default.
Annoyance2: If your return keys haven’t been updates since you indexed fields, you get a bad request.

public async void QueryPlayerData(IExecutionContext context)
{
    // Set the query (if Tier == Standard)

    FieldFilter filter1 = new FieldFilter("Tier", "Standard", FieldFilter.OpEnum.EQ, true);
    var fields = new List<FieldFilter> { filter1 };
    var returnKeys = new List<string> { "ShortPlayerName" };
    var queryIndexBody = new QueryIndexBody(fields, returnKeys, 0, 100);  

    // Conduct the query
    var  results = await _gameApiClient.CloudSaveData.QueryDefaultPlayerDataAsync(context, context.ServiceToken, context.ProjectId, queryIndexBody, default);

    // Log the number of results
    _logger.LogInformation($"QueryPlayerDataTest results: {results.Data.Results.Count}");

    // Debug each result for checking
    foreach (var result in results.Data.Results)
    {
        _logger.LogInformation($"Player ID: {result.Id}");

        foreach (var item in result.Data)
        {
            _logger.LogInformation($"Key: {item.Key}, Value: {item.Value}");
        }
    }
}

Thanks for the feedback! I don’t have an example of using Queries and Access Classes from Cloud Code that I can link to off hand, but I think one is being worked on at the moment.

  1. Annoyance 1: A limit of 1000 breaks the query. I can’t find documentation to the max limit, just default.

Thanks for flagging that, I’ll make a note to expand on that in the documentation - I appreciate there is some other guidance around limits that are also not covered anywhere (or in some cases, only in the REST API docs).

  1. Annoyance2: If your return keys haven’t been updates since you indexed fields, you get a bad request.

Thanks again for feedback on this, it helps with prioritisation. This is definitely a limitation of the platform we would like to address.

1 Like

That’s really interesting feedback, we have been considering supporting something like this (for example JSONPath style queries for key values, if you are familiar with that). Do you think that would be helpful?

Not familiar sorry, I’m a relative self-taught newbie but learning fast. But anything that can query the stored JSON would be very useful and flexible.

1 Like

Holy cow, #2 ( Annoyance2: If your return keys haven’t been updates since you indexed fields, you get a bad request.) just wasted several hours wondering why this new index was not working… Is there a plan to fix soon-ish? Or is there a programatic way to rebuild the indexes with existing data?

There isn’t currently a way to trigger building an Index from existing data in Cloud Save, an Index must be created before the data is saved for it to be queryable.

It’s possible to write a script to iterate overall players (e.g. using the getPlayersWithItems methods in the REST API and read/write back the data, if you decide you want to make existing data queryable) but there isn’t a method to explicitly trigger building an Index using existing data.

There is some additional work we would need to do to support that functionality, including tracking and surfacing the state of index creation in the Dashboard/CLI/API, deciding how to multiple concurrent requests to create or delete indexes on the same keys when dealing with compound indexes, etc. but we welcome feedback to help us understand how much that’s tripping people in production to help us with prioritisation.

Hi, I am having issues with retrieving any data from the Unity Cloud Save Data service. My Player Data is saved in the Default category.

        [CloudCodeFunction("GetAvailablePlayers")]
        public async Task GetAvailablePlayers(IExecutionContext context, IGameApiClient gameApiClient)
        {
            //var availablePlayers = new List<PlayerInfo>();

            FieldFilter filter1 = new FieldFilter("name", "Arthur", FieldFilter.OpEnum.EQ, true);
            var fields = new List<FieldFilter> { filter1 };
            var returnKeys = new List<string> { "name" };
            var queryIndexBody = new QueryIndexBody(fields, returnKeys, 0, 100);

            var response = await gameApiClient.CloudSaveData.QueryDefaultPlayerDataAsync(
                context,
                context.ServiceToken,
                context.ProjectId,
                queryIndexBody);

            _logger?.LogInformation("Number of available players: {Count}", response.Data.ToJson());
        
            return;
        }

My Player Data JSON is the following:

{
    "armies": 10,
    "gold": 100,
    "hasLord": false,
    "level": 1,
    "lord": "None",
    "magic": 5,
    "name": "Arthur",
    "playerId": "HhImcWD06QmeH0hs7dHdY25hjkkopk",
    "vassals": []
}

Any ideas? Thank you.

Has this been added since september? I’m currently at a crossroads about dismantling some classes into single key-values to play nicely with queries, but it would be real neato if I could avoid that.

Hi there!

We are still exploring this in production, and over the last month we have been trying out different options to help us figure out what combination of options make the most sense to enable the most powerful features that cover the most use cases, with the least complexity for developers.

Async multiplayer and working with inventories (e.g. player inventory, guild banks, NPCs vendors, etc) are use cases we are particularly focused on - I’m always happy to hear about specific use cases or other examples of things developers are trying to do.

Examples of some things we are currently exploring are:

  • Filtering options that work out of the box, with zero configuration
  • Improvements to querying, beyond the type of filtering options currently exposed
    ^ This is particular is probably most relevant to what you are asking about
  • Increasing the limits on the numbers of keys / values and storage on Player Data and Custom Items
  • Expanding what you can do with batch queries (number of keys / across access classes)
  • Expanding Player Data to include Private Data (server read + write only)
  • Helping developers solve for working with data for in-game objects / schemas

We are taking some time to try and get this right as there are a lot of overlapping concerns - these features have functionality that is complementary and is shaping our view on how we can best help developers and to help us understand in what order we should sequence some of these improvements to get there (as well as to understand what any edges / constraints will be).

It will probably be 4-6 weeks before I can give more details on some of these, but it’s very much something we are working on and looking forward to being in developers hands.

Related - I replied similar on this thread today, seems worth linking to for more context:

Best regards,

Iain