原文: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. 餐厅点餐APP总结

    总结:经过这几个月的时间里,我们通过学习.讨论一起做出了餐厅点餐这个APP,我们在做这个APP的过程中,每个人都有自己的想法,也通过讨论最后做出了这个app,虽然做的不是很好,但是我们也尽自己的努力尽 ...

  2. SPFA(负环) LightOJ 1074 Extended Traffic

    题目传送门 题意:收过路费.如果最后的收费小于3或不能达到,输出'?'.否则输出到n点最小的过路费 分析:关键权值可为负,如果碰到负环是,小于3的约束条件不够,那么在得知有负环时,把这个环的点都标记下 ...

  3. git的合并与推送

    集中式合作模式 1.git fetch 获取远程更新 2.git merge origin/master 进行合并,如果报错,则相应解决.注:你得用git bash命令行执行才能看见报错详情,用ecl ...

  4. [转]crontab环境变量设置

    原文连接:http://blog.csdn.net/zc02051126/article/details/20480289 come from http://www.360doc.com/conten ...

  5. Oracle 存储过程学习

    转自:http://blog.chinaunix.net/uid-20495387-id-174394.html http://www.cnblogs.com/rootq/articles/11000 ...

  6. ACM 16进制的简单运算

    16进制的简单运算 时间限制:1000 ms  |  内存限制:65535 KB 难度:1   描述 现在给你一个16进制的加减法的表达式,要求用8进制输出表达式的结果.   输入 第一行输入一个正整 ...

  7. 关于UIView的userInteractionEnabled属性

    关于UIView的userInteractionEnabled属性 如果父视图为ParentView包含一个Button,如果再ParentView上添加子视图ChildView,且ChildView ...

  8. 原生js动画效果(源码解析)

    在做页面中,多数情况下都会遇到页面上做动画效果,大部分都是用jquery来实现动画,今天正好看到一篇原生js实现动画效果的代码,特分享在此. 原文地址:http://www.it165.net/pro ...

  9. HDU 1102 最小生成树裸题,kruskal,prim

    1.HDU  1102  Constructing Roads    最小生成树 2.总结: 题意:修路,裸题 (1)kruskal //kruskal #include<iostream> ...

  10. Rational Rose 2007 破解版安装过程

    Rational Rose 2007 破解版安装过程 首先通过网站将软件下载,然后依照以下步骤进行: 选择第二项,下一步 一直点击next,出现如下,可以修改安装的目的文件夹 设置完路径之后出现如下, ...