Unity3D:游戏架构,ScriptableObjects,单调

今天,我们将讨论如何在游戏内部存储,接收和传输数据。 关于称为ScriptableObject的奇妙事物,以及它为何如此奇妙。 让我们谈谈单调在组织场景和它们之间的过渡时的好处。

本文介绍了一种漫长而痛苦的游戏开发方式,以及该过程中使用的各种方法。 最有可能的是,对于初学者来说会有很多有用的信息,而对于“退伍军人”来说并没有什么新鲜的东西。

脚本和对象之间的链接


新手开发人员面临的第一个问题是如何将所有编写的类链接在一起并配置它们之间的交互。

最简单的方法是直接指定指向该类的链接:

public class MyScript : MonoBehaviour { public OtherScript otherScript; } 

然后-通过检查器手动绑定脚本。

这种方法至少具有一个重大缺陷-当脚本数量超过几十个,并且每个脚本都需要两个或三个相互链接时,游戏很快就会变成网络。 一看她就足以引起头痛。

(在我看来)最好是组织一个消息和订阅系统,在该系统中,我们的对象将接收所需的信息-仅此而已! -无需相互之间有六个链接。

但是,在探究了这个话题之后,我发现Unity中所有现成的解决方案都会受到不懒惰的所有人的责骂。 在我看来,从头开始编写这样的系统是一项艰巨的任务,因此,我将寻找更简单的解决方案。

可编写脚本的对象


关于ScriptableObject,基本上有两件事要了解:

  • 它们是Unity内部实现的功能的一部分,例如MonoBehaviour。
  • 与MonoBehaviour不同,它们不与场景对象绑定,而是作为单独的资产存在,并且能够在游戏会话之间存储和传输数据

我立即爱上了他们的热爱。 在某种程度上,它们已成为我解决任何问题的灵丹妙药:

  • 需要存储游戏设置吗? ScriptableObject!
  • 创建库存? ScriptableObject!
  • 写AI? ScriptableObject!
  • 记录有关角色,敌人,物品的信息吗? ScriptableObject将永远不会让您失望!

我三思而后行,创建了几个ScriptableObject类型的类,然后为它们创建了一个存储库:

 public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; } 

每个对象本身都存储所有有用的信息,并可能链接到其他对象。 它们每个都足以通过检查器绑定一次-它们将不会移到其他任何地方。

现在,我不需要在脚本之间指定无限数量的链接! 对于每个脚本,我可以一次指定到我的存储库的链接-它会从那里接收所有信息。

因此,字符速度的计算看起来非常优美:

 //   float speed = database.playerData.speed; //    if (database.spellController.haste.active) speed = speed * database.spellController.haste.speedModifier; // ,     if (database.playerData.health<database.playerData.healthThreshold) speed = speed * database.playerData.woundedModifier; 

并且,例如,如果陷阱仅应向正在运行的角色触发:

 if (database.playerData.isSprinting) Activate(); 

而且,角色不需要了解任何有关咒语或陷阱的知识。 它只是从存储中检索数据。 还不错吧 还不错

但是几乎立即我遇到了问题。 ScriptableOnjects无法直接存储指向场景对象的链接。 换句话说,我无法创建指向玩家的链接,无法通过检查器将其绑定,并且永远忘记玩家的坐标问题。

而且,如果您考虑一下,这很有道理! 资产存在于后台,可以在任何场景中访问。 如果您留下指向资产内另一个场景中的对象的链接,会发生什么?

没事

长期以来,拐杖为我工作:在存储库中创建了一个公共链接,然后每个对象(您要记住的链接)都填充了该链接:

 public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } } 

因此,无论场景如何,我的存储库首先都会收到指向播放器的链接并记住该链接。 现在,任何人都说,敌人不应存储与玩家的链接,也不应通过FindWithTag()寻找他(这是一个非常耗费资源的过程)。 他所做的只是访问存储库:

 public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; } 

看起来:系统是完美的! 但是没有 我们仍然有2个问题。

  1. 我仍然必须为每个脚本手动指定到存储库的链接。
  2. 将引用分配给ScriptableObject中的场景对象很麻烦。

