業餘作的小遊戲,排行榜原本是用PlayerPrefs
存儲在本地,如今想將數據放在服務器上。由於功能很簡單,就選擇了小巧玲瓏的Flask來實現。html
閒話少敘。首先考慮URL的設計。排行榜無非是一堆分數score
的集合,按照REST的思想,不妨將URL設爲/scores
。用GET
得到排行榜數據,用POST
添加一條新紀錄到排行榜。此外,按照慣例,排行榜的數據不須要更新和刪除。python
Flask自身不支持REST,但咱們能夠經過route
和method
本身實現。下面建立一個原型版本的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
測試GET
:github
`curl -i -X GET 127.0.0.1:5000/scores`
測試POST
:sql
`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})
(其中jsonify
和g
在flask
模塊內。後面再也不對導入進行說明,默認都是從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,大功告成!