【转】Unity中的协同程序-使用Promise进行封装(三)
原文:http://gad.qq.com/program/translateview/7170967
译者:崔国军(飞扬971) 审校:王磊(未来的未来)
在这个系列的最后一部分文章,我们要通过Unity的协同程序来建立一个REST API接口的真正示例来作为一个内部web请求的工具,在这个示例中,会使用Promise作为封装层。我们会使用对于任何人都可用的 fake REST APIservice来测试他们的服务。这是一个简单的API,实现了典型的用户待办事项列表、发表帖子和进行评论以及使用相册和照片的场景。如果你建立你自己的前端的话,这会非常的有用而且不需要有自己的运行服务器。
需要注意的是:本教程的内容稍微有一点高阶,它不会教你有关REST的后端理论或者是JSON序列化。它还假设你已经熟悉我们在这个系列的第二部分里面所涉及的Promise的内容。
项目介绍
我们的项目将会基于用户待办事项列表。它的主要特点很简单:它会将用户名作为输入并提供与该用户相关的任务列表。这个应用程序会得到包含所有用户的列表,在这个列表里面找到要搜索的用户名,如果存在的话,它将获取所有和这个用户相关的任务。理想情况下你会希望用户搜索在服务器端完成,但为了这个例子,让我们假设别人没想过这个问题,由你来做这项工作。
对于JSON的反序列化,我们使用了流行的 JSON .NET框架。如果你的项目打算跨平台的话,你应该看看J JSON.NET for Unity,这个框架使用了相同的名字空间和结构,所以可以很容易地作为替代而使用。
我们将会使用Unity 5.4.0f3。你可以在这里 下载.unitypackage,从而得到一个完整的项目和所有必要的插件。让我们深入看下项目。
项目包含了一个插件目录、只有单一示例场景的场景目录还有一个脚本目录,在脚本目录里面有全部的代码。整个代码结构如下:
从我们从最顶层开始。
模型文件夹
模型文件夹就是数据模型类所在的目录。。他们本质上是将类与属性映射到JSON的对象键上。举个简单的例子来说,在JSON中,一个简单的任务对象看上去应该像是这样:
1
2
3
4
5
6
|
{ "userId" : 1, "id" : 1, "title" : "delectus aut autem" , "completed" : false } |
相关的模型类用如下的方法进行实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using Newtonsoft.Json; namespace APIExample.API.Models { public class User { [JsonProperty( "id" )] public int Id { get ; set ; } [JsonProperty( "name" )] public string Name { get ; set ; } [JsonProperty( "username" )] public string Username { get ; set ; } } } |
正如你可以看到的那样,JSON .NET使用JsonProperty属性映射来使得映射变得非常容易。事实上,如果属性名字和JSON的主键匹配的话,你完全可以跳过这些。就我个人而言,我更喜欢在我的JSON中使用camelCase而在我的属性里面使用PascalCase。请记住,在Unity上你应该使用JSON .NET或者使用正则字段。参考文档来获得更多信息。
用户模型是一种简化后的模型,因为jsonplaceholder会返回一个大的多的JSON文件,但在这个例子中我们的目的主要是写一个示例,所以我们不会实现所有的属性。
Promise作为一个服务接口
假设你在一个RESTAPI工作了一个月,然后发现贵公司的管理决定搬到Websocket上实现。或者你是你们公司后台部门的负责人,自己想要测试新功能而不需要使用一个真正的服务器。为了解决这些问题,实现工厂模式通过封装IAPIService里面的所有公共接口来让你选择你的服务的具体实现是一个好主意。这个接口会使用Promise作为抽象层,所以这个接口会很容易使用。
要对用户进行搜索,并列举他们的任务,我们只需要两个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
using System.Collections.Generic; using APIExample.API.Models; using RSG; namespace APIExample.API { /// <summary> /// Represents a higher level idea of an API service. /// </summary> public interface IAPIService { /// <summary> /// Finds user via the API. /// </summary> /// <param name="username">Username of searched user /// <returns>User model instance</returns> IPromise<user> FindUser( string username); /// <summary> /// Gets all the user's tasks from the API. /// </summary> /// <param name="username">Id of user /// <returns>Collection of all tasks associated with user</returns> IPromise<ienumerable<task>> GetUserTasks( int userId); } }</ienumerable<task></user> |
如果你在未来需要另一个API实现,所有你要做的就是创建一个新的类来实现这两个方法。实例化是通过工厂和提供的配置(IClientConfig和ClientConfig)来实现的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
using APIExample.Config; using UnityEngine; namespace APIExample.API { public class APIServiceFactory { /// <summary> /// Creates API service based on provided config. /// </summary> /// <param name="config">IClientConfig implementation /// <returns>IAPIService instance</returns> public static IAPIService CreateAPIService(IClientConfig config) { switch (config.APIType) { case Config.API.TEST: var testApi = new TestAPIService(); return testApi; case Config.API.REST: default : var go = new GameObject( "RestAPI" ); var restApi = go.AddComponent<restapiservice>(); restApi.Initialize(config.IPEndPoint); return restApi; } } } }</restapiservice> |
RestAPIService
REST API的实现在内部使用了Unity的协同程序和UnityWebRequest类。正因为如此,工厂创建一个游戏物体和并往这个游戏物体上附加了RestAPIService类,RestAPIService类也是继承自MonoBehaviour类。这让我们进一步封装了协同程序,你将能够在所有的类中使用该服务,因为这个接口只需要处理Promise。举个简单的例子来说,得到用户的任务是会相面的代码这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public IPromise<ienumerable<task>> GetUserTasks( int userId) { // We return a promise instantly and start the coroutine to do the real work var promise = new Promise<ienumerable<task>>(); StartCoroutine(_GetUserTasks(promise, userId)); return promise; } private IEnumerator _GetUserTasks(Promise<ienumerable<task>> promise, int userId) { var request = UnityWebRequest.Get(apiAddress + "/todos?userId=" + userId); yield return request.Send(); if (request.isError) // something went wrong { promise.Reject( new Exception(request.error)); } else if (request.responseCode != 200) // or the response is not OK { promise.Reject( new Exception(request.downloadHandler.text)); } else { // Format output and resolve promise string json = request.downloadHandler.text; var tasks = JsonConvert.DeserializeObject<list<task>>(json); promise.Resolve(tasks); } }</list<task></ienumerable<task></ienumerable<task></ienumerable<task> |
可以注意到通过promise.Resolve()和 promise.Reject()有多么容易对输出进行控制。
测试的实现仅仅是用来作为一个例子告诉说你能利用这些机制做些什么。它在没有任何外部调用的情况下返回对象,但你也可以用它在没有真正启动服务器的情况下,作为一个房间来测试您的JSON反序列化。底线是这应该给各种混乱留有一定的空间,没有必要担心你硬编码的一些测试场景,这些硬编码的东西需要在之后注释掉。所有你需要做的就是更改配置来得到真正的服务,这样的话你就完成了任务。
所以让我们举个简单的例子来说明下这种情况,如果你想测试你的任务的UI,但服务器端还没有完成任务这一功能,那么你可以只是实现测试服务以及GetUserTasks()方法来返回一大堆测试对象:
1
2
3
4
5
6
7
8
9
|
public IPromise<ienumerable<task>> GetUserTasks( int userId) { var promise = new Promise<ienumerable<task>>(); promise.Resolve( new List<task> { new Task { Id = 0, UserId = userId, Title = "Test task 1" , Completed = true }, new Task { Id = 1, UserId = userId, Title = "Test task 2" , Completed = false } }); return promise; }</task></ienumerable<task></ienumerable<task> |
需要注意的是,当你需要这个值得时候,你可以立刻对promise进行决议。
结果
回报是这个示例当中的测试场景和会利用这个借口的MainScreenController类。首先,它使用工厂进行初始化服务:
1
2
3
4
5
6
7
|
clientConfig = new ClientConfig { APIType = Config.API.REST, }; apiService = APIServiceFactory.CreateAPIService(clientConfig); |
如之前所描述的那样,改变测试的实现就跟在配置中交换APIType属性一样简单。UI包含一个输入字段,在这个字段中你可以输入用户名,还有一个按钮来得到所有用户的任务。这个通用接口的使用没有办法再简化了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void FindUserTasks( string username) { resultArea.text = "" ; status.text = "" ; apiService.FindUser(username) .Then(user => { resultArea.text += string .Format( ">User ID: {0}, Username: {1}, Name: {2}\n" , user.Id, user.Username, user.Name); return apiService.GetUserTasks(user.Id); }) .Then(tasks => { foreach (var task in tasks) { resultArea.text += string .Format( ">>Task ID: {0}, Title: {1}, Completed: {2}\n" , task.Id, task.Title, task.Completed); } }) .Catch(error => { status.text = string .Format( "Error: {0}" , error.Message); }); } |
总结
最后,终于到了这个系列结束的时候了。再一次,完整的这个项目可以从这里下载,都在.unitypackage文件里面。总而言之,Promise被证明是一个很伟大的方式来从Unity中具体的协同程序进行抽象代码。他们也可以使用在许多不同的情况下,是一种优雅的方式来创建干净的接口。我们希望你会喜欢我们所做的一切。如果你有任何关于这个系列或示例项目的问题,请在文件下面的部分留下你的评论。
【转】Unity中的协同程序-使用Promise进行封装(三)的更多相关文章
- 【转】Unity中的协同程序-使用Promise进行封装(二)
原文:http://gad.qq.com/program/translateview/7170970 译者:王磊(未来的未来) 审校:崔国军(飞扬971) 在上一篇文章中,我们的注意力主要是 ...
- 【转】Unity中的协同程序-使用Promise进行封装(一)
原文:http://gad.qq.com/program/translateview/7170767 译者:陈敬凤(nunu) 审校:王磊(未来的未来) 每个Unity的开发者应该都对协同程序非 ...
- Unity 中的协同程序
今天咱就说说,协同程序coroutine.(这文章是在网吧敲的,没有unity,但是所有结论都被跑过,不管你信得过我还是信不过我,都要自己跑一下看看,同时欢迎纠错)先说说啥是协程:协同程序是一个非常让 ...
- Lua中的协同程序
[前言] 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈.局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.从概念上讲,线程与协同程序的主要区别在于,一个具有多个线程的 ...
- Lua中的协同程序 coroutine
Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...
- Lua中的协同程序 coroutine(转)
Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...
- 小程序 请求Promise简单封装
最近做小程序在调用后台接口的时候感觉总写很长一串,很冗杂.非常想念vue中promise封装的写法,于是自己初步封装了一下. 1.url 接口地址 2.headers请求头 3. params 请求参 ...
- 关于Unity中常用的数据结构和JSON处理(专题三)
数据结构通俗来讲就是用某个对象去存储数据集合,比如要存储100个整数,要用什么样的数据类型能把它们存储好. Jason处理,服务器对接,配置文件的使用,Unity和Jason之间相互的转换. Arra ...
- 【转】关于Unity协同程序(Coroutine)的全面解析
http://www.unity.5helpyou.com/2658.html 本篇文章我们学习下unity3d中协程Coroutine的的原理及使用 1.什么是协调程序 unity协程是一个能暂停执 ...
随机推荐
- POJ3635 Full Tank?(DP + Dijkstra)
题目大概说,一辆带有一个容量有限的油箱的车子在一张图上行驶,每行驶一单位长度消耗一单位油,图上的每个点都可以加油,不过都有各自的单位费用,问从起点驾驶到终点的最少花费是多少? 这题自然想到图上DP,通 ...
- Visual Studio: How to change ipch path in Visual Studio 2010 (.sdf, *.opensdf, ...)
Link: http://stackoverflow.com/questions/4315681/how-to-change-ipch-path-in-visual-studio-2010 引用: T ...
- iOS学习06C语言结构体
1.结构体的概述 在C语言中,结构体(struct)指的是一种数据结构,是C语言中构造类型的其中之一. 在实际应用中,我们通常需要由不同类型的数据来构成一个整体,比如学生这个整体可以由姓名.年龄.身高 ...
- BZOJ3607 : 数据网络
首先答案一定是包含直径某个端点的一个连通块里所有边权值之和,设直径为$AB$,以$A$和$B$分别为根进行处理. 首先按照最长路法则将这棵树进行树链剖分,那么每个叶子的贡献为它与它所在链顶端的点的距离 ...
- 'libxml/HTMLparser.h' file not found in ASIHTTPRequest 解决方法
首先导入libxml2.dylib,具体怎么导入跟导入frameworks一样 然后在Build Setting中的Header Search Paths to: 添加 ${SDK_DIR}/usr/ ...
- 洛谷 P1803 凌乱的yyy Label:Water 贪心
题目背景 快noip了,yyy很紧张! 题目描述 现在各大oj上有n个比赛,每个比赛的开始.结束的时间点是知道的. yyy认为,参加越多的比赛,noip就能考的越好(假的) 所以,他想知道他最多能参加 ...
- iOS 网络框架编写总结
一,常用 1> 不错的处理接收到的网络图片数据的方法 id img= ISNSNULL(pic)?nil:[pic valueForKey:@"img"]; NSString ...
- FS_11C14温湿度传感器(二)
作者:刘老师,华清远见嵌入式学院讲师. 在FS_11C14平台DHT11传感器程序: /******************************************************** ...
- 纪念逝去的岁月——C/C++二分查找
代码 #include <stdio.h> int binarySearch(int iList[], int iNum, int iX, int * pPos) { if(NULL == ...
- sql分页代码
//三种sql分页语句 SELECT TOP 分页尺寸 * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY id) AS RowNumber,* FROM Blob ...