关于第二个更详细。 假设一个玩家有一个轻咒。 玩家将其投放,游戏对商店说:光线已投射!

 database.spellController.light.CastSpell(); 

这引起了一系列反应:

  • 在光标点处创建了一个新的(或旧的)游戏对象光源。
  • 将启动一个GUI模块,告诉我们该指示灯已激活。
  • 比如说,敌人会获得暂时的奖励来寻找玩家。

怎么做?

他们可能会说,每个对灯光感兴趣的对象都可以直接在Update()中写入,这样,每个帧都跟随灯光(如果(if(database.spellController.light.isActive)),并且当灯光点亮时,做出反应! 并且不必担心此检查有90%的时间会闲置。 在几百个对象上。

或以准备好的链接的形式组织所有这一切。 事实证明,简单的CastSpell()函数应该有权访问播放器,灯光和敌人列表。 这是最好的。 很多链接,对吧?

当然,您可以在场景开始时将所有重要的内容保存在我们的存储库中,分散资产上的链接,这些资产通常不用于此目的……但是我再次违反了单个存储库的原理,将其变成了一个链接网。

辛格尔顿


这就是单例发挥作用的地方。 实际上,这是一个仅存在于一个副本中的对象(并且可以存在)。

 public class GameController : MonoBehaviour { public static GameController Instance; //   ,      public Database database; public GameObject player; public GameObject GUI; public List<Enemy> enemies; public List<Spell> spells; void Awake () { if (Instance == null) { DontDestroyOnLoad (gameObject); Instance = this; } else if (Instance != this) { Destroy (gameObject); } } } 

我将其捕捉到一个空的场景对象。 我们称之为GameController。

因此,我在场景中有一个对象,用于存储有关游戏的所有信息。 此外,它可以在场景之间移动,销毁其双打图像(如果新场景上已经存在另一个GameController),在场景之间传输数据,以及在需要时实现保存/加载游戏。

在已经编写的所有脚本中,您可以删除到数据仓库的链接。 毕竟,现在我不需要手动配置它。 对场景对象的所有引用都将从商店中删除,并转移到我们的GameController中(退出游戏时,我们很可能需要它们来保存场景状态)。 然后,以对我方便的方式用所有必要的信息填充它。 例如,在玩家和敌人(以及场景的重要对象)的Awake()中,规定在GameController中向自己添加链接。 由于我现在正在处理Monobehaviour,因此对场景中对象的引用非常有机地融入其中。

我们得到什么?

任何对象都可以获取有关他需要的游戏的任何信息:

 if (GameController.Instance.database.playerData.isSprinting) ActivateTrap(); 

在这种情况下,绝对不需要配置对象之间的链接,所有内容都存储在我们的GameController中。

现在,维持场景状态将没有什么复杂的事情。 毕竟,我们已经拥有所有必要的信息:敌人,物品,玩家位置,数据仓库。 选择有关要保存的场景的信息,并在退出场景时使用FileStream将其写入文件就足够了。

危险


如果您读到这一点,我应该警告您这种方法的危险。

一个非常糟糕的情况是,许多脚本在ScriptableObject中引用了一个变量。 获取值没有错,但是当它们开始从不同地方影响变量时,这是潜在的威胁。

如果我们有一个保存的playerSpeed变量,并且我们需要玩家以不同的速度移动,则我们不应该更改存储中的playerSpeed,而应该获取它,将其保存在一个临时变量中,并且已经对其应用了速度修改器。

第二点-如果任何对象可以访问任何东西-这是很大的力量。 而且责任重大。 而且您需要谨慎处理它,以便某些真菌脚本不会完全破坏您所有的AI敌人。 正确配置的封装将减少风险,但不会使您摆脱风险。

同样,不要忘记单调是温柔的生物。 不要滥用它们。

今天就这些了。

Unity官方教程收集了很多内容,非官方的教程收集了很多内容。 我必须自己去做些事情。 因此,上述方法可能存在其危险和不利之处,而我没有想到。

因此,欢迎进行讨论!

Source: https://habr.com/ru/post/zh-CN414361/


All Articles