英文原文:https://thegamedev.guru/unity-architecture/a-better-architecture-for-unity-projects/

  在使用 Unity 重新制作 Diamond Dash 工作了六个月后,我可以说我从 Wooga 的工程师那里学到了很多,除了自我反思。 我经常学习软方式,也学习硬方式。 无论如何,在经历了比失败更多的成功之后,我提出了我对伟大架构应该是什么样子的看法。

  无论您是构建 Unity 应用程序还是 Unity 游戏,您是从头开始,或者您只是对当前系统不满意,我认为您会从阅读中受益。

  全面披露:本文档背后的大部分想法和系统实现都是在 Wooga 开发的。 我主要对它们进行了进一步的完善和增强,以适应我们新项目的需求,并且还花时间对其进行了重组并写了一篇关于它的博客文章。 传播知识!


让我们开始吧!

1. 依赖注入

  你的类不负责获取他们需要的引用,不要强迫他们这样做。 作为单一责任原则的延伸,他们应该只关注他们定义明确的小任务。

  您可以使用著名的 DI 框架,例如 Zenject 和 StrangeIoC。 但是,如果您有时间,我鼓励您编写自己的 DI 类。 您将通过它学到很多东西,并准备好处理将来可能出现的 DI 问题。 不到 100 行代码就可以写出 1 行代码; 作为参考,您可以查看我在 Unity 中开发 Diamond Dash 时使用的相同 DI 脚本。

  DI 允许您编写更少的代码,而更少的代码意味着出错的可能性更小。 你的代码会更干净,你的开发者会更开心。 DI 系统是伟大架构的关键需求。 不要忽视它。

2.单入口点

  在你的游戏中有一个单一的入口点来初始化和保存全局对象。 这将帮助您创建、配置和维护全局对象:广告管理器、音频系统、调试选项等。同样重要的是您可以设置的显式系统初始化顺序,构建依赖图。

  如果您添加一个系统来检测未处理的异常,则会出现另一个额外的好处。 在这些情况下,您可以向用户显示道歉消息并重新加载初始化场景,以便您重新启动(引导)整个应用程序而不实际退出它。

  一个例子如下:

public class Game : BaseGame
{private void Awake(){DontDestroyOnLoad(this);SetupGame();}protected override void BindGame(){Container.Bind().FromInstance(new FacebookManager());Container.Bind().FromInstance(new Backend());Container.Bind().FromInstance(new MainPlayer());}protected override IEnumerator LoadProcess(){yield return new WaitForSeconds(2);yield return CommandFactory.Get<LoadMainMenu>().Load(LoadSceneMode.Single);}
}

3. 附加场景加载

  小心使用预制件。 始终牢记黄金法则:一旦持有对预制件(基本上是任何其他对象)的引用,其内容将完全(递归)加载到内存中。 这意味着,包括纹理、几何体、其他引用的预制件、音频剪辑等在内的所有资产都将被同步加载。 它们是否被实例化并不重要,因为实例化过程只会创建一个浅拷贝并调用 Awake/Start/OnEnable/… 方法,这在帧速率、内存、电池等方面可能非常昂贵。 动画师是昂贵系统的一个很好的例子。

  我见过一些项目完全在预制件上构建他们的 UI。 这些项目一旦在功能和用户方面扩大规模,就无法再维护这样的系统了。 虽然它背后的想法是良性的,但它在 Unity 生态系统中的转化效果非常差。 例如,您很可能会在移动设备中获得 40 多秒的加载时间。

  更好地解决这个问题的实际选择是什么? 您始终可以使用asset bundles,但维护其管道并不是特别轻松。 我强烈推荐的方式是使用附加场景加载。

  这个想法是有一个根场景(例如主菜单),它可以根据需要以加法和异步的方式动态加载和卸载场景。 它们将自动堆叠在一起,但仍要小心画布排序顺序。 这种方法比天真的预制加载更高级,但有相当大的好处:

  • 内存占用大大减少,因为您随时只加载您需要的目标场景的内容。
  • 加载时间大大减少,因为要处理、加载和反序列化的东西要少得多。
  • 由此产生的层次结构更清晰,组织良好,其布局类似于一堆有凝聚力的屏幕。 更好的组织意味着更有效的工作。

  您可以通过强制每个单独的场景拥有一个负责管理该场景的顶级根对象来很好地实现这一点。 该根对象通常从加载它的场景中访问和初始化以进行进一步配置。

