Sharing Custom Levels With Dreamlo
Unity
Ank'hur is a puzzle game where players slide pieces around a board, with levels being made up of a list of pieces arranged on a grid. A core element of the game I wanted to include was creating custom levels, and I needed a low-cost, reliable way for users to share those levels from within the game.
Dreamlo is a free service that provides highscores and promo codes that can be changed/retrieved with a simple HTTP GET request. Each highscore is stored with two strings (usually used, for example, to store a player name and comment), two integers (for example a score value and a time value), the date the score was uploaded and the index in the leaderboard. We can use these fields to store a serialised version of user-created levels and accompanying meta-data such as the level's name and the number of ‘likes' it has. We'll also need to include the size of the board.
Dreamlo highscores as retrieved in JSON format
Each level in Ank'hur is made up of a list of pieces, and we can form a string from this list by giving each piece a single character for its position and one for its type, starting the string with the width and height of the board:


                    // To keep the level string somewhat readable, limit the characters used to letters and numbers
                    const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
                    // The first two characters are the width and height of the board
                    string levelString = chars[boardWidth] + "" + chars[boardHeight];

                    for (int j = 0; j < boardHeight; j++) {
                        // Row is an array of PieceType values
                        Row row = Rows[j];
                        for (int i = 0; i < boardWidth; i++) {
                            if (row.Pieces[i] != PieceType.Blank) {
                                levelString += chars[j * boardWidth + i];
                                levelString += chars[(int) row.Pieces[i]];
                            }
                        }
                    }
                

When uploading to dreamlo, we'll use this string as the highscore name, which will also ensure there will be no duplicate levels. The second string can be used for the level's name, and one integer (the actual score) for the number of ‘likes' other players have given the level. To upload/retrieve the scores we'll build our URL and send the GET request with UnityWebRequest:


                    // Send the request asynchronously as a coroutine
                    public static IEnumerator UploadLevel(string levelString, string levelName) {
                        string url = "http://dreamlo.com/lb/" + PRIVATE_KEY + "/add/" + levelString + "/0/0/" + levelName;
                        
                        UnityWebRequest request = UnityWebRequest.Get(url);
                        request.timeout = 5;

                        Debug.Log("Sending request to " + request.url);
                        yield return request.SendWebRequest();
                
                        // Error handling
                        if(request.isNetworkError || request.isHttpError) {
                            Debug.LogError(request.error);
                        } else {
                            string result = request.downloadHandler.text;
                            // Parse result
                        }
                    }
                

When retrieving the highscore list we can use SimpleJSON to parse the results into an array of level objects...


                    public static Level[] GetLevels() {
                        string url = "http://dreamlo.com/lb/" + PUBLIC_KEY + "/json";

                        // ... Make GET Request, returning a result string

                        List levels = new List();
                        if(result == null) return levels.ToArray();
                    
                        JSONNode json = JSON.Parse(result);
                        JSONNode entry = json["dreamlo"]["leaderboard"]["entry"];

                        if (entry == null) return levels.ToArray();
                        // Get array from JSON node
                        if (entry.IsArray) {
                            entry = entry.AsArray;
                        } else {
                            JSONArray array = new JSONArray();
                            array.Add("", entry);
                            entry = array;
                        }

                        for (int i = 0; i < entry.Count; i++) {
                            if (i > 10) break;
                
                            JSONNode result = entry[i];
                
                            string name = result["name"].Value;
                            int score = result["score"].AsInt;
                            string info = result["text"].Value;
                
                            // Store values from JSON node in level object
                            Level level = UserLevel.LevelFromString(name);
                            level.Name = info;
                            level.Likes = score;
                
                            levels.Add(level);
                        }

                        return levels.ToArray();
                    }
                

...and deserialise the level string by iterating over pairs of characters, converting the first to a position and the second to a piece type:


                    public static Level LevelFromString(string levelString) {
                        Level level = CreateInstance();

                        // Iterate over every other character in the string to get pairs of characters
                        for (int i = 0; i < levelString.Length; i += 2) {
                            int first = System.Array.IndexOf(chars.ToCharArray(), levelString[i]);
                            int second = System.Array.IndexOf(chars.ToCharArray(), levelString[i + 1]);

                            if (i == 0) {
                                // Store the first pair of characters as the level's width and height
                                level.Width = first;
                                level.Height = second;
                                level.Rows = new Row[level.Height];
                                for (int r = 0; r < level.Rows.Length; r++) {
                                    level.Rows[r] = new Row(level.Width);
                                }
                            } else {
                                // Convert index to coordinates
                                int y = Mathf.FloorToInt((float) first / level.Width);
                                int x = first % level.Width;
                                // Cast character value to PieceType enum
                                level.Rows[y].Pieces[x] = (PieceType) second;
                            }
                        }

                        return level;
                    }
                

Using the number of likes as the actual score means we can easily sort by most popular, but Dreamlo also offers an option to sort by date to get the most recently uploaded levels. Hopefully this has served as an introduction to the versatility of Dreamlo as a content-sharing service and the ways you can leverage platforms to distribute unintended content.

Ank'hur is available now on the Play Store!