Unity combined with flag to realize ranking function

Time:2020-7-14

Amateur do a small game, the leaderboard was originally usedPlayerPrefsStore locally and now want to put the data on the server. Because the function is very simple, we chose the small and exquisite flag to realize.

Talk less. First consider the design of the URL. The charts are nothing more than scoresscoreAccording to the idea of rest, you might as well set the URL to/scores。 useGETTo get the ranking data, usePOSTAdd a new record to the leaderboard. In addition, by convention, the data of the leaderboard does not need to be updated or deleted.

Flash itself does not support rest, but we can use therouteandmethodDo it yourself. Create a prototype version of therank_server.py。 Naming follows the rails habit:

from flask import Flask

app = Flask(__name__)

@app.route('/scores', methods=['GET'])
def index():
     return 'index'

@app.route('/scores', methods=['POST'])
def create():
     return 'create'


if __name__ == '__main__':
     app.run(debug=True)

implementpython rank_server.pyTo start your own server. Let’s installcURLTo test the application.

brew install curl

testGET

`curl -i -X GET 127.0.0.1:5000/scores`

testPOST

`curl -i -X POST 127.0.0.1:5000/scores`

-iParameters can display the header information of the response for debugging.-XParameter specifies the method of the requestmethod
You can see that the test was successful.

Let’s create a table to store the data. For local test, we use SQLite, and then deploy mysql.
Table building documentcreate_rank.sqlThe contents are as follows:

DROP TABLE IF EXISTS rank;
CREATE TABLE rank(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(255) NOT NULL,
    score INTEGER NOT NULL
);

The MAC comes with SQLite. Execute the following statement to import the SQL file:

sqlite3 rank.db < create_rank.sql

Then insert a few test data randomly. For example:

INSERT INTO rank (name, score) VALUES ('A', 100);
INSERT INTO rank (name, score) VALUES ('B', 200);
INSERT INTO rank (name, score) VALUES ('C', 300);

For databases, we haverank_server.pyAdd the following code to handle the database connection before and after the request.

import sqlite3

DATABASE = 'rank.db'

@app.before_request
def before_request():
    g.db = sqlite3.connect(DATABASE)

@app.teardown_request
def teardown_request(exception):
    if hasattr(g, 'db'):
        g.db.close()

We regulate the use of servers and clientsJSONTransmit data.
GETRequest returnedJSONThe format is as follows:

{
    "data":
    [
        {
            "id": 0,
            "name": "A",
            "score": 100
        },
        {
            "id": 1,
            "name": "B",
            "score": 200
        }
    ]
}

thereidIn fact, it is an auto increment primary key. You don’t need to keep it, but you keep it for later processing convenience.

POSTSubmittedJSONThe format is as follows:

{
    "id": 0,
    "name": "C",
    "score": 300
}

Now we can get startedindexMethods: 1

def index():
    cur = g.db.execute('select id, name, score from rank order by score desc;')
    result = cur.fetchmany(100)
    data = []
    for row in result:
        data.append({'id': row[0], 'name': row[1], 'score': row[2]})
    return jsonify({'data': data})

(among themjsonifyandgstayflaskIn the module. The import will not be described later. The default is from theflaskImport.)
In the query, the data is sorted and only the first 100 records are returned. It can be usedcurlTest it again. Test no error and realize againcreatemethod:

def create():
    status = {'status': 'OK'}
    if not request.json or not 'name' in request.json or not 'score' in request.json:
        status['status'] = 'bad request'
    try:
        g.db.execute('insert into rank (name, score) values (?, ?)', [request.json['name'], request.json['score']])
        g.db.commit()
    except:
        status['status'] = 'database error'
    return jsonify(status)

oursPOSTThe requests are allJSONType, so fromrequest.jsonGain, notargsperhapsform。 In addition, astatusVariable, easy to see the cause of the error.

ReusecurlTest itPOST。 This time, we are going toPOSTAdd data to request:

