【转】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中具体的协同程序进行抽象代码。他们也可以使用在许多不同的情况下,是一种优雅的方式来创建干净的接口。我们希望你会喜欢我们所做的一切。如果你有任何关于这个系列或示例项目的问题,请在文件下面的部分留下你的评论。