Unity結合Flask實現排行榜功能

業餘作的小遊戲,排行榜原本是用PlayerPrefs存儲在本地,如今想將數據放在服務器上。由於功能很簡單,就選擇了小巧玲瓏的Flask來實現。html

閒話少敘。首先考慮URL的設計。排行榜無非是一堆分數score的集合,按照REST的思想,不妨將URL設爲/scores。用GET得到排行榜數據,用POST添加一條新紀錄到排行榜。此外,按照慣例,排行榜的數據不須要更新和刪除。python

Flask自身不支持REST,但咱們能夠經過routemethod本身實現。下面建立一個原型版本的rank_server.py。命名沿襲了Rails的習慣:mysql

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)

執行python rank_server.py來啓動自帶的服務器。下面咱們安裝cURL來測試應用。git

brew install curl

測試GETgithub

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

測試POSTsql

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

-i參數能夠展現響應的頭部信息,便於debug。-X參數指定請求的方法method
能夠看到測試成功。數據庫

下面咱們創建存儲數據的表。本地測試咱們使用sqlite,以後部署使用mysql。
建表文件create_rank.sql內容以下:json

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

Mac自帶sqlite。執行下面語句導入sql文件:flask

sqlite3 rank.db < create_rank.sql

而後隨便插入幾條測試數據。如:跨域

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

針對數據庫,咱們在rank_server.py中加入下面一段代碼,用於在請求先後處理數據庫鏈接。

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()

咱們規定服務器和客戶端使用JSON傳輸數據。
GET請求返回的JSON格式以下:

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

這裏的id實際上是自增主鍵,能夠沒必要保留,但爲了後面處理方便就一塊兒保留了。

POST提交的JSON格式以下:

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

如今咱們能夠着手實現index方法了:

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})

(其中jsonifygflask模塊內。後面再也不對導入進行說明,默認都是從flask導入。)
在查詢時對數據作了排序,而且只返回了前100條記錄。能夠用curl再測試一下。測試無誤再實現create方法:

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)

咱們的POST請求都是JSON類型的,因此要從request.json得到,而不是args或者form。此外,返回了一個status變量,便於查看出錯緣由。

再用curl測試一下POST。此次,咱們要向POST請求中加入數據:

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

-H參數用於指定頭部信息,-d參數能夠攜帶數據,這裏就是一條符合咱們提交格式的JSON數據。

如今服務器端就(暫時)實現完了。下面該寫C#代碼啦。

咱們須要設計一個和服務器交互、並返回數據給UI層的類。

首先,這個類應該是單例的,要繼承MonoBehaviour(由於和服務器交互要利用Coroutine);並且最好獨立於場景以外。關於Unity中實現單例類的集中方式,請看個人另外一篇文章。單例的代碼以下:

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;
        }
    }

還須要定義一些常量:

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

定義一個數據結構:

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

在動手以前,還要了解兩個東西:WWW類和LitJson庫。WWW類是Unity自帶的處理HTTP請求的類;LitJson是一個C#處理JSON的開源庫。要使用LitJson,先從官網下載dll文件,而後導入Asset。

SaveLoad類的功能就像名字同樣,包括保存Save和載入Load

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);
    }

這裏建立WWW實例,指定了URL、header和提交數據。第一行的JsonMapper能夠在對象和JSON之間進行轉換,前提是對象中的屬性和JSON中的鍵要保持一致。

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);
        }
    }

Load方法中是將前面index方法返回的JSON文本轉換成對象,這裏爲了實現轉換,新建一個DataList類,其中的屬性是List<Data>

到這裏,客戶端的讀取和保存數據就實現了。其他的邏輯,好比和UI的交互,在這裏就不寫了。感興趣的能夠看個人小遊戲的完整代碼。GitHub傳送門

最後談談部署的事情。若是要部署到SAE有幾點要注意:

  • 代碼要進行必定的修改以適應MySQLdb
  • 要注意中文的編碼。如用unicode方法轉換名字屬性,以及文件頭部的:

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

最後說說比較坑的Unity跨域訪問的限制。在我成功部署後,curl測試沒有問題了。結果Unity報了錯:

SecurityException: No valid crossdomain policy available to allow access

通過一番搜索,原來要在服務器的根目錄增長一個crossdomain.xml文件。文件內容大體以下:

<?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>

可是SAE好像不支持上傳文件到根目錄。只能用Flask仿冒一下了:

@app.route('/crossdomain.xml')
def fake():
    xml = """上面的那堆內容"""
    return xml, 200, {'Content-Type': 'text/xml; charset=ascii'}

OK,大功告成!

本地的rank_server.py文件下載

部署後的rank_server.py文件下載

相關文章
相關標籤/搜索