天真。超级:简单游戏的代码和架构

我们生活在一个复杂的世界中,似乎已经开始忘记简单的事情。例如,关于奥卡姆(Occam)的剃刀,其原理是:“在较小数量的基础上可以做的事情,不应在较大数量的基础上进行。” 在本文中,我将讨论可用于开发简单游戏的简单而不是最可靠的解决方案。



到秋天在莫斯科的DotNext时,我们决定开发一款游戏。这是流行的炼金术的IT变体。玩家必须从可用的4个元素中收集与IT,披萨和Dodo相关的128个概念。我们需要在一个多月的时间内实现从构思到可行的游戏。

在上一篇文章中,我写了关于作品的设计部分的内容:计划,fakapy和情感。而这篇文章是关于技术部分的。甚至会有一些代码!

免责声明:我在下面编写的方法,代码和体系结构并不复杂,原始或可靠。相反,它们非常简单,有时很幼稚,并非设计使然。但是,如果您从未制作过使用服务器上的逻辑的游戏或应用程序,那么本文可以作为起点。

客户代码


简而言之,该项目的体系结构是:我们在Unity上有Android和iOS的移动客户端,在ASP.NET上有一个以CosmosDB作为存储的后端服务器。

Unity上的客户端仅表示与UI的交互。玩家单击元素,它们以固定的方式在屏幕上移动。创建新元素时,将显示一个窗口及其说明。


游戏

过程这个过程可以用一个非常简单的状态机来描述。该状态机的主要作用是等待过渡到下一个状态的动画,从而阻止播放器的UI。



我使用非常酷的UnitRx以完全异步的方式编写Unity代码。最初,我尝试使用本机Task,但它们在iOS版本上表现不稳定。但是UniRx.Async像时钟一样工作。

任何需要动画的动作都可以通过AnimationRunner类来调用:

public static class AnimationRunner
   {
       private const int MinimumIntervalMs = 20;
     public static async UniTask Run(Action<float> action, float durationInSeconds)
       {
           float t = 0;
           var delta = MinimumIntervalMs / durationInSeconds / 1000;
           while (t <= 1)
           {
               action(t);
               t += delta;
               await UniTask.Delay(TimeSpan.FromMilliseconds(MinimumIntervalMs));
           }
       }
   }

这实际上是用UnitTask代替了经典的协程。此外,任何应阻止UI的调用都通过HandleUiOperation全局类方法调用GameManager

public async UniTask HandleUiOperation(UniTask uiOperation)
       {
           _inputLocked = true;
           await uiOperation;
           _inputLocked = false;
       }

因此,在所有控件中,首先检查InputLocked的值,并且只有在它为false时,控件才会做出反应。

这使得使用嵌套调用的异步/等待方法(如在俄罗斯玩偶中)轻松实现上述状态机,包括网络调用和I / O。

客户端的第二个重要特征是,所有在元素上接收数据的提供程序都是以接口的形式进行的。会议结束后,当我们关闭后端时,它实际上允许一个晚上重写客户端代码,从而使游戏完全脱机。现在可以从Google Play下载该版本

客户与客户之间的互动


现在,让我们讨论一下在开发客户端-服务器体系结构时我们做出了哪些决定。



图片存储在客户端上,服务器负责所有逻辑。在启动时,服务器读取带有ID和所有元素描述的csv文件,并将它们存储在其内存中。之后,他准备出发。

API方法是必需的最小值-只有5种。他们实现了游戏的所有逻辑。一切都非常简单,但是我会告诉您一些有趣的观点。

身份验证和入门元素


我们放弃了任何复杂的身份验证系统以及通常的任何密码。当玩家在开始屏幕上输入名称并按下“开始”按钮时,将在客户端中创建唯一的随机令牌(ID)。但是,它没有连接到设备。玩家的名字和令牌一起发送到服务器。从客户端到后端的所有其他请求都包含此令牌。



此解决方案的明显缺点是:

  1. 如果用户拆除该应用程序并重新安装它,则他将被视为新播放器,并且所有进度都将丢失。
  2. 您不能在其他设备上继续游戏。

这些是有意为之的假设,因为我们了解到人们仅通过一种设备就可以在会议上玩游戏。而且他们将没有时间切换到另一台设备。

因此,在计划的方案中,客户端AddNewUser调用服务器方法一次。

加载游戏屏幕时GetBaseElements,该方法也被调用一次,该方法返回ID,精灵名称和四个基本元素的描述。客户端在其资源中找到了必要的精灵,创建了元素的对象,将其本地写入并绘制在屏幕上。

反复启动后,客户端不再在服务器上注册,也没有请求启动元素,而是从本地存储中获取了它们。结果,游戏屏幕立即打开。

合并元素


当玩家尝试连接两个元素时,将调用一个方法,该方法MergeElements返回有关新元素的信息或报告未收集这两个元素。如果玩家收集了新元素,则有关此信息的信息会记录在数据库中。



我们应用了一个显而易见的解决方案:在播放器尝试添加两个元素之后,为减轻服务器的负载,结果被缓存在客户端(在内存和csv中)。如果玩家尝试重新堆叠物品,则首先检查缓存。并且仅当结果不存在时,请求才会发送到服务器。

因此,我们知道每个玩家都可以与背部进行有限数量的互动,并且数据库中的条目数不会超过可用元素的数量(我们有128个元素)。我们能够避免许多其他会议应用程序出现的问题,在与会者大量同时参加会议之后,通常会对此进行备份。

高分表


参与者不仅玩了我们的“炼金术”,还为了获得奖品。因此,我们需要一个记录表,该记录表显示在我们展台的屏幕上,也显示在游戏的单独窗口中。



为了形成记录表,使用了后两种方法,GetCurrentLadder并且GetUser.还有一个细微的差别。如果一名玩家在前20名的结果中,则其姓名将在表中突出显示。问题在于这两种方法的信息不直接相关。

该方法GetCurrentLadder访问集合Stats,获取20个结果并快速执行。该方法GetUser访问集合。Users由UserId执行,并且速度过快。结果的合并已经在客户端上。只是我们不想在结果中使用UserId,因此它们不存在。根据玩家的姓名和得分点进行比较。对于成千上万的玩家,不可避免地会发生碰撞。但是我们指望这样一个事实,即在所有球员中,不太可能有名字和得分相同的球员。就我们而言,这种方法是完全合理的。

游戏结束


我们的最初任务是为一个月内为期两天的会议组装游戏。我们在体系结构和代码中体现的所有这些决定都充分证明了自己的合理性。服务器没有躺下,所有请求都得到了正确处理,并且播放器没有抱怨应用程序中的错误。

在下一个Moscow DotNext之前,我们最有可能引起另一场比赛,因为现在它已成为我们的优良传统(CMAN-2018IT-Alchemy-2019)。在评论中写下您准备好与哪个时间杀手交换开发明星的核心报告。:)
出于同样的天真和兴趣,我们已在公共领域发布了IT炼金术的客户代码

并查看电报频道,我在其中撰写有关发展,生活,数学和哲学的所有文章。

All Articles