4. 命令

  作为开发人员,我们鼓励设计(自动)可测试的解耦系统。 然而,系统很少独立工作。 它们必须经常协调,即再次耦合,才能正确执行某些过程。

  我拥有的黄金法则之一是:启动进程的对象负责完成和清理它。 例子:

  • 你撒掉咖啡 -> 你清洗它。
  • 函数分配内存 -> 同一个函数释放它,
  • Manager 开始购买 -> 经理完成并清理
  • Manager实例化一个敌人 -> 经理在死亡时将其摧毁
  • 功能阻塞教程中的界面 -> 相同的功能在完成后解除阻塞

  在这种情况下,一个行之有效的想法是命令模式。 行为可以通过这种方式被视为一流的实体。 命令是一个可实例化的类,用于包装方法调用并在完成时销毁。 这样做的好处是我们可以在异步调用中以对象变量的形式存储时间信息。 命令确实以干净的状态开始和结束,并且只有在它们有最终结果(数据、成功/失败)时才返回。 Unity 通过使用协程很好地适应了这种模式。

public class MainMenu : MonoBehaviour
{[Inject] private CommandFactory _commandFactory;private void Start(){StartCoroutine(OpenPopup());}private IEnumerator OpenPopup(){var popupCommand = _commandFactory.Get<ShowPopup>();yield return popupCommand.Run("Showing popup from main menu");Debug.Log("Result: " + popupCommand.Result);}
}public class ShowPopup : Command
{public Popup.ResultType Result;public IEnumerator Run(string text){var loadSceneCommand = CommandFactory.Get<LoadModalScene<Popup>>();yield return loadSceneCommand.Load();var popup = loadSceneCommand.LoadedRoot;popup.Initialize(text);yield return new WaitUntil(() => popup.Result.HasValue);Result = popup.Result.Value;}
}
public class Popup : MonoBehaviour
{public enum ResultType { Ok, Cancel }public ResultType? Result;[SerializeField] private Text _sampleText;public void Initialize(string text){_sampleText.text = text;}private void OnOkPressed(){Result = ResultType.Ok;}private void OnCancelPressed(){Result = ResultType.Cancel;}
}

5. Transaction

setters 可能非常危险,例如:

_mainPlayer.SetGold(userInput);

  一种更安全的方法是将写入操作限制在非常具体的地方,这些地方有一个具体的、明确的原因。 这种额外的安全性可以通过提供一个可注入的只读接口并保持其可写对象引用来简单地实现。

  只读接口(例如 _mainPlayer.GetGold() )可以注入到每种类型中,尤其是在用户界面中,而启用写的对象引用保持实例化但不可注入。

  可写对象仅适用于派生自 Transaction 的类。 事务是原子的,可以远程跟踪以增强安全性和可调试性。 它们在目标类上执行。

public interface IMainPlayer
{int Level { get; }IResource Gold { get; }
}public class MainPlayer : IMainPlayer
{public int Level { get { return _level; } }public IResource Gold { get { return _gold; } }public void ExecuteTransaction(MainPlayerTransaction transaction){_injector.Inject(transaction);transaction.Execute(this);MarkDirty();}public void SetLevel(int newLevel) { _level = newLevel; }
}public class UpdateAfterRoundTransaction : MainPlayerTransaction
{public UpdateAfterRoundTransaction(GameState gameState, string reason){_gameState = gameState;_reason = reason;}public override void Execute(MainPlayer mainPlayer){Debug.Log("Updating after round for reason: " + _reason);mainPlayer.SetLevel(_gameState.Level);mainPlayer.Gold.Set(_gameState.Gold);}
}public class FinishRoundCommand : BaseCommand
{public bool Successful;[Inject] private IMainPlayer _mainPlayer;[Inject] private IBackend _backend;public IEnumerator Run(IngameStatistics statistics){Successful = false;var eorCall = new FinisHRoundCall(statistics);yield return _backend.Request(eorCall);var gameState = eorCall.ParseResponse();_mainPlayer.ExecuteTransaction(new UpdateAfterRoundTransaction(gameState, "Normal end of round response"));Successful = gameState.Successful;}
}

6. 信号/事件与轮询

