当前位置:   article > 正文

Unity结合Flask实现排行榜功能_flask框架连接unity

flask框架连接unity

业余做的小游戏,排行榜本来是用PlayerPrefs存储在本地,现在想将数据放在服务器上。因为功能很简单,就选择了小巧玲珑的Flask来实现。

闲话少叙。首先考虑URL的设计。排行榜无非是一堆分数score集合,按照REST的思想,不妨将URL设为/scores。用GET获得排行榜数据,用POST添加一条新纪录到排行榜。此外,按照惯例,排行榜的数据不需要更新和删除。

Flask自身不支持REST,但我们可以通过routemethod自己实现。下面创建一个原型版本的rank_server.py。命名沿袭了Rails的习惯:

  1. from flask import Flask
  2. app = Flask(__name__)
  3. @app.route('/scores', methods=['GET'])
  4. def index():
  5. return 'index'
  6. @app.route('/scores', methods=['POST'])
  7. def create():
  8. return 'create'
  9. if __name__ == '__main__':
  10. 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内容如下:

  1. DROP TABLE IF EXISTS rank;
  2. CREATE TABLE rank(
  3. id INTEGER PRIMARY KEY AUTOINCREMENT,
  4. name VARCHAR(255) NOT NULL,
  5. score INTEGER NOT NULL
  6. );

Mac自带sqlite。执行下面语句导入sql文件:

sqlite3 rank.db < create_rank.sql

然后随便插入几条测试数据。如:

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

针对数据库,我们在rank_server.py中加入下面一段代码,用于在请求前后处理数据库连接。

  1. import sqlite3
  2. DATABASE = 'rank.db'
  3. @app.before_request
  4. def before_request():
  5. g.db = sqlite3.connect(DATABASE)
  6. @app.teardown_request
  7. def teardown_request(exception):
  8. if hasattr(g, 'db'):
  9. g.db.close()

我们规定服务器和客户端使用JSON传输数据。
GET请求返回的JSON格式如下:

  1. {
  2. "data":
  3. [
  4. {
  5. "id": 0,
  6. "name": "A",
  7. "score": 100
  8. },
  9. {
  10. "id": 1,
  11. "name": "B",
  12. "score": 200
  13. }
  14. ]
  15. }

这里的id其实是自增主键,可以不必保留,但为了后面处理方便就一起保留了。

POST提交的JSON格式如下:

  1. {
  2. "id": 0,
  3. "name": "C",
  4. "score": 300
  5. }

现在我们可以着手实现index方法了:

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

(其中jsonifygflask模块内。后面不再对导入进行说明,默认都是从flask导入。)
在查询时对数据做了排序,并且只返回了前100条记录。可以用curl再测试一下。测试无误再实现create方法:

  1. def create():
  2. status = {'status': 'OK'}
  3. if not request.json or not 'name' in request.json or not 'score' in request.json:
  4. status['status'] = 'bad request'
  5. try:
  6. g.db.execute('insert into rank (name, score) values (?, ?)', [request.json['name'], request.json['score']])
  7. g.db.commit()
  8. except:
  9. status['status'] = 'database error'
  10. 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中实现单例类的集中方式,请看我的另一篇文章。单例的代码如下:

  1. private static SaveLoad _instance = null;
  2. public static SaveLoad Instance {
  3. get
  4. {
  5. if (_instance == null)
  6. {
  7. GameObject go = new GameObject("SaveLoadGameObject");
  8. DontDestroyOnLoad(go);
  9. _instance = go.AddComponent<SaveLoad>();
  10. }
  11. return _instance;
  12. }
  13. }

还需要定义一些常量:

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

定义一个数据结构:

  1. public struct Data {
  2. public int id;
  3. public string name;
  4. public int score;
  5. }

在动手之前,还要了解两个东西:WWW类和LitJson库。WWW类是Unity自带的处理HTTP请求的类;LitJson是一个C#处理JSON的开源库。要使用LitJson,先从官网下载dll文件,然后导入Asset。

SaveLoad类的功能就像名字一样,包括保存Save和载入Load

  1. public void Save(Data data)
  2. {
  3. var jsonString = JsonMapper.ToJson(data);
  4. var headers = new Dictionary<string, string> ();
  5. headers.Add ("Content-Type", "application/json");
  6. var scores = new WWW (URL, new System.Text.UTF8Encoding ().GetBytes (jsonString), headers);
  7. StartCoroutine (WaitForPost (scores));
  8. }
  9. IEnumerator WaitForPost(WWW www){
  10. yield return www;
  11. Debug.Log (www.text);
  12. }

这里创建WWW实例,指定了URL、header和提交数据。第一行的JsonMapper可以在对象和JSON之间进行转换,前提是对象中的属性和JSON中的键要保持一致。

  1. public void Load()
  2. {
  3. var scores = new WWW (URL);
  4. StartCoroutine(WaitForGet(scores));
  5. }
  6. IEnumerator WaitForGet(WWW www){
  7. yield return www;
  8. if (www.error == null && www.isDone) {
  9. var dataList = JsonMapper.ToObject<DataList>(www.text);
  10. data = dataList.data;
  11. }else{
  12. Debug.Log ("Failed to connect to server!");
  13. Debug.Log (www.error);
  14. }
  15. }

Load方法中是将前面index方法返回的JSON文本转换成对象,这里为了实现转换,新建一个DataList类,其中的属性是List<Data>

到这里,客户端的读取和保存数据就实现了。其余的逻辑,比如和UI的交互,在这里就不写了。感兴趣的可以看我的小游戏的完整代码。GitHub传送门

最后谈谈部署的事情。如果要部署到SAE有几点要注意:

  • 代码要进行一定的修改以适应MySQLdb
  • 要注意中文的编码。如用unicode方法转换名字属性,以及文件头部的:

    1. # -*- coding:utf8 -*-
    2. #encoding = utf-8

最后说说比较坑的Unity跨域访问的限制。在我成功部署后,curl测试没有问题了。结果Unity报了错:

SecurityException: No valid crossdomain policy available to allow access

经过一番搜索,原来要在服务器的根目录增加一个crossdomain.xml文件。文件内容大致如下:

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

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

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

OK,大功告成!

本地的rank_server.py文件下载

部署后的rank_server.py文件下载

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/369441
推荐阅读
相关标签
  

闽ICP备14008679号