原文: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进行封装(三)的更多相关文章

  1. 【转】Unity中的协同程序-使用Promise进行封装(二)

    原文:http://gad.qq.com/program/translateview/7170970 译者:王磊(未来的未来)    审校:崔国军(飞扬971)   在上一篇文章中,我们的注意力主要是 ...

  2. 【转】Unity中的协同程序-使用Promise进行封装(一)

    原文:http://gad.qq.com/program/translateview/7170767 译者:陈敬凤(nunu)    审校:王磊(未来的未来) 每个Unity的开发者应该都对协同程序非 ...

  3. Unity 中的协同程序

    今天咱就说说,协同程序coroutine.(这文章是在网吧敲的,没有unity,但是所有结论都被跑过,不管你信得过我还是信不过我,都要自己跑一下看看,同时欢迎纠错)先说说啥是协程:协同程序是一个非常让 ...

  4. Lua中的协同程序

    [前言] 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈.局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.从概念上讲,线程与协同程序的主要区别在于,一个具有多个线程的 ...

  5. Lua中的协同程序 coroutine

    Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...

  6. Lua中的协同程序 coroutine(转)

    Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...

  7. 小程序 请求Promise简单封装

    最近做小程序在调用后台接口的时候感觉总写很长一串,很冗杂.非常想念vue中promise封装的写法,于是自己初步封装了一下. 1.url 接口地址 2.headers请求头 3. params 请求参 ...

  8. 关于Unity中常用的数据结构和JSON处理(专题三)

    数据结构通俗来讲就是用某个对象去存储数据集合,比如要存储100个整数,要用什么样的数据类型能把它们存储好. Jason处理,服务器对接,配置文件的使用,Unity和Jason之间相互的转换. Arra ...

  9. 【转】关于Unity协同程序(Coroutine)的全面解析

    http://www.unity.5helpyou.com/2658.html 本篇文章我们学习下unity3d中协程Coroutine的的原理及使用 1.什么是协调程序 unity协程是一个能暂停执 ...

随机推荐

  1. Codeforces Round #333 (Div. 2)

    水 A - Two Bases 水题,但是pow的精度不高,应该是转换成long long精度丢失了干脆直接double就可以了.被hack掉了.用long long能存的下 #include < ...

  2. mysql建表建索引

    建表: CREATE TABLE `sj_projects` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL ...

  3. 应用程序间跳转 (友盟SSO 授权 与系统自带的分享)

    应用程序间跳转的应用场景 使用第三方用户登录,如微信登录,返回用户名和密码 需要用户授权,返回到调用程序,同时返回授权的用户名 应用程序推广,跳转到itunes并显示指定app下载页 第三方支付,跳转 ...

  4. jquery 最简单的动画效果

    <p style="border: 1px solid red"> 我会慢慢变大 </p> <a>dianji</a> <sc ...

  5. CentOS上安装RabbitMQ3.6.X

    RabbitMQ3.6.1的安装方法跟以前的版本有点不一样,我在网上找了很多资料,基本都是3.1左右的版本,而且安装过程很繁琐,所以我花了一下午 的时间研究如何实现最简安装.为了让大家少走弯路,就把安 ...

  6. 如何在WORD2010中取消自动编号?

    如何在WORD2010中取消自动编号? 使用WORD2010有一个很大的问题就是WORD2010的自动编号问题,WORD2010的自动编号是符合外国人的写作习惯的,对中国人来说不适用. WORD201 ...

  7. 在Eclipse中在线安装Emmet和图文使用教程

    ZenCoding 升级为 Emmet 之后,基于 Eclipse 的插件安装地址也发生了变化, 下面是在基于 Eclipse 的 IDE 中安装和使用 Emmet 的图文示例. 一.打开 Eclip ...

  8. MongoDB的数据库基本操作(二)

    创建数据库 >use mydb switched to db mydb  查看所有的数据表 >show collections  system.indexes 创建数据表 >db.c ...

  9. signal(SIGPIPE, SIG_IGN)

    文章来源:http://blog.163.com/niuxiangshan@126/blog/static/170596595201221942952676/   当服务器close一个连接时,若cl ...

  10. CSS3+HTML5实现块阴影与文字阴影

    CSS 3 + HTML 5 是未来的 Web,它们都还没有正式到来,虽然不少浏览器已经开始对它们提供部分支持.本教程分5节介绍了 5 个 CSS3 技巧,可以帮你实现未来的 Web,不过,这些技术不 ...