您的位置:首页 > 移动开发 > Unity3D

Unity结合Flask实现排行榜功能

2015-01-07 17:05 573 查看
业余做的小游戏,排行榜本来是用
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
文件下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: