业余做的小游戏,排行榜本来是用PlayerPrefs
存储在本地,现在想将数据放在服务器上。因为功能很简单,就选择了小巧玲珑的Flask来实现。
闲话少叙。首先考虑URL的设计。排行榜无非是一堆分数score
的集合,按照REST的思想,不妨将URL设为/scores
。用GET
获得排行榜数据,用POST
添加一条新纪录到排行榜。此外,按照惯例,排行榜的数据不需要更新和删除。
Flask自身不支持REST,但我们可以通过route
和method
自己实现。下面创建一个原型版本的rank_server.py
。命名沿袭了Rails的习惯:
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
来测试应用。
brew install curl
测试GET
:
`curl -i -X GET 127.0.0.1:5000/scores`
测试POST
:
`curl -i -X POST 127.0.0.1:5000/scores`
-i
参数可以展示响应的头部信息,便于debug。-X
参数指定请求的方法method。
可以看到测试成功。
下面我们建立存储数据的表。本地测试我们使用sqlite,之后部署使用mysql。
建表文件create_rank.sql
内容如下:
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文件:
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,大功告成!
本地的rank_server.py
文件下载
部署后的rank_server.py
文件下载