让我们从简单的非官方定义开始:

  • 信号/事件
    可以订阅这些对象以接收有关值的未来更新的信息。
  • 轮询
    每 x 帧检查一个变量的当前值。

  两者都反映了有机会对变量的变化做出反应的想法,例如 购买包裹后,在金色标签上制作动画。 现在让我们讨论一些相关的差异。

  事件总是比轮询更有效。 时期。 事件的主要缺点是它们的复杂性随着具体过程所需的数量呈指数增长。 例如。 在HP文本框中显示的文本取决于当前生命数量、无限生命等修饰符、互联网连接、教程状态、玩家级别、特殊玩家特权等。此外,您可能会忘记取消注册信号, 这最终将导致致命的崩溃。 在这些情况下,以对 Unity 友好的方式进行轮询通常是更好的选择。

  执行轮询的推荐方法是使用在设置阶段启动一次的协程。 它在后台运行,每次执行时,您都可以确定您正在使用当前状态。

public class LivesView : MonoBehaviour
{private void Start(){StartCoroutine(MainLoop());}private IEnumerator MainLoop(){var wait = new WaitForSeconds(1);while (true){var hasUnlimitedLives = _mainPlayer.HasUnlimitedLives;var waitForNewLive = _mainPlayer.Lives == 0;if (hasUnlimitedLives){SetCurrentState(State.Unlimited);_livesUnlimitedCountdownTimer.SetTarget(_mainPlayer.Lives.UnlimitedEndDate.Value);}else if (waitForNewLive){SetCurrentState(State.NewLifeIn);_newlifeInCountdownTimer.SetTarget(_mainPlayer.DateTimeOfNewLife.Value);}else{SetCurrentState(State.Normal);if (_mainPlayer.Lives != _lastAmount){_lastAmount = _mainPlayer.Lives;_livesAmountText.AnimateTo(_lastAmount);}}yield return wait;}}
}

7.构建Pipeline

我设置的构建管道由三种协作技术组成:

  • Docker (可选).
    它允许快速部署预装了构建项目所需的环境(Unity、NDK、Android SDK 等)的 Linux 容器。 一旦你启动它,它就可以编译了,不需要进一步的设置。
    Docker 可以帮助您,尤其是在这里,因为不再需要手动配置和更新(Jenkins?)构建节点。 它们在 Mac 和 Linux 中运行,构建速度非常快。

  • Bitrise
    它是构建运行器软件,将在您的主机(docker 映像或真实主机)中执行。 它负责从高级别的角度管理构建过程。 就我而言:

    • Unity 许可证激活。
    • Unity 构建过程。
    • Unity 许可证停用。
    • 上传到 Hockeyapp/aws/testflight
    • 将构建链接发布到 Slack。

  uTomate/UBS/Jenkins 脚本。 归根结底,您仍然需要一些从低级别控制构建的技术:例如设置版本、更改目标平台、纹理压缩、纹理图集、资产包等。

  如果你身边有经验丰富的人,我建议你试一试。 使用它,您可以获得稳定、强大且可维护的构建管道。 更多信息在我之前的博文中。
  在我们的案例中,我们实现了一些有用的构建步骤来自动化重复性任务:

  • 检查场景未分配的引用
  • 纹理图集创建
  • 纹理压缩格式
  • Asset bundles

我希望这些信息对您有所帮助。 如果您有反馈,请发表评论。 对 Wooga 的人们有很多功劳!