curl -i -X POST -H "Content-Type: application/json" -d '{"id": 0, "name": "xyz", "score": "800"}' 127.0.0.1:5000/scores

-HParameter is used to specify the header information,-dParameters can carry data. Here is the one that conforms to our submission formatJSONData.

Now the server side is finished. Now it’s time to write c code.

We need to design a class that interacts with the server and returns data to the UI layer.

First of all, this class should be singleton and inheritMonoBehaviour(because interaction with the server takes advantage ofCoroutine); and it’s better to be independent of the scene. See my other article on the centralized way to implement singleton classes in unity. The code for the singleton is as follows:

    private static SaveLoad _instance = null;

    public static SaveLoad Instance {
        get
        {
            if (_instance == null)
            {                                   
                GameObject go = new GameObject("SaveLoadGameObject");
                DontDestroyOnLoad(go);
                _instance = go.AddComponent<SaveLoad>();
            }
            return _instance;
        }
    }

You also need to define some constants:

    const int recordsPerPage = 5;
    const string URL = "127.0.0.1:5000/scores";

Define a data structure:

    public struct Data {
        public int id;
        public string name;
        public int score;
    }

There are two things you need to know before you startWWWClass andLitJsonLibrary.WWWClass is the class of unity to handle HTTP requests;LitJsonIt’s a C ා processJSONOpen source library. To useLitJsonFirst download the DLL file from the official website, and then import the asset.

SaveLoadClass functions like names, including savingSaveAnd loadLoad

    public void Save(Data data)
    {
        var jsonString = JsonMapper.ToJson(data);
        var headers = new Dictionary<string, string> ();
        headers.Add ("Content-Type", "application/json");
        var scores = new WWW (URL, new System.Text.UTF8Encoding ().GetBytes (jsonString), headers);
        StartCoroutine (WaitForPost (scores));
    }

    IEnumerator WaitForPost(WWW www){
        yield return www;
        Debug.Log (www.text);
    }

Create it hereWWWInstance, specifying the URL, header, and submit data. First lineJsonMapperYou can use theJSONIf the property and theJSONKeep the keys in the.

    public void Load()
    {
        var scores = new WWW (URL);
        StartCoroutine(WaitForGet(scores));
    }

    IEnumerator WaitForGet(WWW www){
        yield return www;
        if (www.error == null && www.isDone) {
            var dataList = JsonMapper.ToObject<DataList>(www.text);
            data = dataList.data;
        }else{
            Debug.Log ("Failed to connect to server!");
            Debug.Log (www.error);
        }
    }

LoadIn the method, theindexMethodJSONThe text is converted into an object. In order to realize the conversion, a new one is createdDataListClass, where the properties areList<Data>

Here, the client can read and save data. The rest of the logic, such as the interaction with the UI, is not written here. Interested can see the complete code of my small game. GitHub portal

Finally, let’s talk about deployment. If you want to deploy to SAE, there are several points to note:

  • The code has to be modified to fit inMySQLdb
  • Pay attention to the coding of Chinese. If usedunicodeMethod to convert the name attribute, as well as the file header’s:

    # -*- coding:utf8 -*-
    #encoding = utf-8
    

Finally, we talk about the cross domain access restrictions of unity of comparison pits. After I successfully deployed,curlThere is no problem with the test. As a result, unity reported an error

SecurityException: No valid crossdomain policy available to allow access

After a search, it was originally added to the root directory of the servercrossdomain.xmlDocuments. The contents of the document are as follows:

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <site-control permitted-cross-domain-policies="master-only"/>
    <allow-access-from domain="*"/>
    <allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

However, SAE does not seem to support uploading files to the root directory. Only flag can be usedCounterfeitingHere we are

@app.route('/crossdomain.xml')
def fake():
    XML = 'the pile of content above'
    return xml, 200, {'Content-Type': 'text/xml; charset=ascii'}

OK, it’s done!

Localrank_server.pyFile download

Post deploymentrank_server.pyFile download