Unity是一个出色的游戏开发平台。 它基本上是免费的(除非您开始用它赚很多钱),易于使用,并且可以很快产生令人印象深刻的结果。 但是,在规模较大的项目中使用统一时,可能很难保持良好的技术结构。 在大多数平台上,通常使用诸如Model View Controller之类的设计模式来保持所有内容的组织性和灵活性。 但是由于在Unity上几乎所有事物都自动成为MonoBehaviour,因此当您的项目确实需要共享的中央智能时,很容易最终获得许多单独运行的组件。 最重要的是,Unity的资产管理并不总是像您希望的那样透明,尤其是动态资产加载可能是一件棘手的事情。 我自己做了一些更认真的项目后,我想我会分享自己的发现。
单身人士
Unity的组件系统是在游戏对象上添加和重用功能的一种非常强大的方法。 但是在大多数项目中,您很快就会感到您的组件需要与其他GameObjects或MonoBehaviours进行通信。 乍一看,这似乎是不可能的,但幸运的是,事实并非如此。 实际上,使用单例可以轻松实现。
- 使用标准资产套件以正确的方式开始在Unity内部学习
- “来自开发者”:CollegiateJokes吸引玩家加入您的游戏
- 学习爱(2d)游戏开发0 —用CSCI E-23a构建Pong
- A:\ dventure开发日志:模拟基本市场经济学
- 游戏:可访问性/排他性
什么是单身? 单例是一类,不能在任何地方构造。 实际上,在干净的单例实现中,构造函数甚至是私有的或受保护的,因此只能靠自身构造。 而是,类包含返回该类实例的静态函数或属性。 通常将此函数称为“ getInstance()”之类的东西。 单例类通常具有一个包含其自身唯一实例的私有静态变量。 在项目开始时,此变量包含null。 调用getInstance()时,它将检查变量(仍然)是否包含null。 如果是这样,它将创建自己的实例(因为它可以访问自己的私有构造函数)并将其存储在静态变量中。 然后无论如何它都会在变量中返回实例。 这样一来,此类始终只有1个实例。
尽管Unity项目中的许多类都是MonoBehaviours,但是仍然可以使用其他类型。 可以从任何地方访问Singleton,因此它们非常适合用作控制器或管理器,因为可以从任何组件访问它们。
单接入点
但是要小心,不要创建一大堆单例。 如果某个项目中某个时刻有大约15个单例,那么它将变成一个猜谜游戏,对于这个游戏,有一个单例及其所谓的东西。 如果确实需要从任何位置访问许多对象,则最好使用1个单例,并为其指定许多命名成员变量。 这样,您始终有一个共同的起点,并且可以轻松找到可以从那里访问的成员。 我倾向于为此使用“依赖项”单例。 这样,您可以通过以下方式访问当前级别:
Dependencies.shared.LevelManager.CurrentLevel
如果某个项目中某个时候有大约15个单例,那么它将变得有点猜谜游戏。
技术设计
通过具有一些与场景层次结构分开的管理实体,您可以在项目中实现更多的结构。 现在您已经可以做到这一点,花点时间考虑一下如何从技术上组织项目。 您不必走到使用Model View Controller的地步,但对项目的结构以及允许与之对话的内容有一个大致的了解确实有很大帮助。
以我的经验,在场景层次结构外部的代码与其内部代码之间进行某种单向控制非常有效。 我通常会让经理们更像“模范”。 他们大多只控制游戏的基础数据,并将对这些数据的任何更改作为事件进行分发。 Unity GameObjects和Components只是聆听并做出相应调整。 他们还在自己的Start方法中将自己设置为适当的状态,因此管理人员必须尽可能少地了解项目的Unity方面。 这意味着您可以真正自由地显示数据。 您甚至可以同时以多种方式显示它。 想想一个小地图:实际的游戏角色会根据数据更新其位置,但与此同时,小地图上的点也会根据相同的数据更新其位置。 您可以添加此小地图,而无需更改管理者的代码。
花时间考虑一下如何组织项目
除了可访问性之外,这些非MonoBehaviour单身人士还有另一个巨大好处:卸载场景时它们不会走到任何地方。 您有时也希望在Unity组件中看到此功能。 幸运的是Unity为您提供了一种方法。 你可以打电话
DontDestroyOnLoad(this.gameObject)
来自任何组件或任何GameObject。 从调用此刻起,当您卸载场景或切换到另一个场景时,指定的GameObject及其所有子代将不再被销毁。 这对于在切换到其他环境(例如HUD或某些类型的叠加层)时需要记住其状态的事物特别有用。
缺点是,尽管此命令可以防止在卸载场景时破坏对象,但并不能防止在重新加载场景时再次创建该对象。 几次切换到不同的环境后,最终可能会出现很多HUD实例彼此重叠的情况,而可能根本没有注意到它。 为了解决这个问题,您必须自己为这些组件实现单例行为。 这是我执行此操作的代码:
使用UnityEngine;公共类SomeBehaviour:MonoBehaviour {保护静态UnityHelper sharedInstance = null; 无效Awake(){ 如果(UnityHelper.sharedInstance == null){ UnityHelper.sharedInstance = this; DontDestroyOnLoad(this.gameObject); }其他{ 销毁(this.gameObject); } } }
当使用在切换场景时不会销毁的对象时,将它们捆绑到一个(预制的)游戏对象中可能是一个好主意,您可以在任何场景中重复使用这些对象。 另一个好处是您只需要防止根GameObject被破坏。 它的子代也会自动保留。
我确实认为这是应该稀疏使用的东西。 尽管在场景之间保留某些对象及其状态非常方便,但是如果所有内容都尽可能独立于状态,则将创建一个更灵活的项目。 如果某些东西可以在初始化时适当地适应数据状态,则可以更自由地使用它。 因此,无论何时创建DontDestroyOnLoad对象,都要确保有充分的理由。
如果一切都尽可能独立于状态,则您将创建一个更灵活的项目
Unity在Assets文件夹中有一个系统文件夹,您可以将其用于需要动态访问的任何资源。 从理论上讲,这听起来很方便。 该文件夹的问题是Unity特别建议您不要使用它。 它似乎很容易使用,主要是为了快速制作原型。 如果使用不当(经常阅读),它可能会减慢启动时间并严重缩短构建时间。 它还会导致Unity的内存管理出现问题。 因此,似乎确实要谨慎使用。
资源文件夹的问题是Unity特别建议您不要使用它。
工厂工厂
不过,您仍然经常必须动态加载内容,那么该怎么做呢? 我的解决方案是使用工厂组件。
工厂是一个类,其唯一目的是实例化许多通常具有相似类型的其他类。 如果您的游戏中有很多类型的敌人,那么最好的方法是,创建关卡的代码不必知道有哪些类型的敌人就可以构建它们。 如果要添加敌人类型,则必须更改所有需要构造敌人的类。 要解决此问题,可以使用工厂。 工厂类包含一个函数,例如“ createEnemy(string敌人类型)”,该函数返回适当的敌人。 添加敌人类型后,您只需更改出厂等级即可。
假设您需要实例化敌人对象,则将创建一个EnemyFactory。 对于所有敌人类型,此组件将具有一个GameObject类型的公共变量,您可以在其中存储相应的预制件。 代码看起来像这样:
使用UnityEngine;公共类EnemyFactory:MonoBehaviour {public GameObject MeleeAttackEnemyPrefab; 公共GameObject RangedAttackEnemyPrefab; 公共GameObject HeavyAttackEnemyPrefab; public GameObject CreateEnemy(string type,GameObject parentObject){GameObject prefab = null; GameObject敌人= null; 开关(类型){ 案例“ MeleeAttackEnemy”: prefab = this.MeleeAttackEnemyPrefab; 打破; 案例“ RangedAttackEnemy”: 预制= this.RangedAttackEnemyPrefab; 打破; 案例“ MeleeAttackEnemy”: prefab = this.HeavyAttackEnemyPrefab; 打破; } 如果(预制!=空){ GameObject skin =(GameObject)GameObject.Instantiate(prefab); } 如果(敌人!= null){ transform.SetParent(parentObject.transform,false); } 返回敌人 } }
工厂中的公共预制变量必须通过Unity编辑器进行设置。 这意味着它必须位于场景层次结构中的某个位置。 尽管这还使您能够将工厂分配给其他组件的成员变量,但这不是一种非常干净灵活的访问方法。 我倾向于做的就是改善所有需要大量访问的工厂,并将它们自己存储在Awake()方法中依赖对象的实例var中。 这样,您可以从任何地方实例化敌人(或工厂所适用的任何类型)。
规则例外
在大多数项目中,“资源”文件夹中仍然有一些文件。 通常,它们只是JSON文件,其中包含诸如级别描述符之类的内容以及其他必须由设计人员进行编辑的定义。 您可能也可以将它们保留在工厂中,但是到目前为止,我还没有发现将其中一些保留在参考资料中的任何缺点。 从技术上来说,这可能不是最好的方法,但是它确实使编辑某些内容变得更容易,并且对设计师或其他技术含量较低的团队成员也更易于访问。
Unity实际上并不支持线程。 通过使用Unity的协程,您仍然可以实现类似的目的。 如上所述,在较大的项目中使用的组件层次之外的数据模型,我经常需要在不是MonoBehaviours或GameObjects的对象中使用协程。
解决方案是我的Dependencies对象中的另一个对象,我称之为“ UnityHelper”。 这个辅助对象不过是一个MonoBehaviour,它可以防止在(卸载)场景时破坏该对象,并且可以从任何地方对其进行访问。 每当需要运行协程时,都可以简单地使用此共享组件来运行它。 当某些非MonoBehaviour对象在运行Update或FixedUpdate方法时需要执行某些操作时,这也很有用。 辅助对象可以使用这两种方法中的任何一种调度事件,以通知您的模型对象正在运行。
每当需要运行协程时,都可以简单地使用此共享组件来运行它。
Unity是快速设置游戏并快速产生结果的出色工具。 在较大的项目中使用它时,您可能需要找到更多的方法,但是当您这样做时,我相信Unity仍然非常适合于此。 我希望以上内容可以帮助某些人弄清楚如何很好地设置他们的项目,并使他们继续享受Unity。