[Unity 架构] 更好的 Unity 游戏架构相关推荐

  1. 游戏行业如何上云?阿里云架构师解读四大主流游戏架构

    游戏行业是阿里云最早聚焦的行业之一,近年来游戏行业的变化.云计算产品技术的变化都与日俱进.随着行业业务的变化.技术架构的演进以及阿里云产品的迭代演进,整体的产品技术选型在不同的游戏场景.业务场景也不尽 ...

  2. 阿里云架构师解读四大主流游戏架构

    一.概述 游戏行业是阿里云最早聚焦的行业之一,近年来游戏行业的变化.云计算产品技术的变化都与日俱进.随着行业业务的变化.技术架构的演进以及阿里云产品的迭代演进,整体的产品技术选型在不同的游戏场景.业务 ...

  3. 【深度分享】阿里云架构师解读四大主流游戏架构

    文丨冯宇  阿里云弹性计算产品解决方案架构师 游戏行业是阿里云最早聚焦的行业之一,近年来游戏行业的变化.云计算产品技术的变化都与日俱进.随着行业业务的变化.技术架构的演进以及阿里云产品的迭代演进,整体 ...

  4. 让架构更简单,QCon上海2016热点前瞻

    架构设计是软件设计过程中最重要的部分之一,是降低成本.改进质量.按时和按需交付产品的关键因素,架构设计的优劣会直接影响到目标系统的各个质量属性.对于软件开发而言,架构设计能保证系统质量,可以全过程指导 ...

  5. 游戏云间之五:游戏架构

    说起架构,分为两块, 一个是软件层次的代码架构 , 另外一个是硬件层次的系统架构. 软件层次的,模块划分.代码重构及业务层的架构为主.系统层次的,以网络.部署.服务器集群为主.软件层次的架构,在于前期 ...

  6. 游戏架构 云游戏(5)-游戏架构

    说起架构,分为两块, 一个是软件层次的代码架构 , 另外一个是硬件层次的系统架构. 软件层次的,模块划分.代码重构及业务层的架构为主.系统层次的,以网络.部署.服务器集群为主.软件层次的架构,在于前期 ...

  7. 同时看过 unreal4 和 Unity 源代码的人觉得哪个引擎架构更好?

    同时看过 unreal4 和 Unity 源代码的人觉得哪个引擎架构更好? UE VS U3D 技术策略上 U3D技术策略是很保守的,发出来的featurelist测试覆盖率无可非议,开发者无需多少新 ...

  8. 【Unity学习笔记】b站Unity架构课Unity3D 商业化的网络游戏架构(高级/主程级别)

    [Unity学习笔记]b站Unity架构课Unity3D 商业化的网络游戏架构(高级/主程级别) 自己跟着学完了,写了不少代码,会放在CSDN代码库,因为老师并没有提供源码,录屏也不是完全连续,所以难 ...

  9. moba寻路_GitHub - ylmbtm/MoBaDemo: 用Unity做的一个类Moba游戏Demo

    游戏整体逻辑 出兵逻辑 游戏会在两个阵营的固定区域在固定的频率中出兵.小兵只能沿着规定好的轨道进行行走,并按照各个防守塔的顺序向敌方进行进攻. 需要注意的点 对于固定出现的小兵来说,不适合在其死亡之后 ...

最新文章

  1. 探秘云游戏背后实时音视频技术实践
  2. getFilterFromRunTimeService - what is the trigger point of data load
  3. ZZULIOJ 1052:数列求和4
  4. java复习系列[6] - Java集合
  5. php常见问题辨析(二)
  6. 江山三侠—Flash短片轻松学(第2季)
  7. 百旺如何看是否清卡_​百旺如何看是否清卡
  8. java人员的宝贝:百宝箱。
  9. 小程序登录问题--登录函数getUserInfo()写在app.js中,首次加载无法获取后台处理过的用户的信息,刷新一次后就可以获取的解决
  10. PHP ob缓存页面静态化技术
  11. Maven项目+MVC三层架构+Mysql+Tomcat+私教预约系统前后端(私教、用户、管理员)+可以用于学习SSM框架、javaweb、maven项目入门
  12. 分享下剪辑师必须知道的13个剪辑技巧!
  13. 26. 删除排序数组中的重复项Leetcode
  14. FDFS_Ubuntu部署fdfs测试上传文件不成功
  15. AES-128加解密工具类
  16. 网上购物支付方式有哪些
  17. spring webmvc原理
  18. 【Java全栈】Java全栈学习路线及项目全资料总结【JavaSE+Web基础+大前端进阶+SSM+微服务+Linux+JavaEE】
  19. access如何查询两张表的内容_计算机二级access题库
  20. Java窗口游戏开发,飞机大战,打飞机,打大飞机,打无敌飞机妙啊!!!!————————香啊~~~~~~~~~~~~~~~~~

热门文章

  1. 2010福布斯中国富豪榜榜单(前50名)
  2. Java程序员:java游戏开发引擎
  3. 系统集成项目管理工程师考试时间
  4. 黑莓9000软件测试面试,初步测试有5大发现_黑莓9000 Bold - CNMO
  5. SRPG游戏开发(七)第三章 绘制地图 - 四 初步完善地图编辑器(Map Graph)
  6. 我发现凡是给offer的公司,面试时基本不问技术细节,那些问得又多又细的公司,后面就没下文了
  7. Java实现crc16校验 附上校验工具对照。解决长数据校验不正确的问题
  8. c语言教程英文版讲义,c语言教程英文版讲义(四).pdf
  9. 算法竞赛中的JAVA使用笔记(转载)
  10. 草食系的“恋爱秘方”