Game architecture with ScriptableObjects | Open Projects Devlog
Summary
TLDR本期开发日志介绍了Unity首个“开放项目”游戏架构的设计,使用了ScriptableObjects来创建灵活、模块化和可扩展的游戏架构。通过避免硬编码和Singleton模式的问题,项目利用ScriptableObjects作为数据容器和事件通信的中间点,实现了跨场景的全局访问和一致性。同时,项目鼓励社区贡献和学习,提供了GitHub链接和相关资源,以促进项目的持续发展和功能的丰富。
Takeaways
- 📝 这是Unity首个“开放项目”的开发日志,名为'Chop Chop',是一个经典动作冒险游戏,允许社区成员自由协作并贡献于整个游戏开发过程。
- 🔗 项目链接和相关信息可以在视频描述中找到,鼓励开发者根据专长贡献代码或学习新知识。
- 💻 游戏开发使用Unity 2019.4 LTS版本以确保稳定性,但为了与最新Unity版本兼容,项目已升级至Unity 2020.2版本。
- 🎯 开放项目的目标是展示有价值的开发模式和技术,分享有用的工具,并创建一个灵活、模块化、可扩展的游戏架构。
- 🔗 硬编码连接在项目初期可能方便,但随着项目扩大,会导致可扩展性问题和难以追踪的错误。
- 🔄 单例模式是一种常用解决方案,它使用一个全局可访问的类实例,适用于全局管理器,但大型项目中会引入依赖问题。
- 📦 ScriptableObjects是数据容器,类似于材料或3D模型,它们不依赖于播放模式,数据全局可访问且与场景无关。
- 🗣️ ScriptableObjects用于存储对话行、声音预设和库存项目等数据,易于设计师修改,并且所有脚本都可以访问。
- 🎮 ScriptableObjects还可以作为Unity功能的扩展层,以重用通用功能和连接系统,例如InputReader用于集中处理玩家输入。
- 📡 使用ScriptableObjects作为通信的中间点,创建了灵活的事件系统,避免了场景内对象之间的依赖。
- 🔊 通过ScriptableObjects实现的音频系统允许在不同场景中播放声音,AudioManager脚本根据请求激活AudioSources。
- 🔄 加载系统使用ScriptableObjects实现场景切换,通过LocationExit脚本触发事件,而LocationLoader管理器负责加载所有场景。
- 🛠️ 项目正在开发自定义检查器和编辑器窗口,以更舒适地创建和管理ScriptableObjects,避免手动插入场景名称的错误。
- 🌟 鼓励社区成员参与开发,下载项目进行实验,提出新特性,或在自己的游戏中重用部分代码。
Q & A
什么是'Open Project',它有什么特点?
-‘Open Project’是一种开放源代码的游戏项目,允许创作者社区自由地协作并积极参与整个游戏开发过程。参与者可以根据自己的专长做出贡献,或者学习新知识。
Unity 'Open Project'的第一个项目叫什么名字,它属于哪种游戏类型?
-Unity 'Open Project'的第一个项目名为'Chop Chop',它属于经典的动作冒险游戏类型。
为什么Unity 2019.4 LTS版本被选为开发'Chop Chop'的主要版本?
-Unity 2019.4 LTS版本被选为开发'Chop Chop'的主要版本,因为它能提供最佳的开发稳定性。
ScriptableObject和MonoBehaviour有什么区别?
-ScriptableObject是数据容器,它们像材质或3D模型一样是资源,不依赖于应用程序的播放状态,可以存储在播放模式之外的值。而MonoBehaviour通常被称为脚本,是附加到GameObject上的组件,它们的值在退出播放模式时会被重置。
为什么在游戏开发中使用硬编码连接可能会导致问题?
-硬编码连接在项目规模扩大时可能导致严重的可扩展性问题,并引入难以追踪的意外错误。
Singleton模式是什么,它在Unity中通常用于解决什么问题?
-Singleton模式是一种编程模式,它使用一个类的唯一、全局可访问的实例,始终可用。这在创建全局管理器以持有全局可访问的变量和函数、实现跨多个场景的持久状态以及快速实现方面非常有用。
ScriptableObject在'Chop Chop'项目中扮演了什么角色?
-在'Chop Chop'项目中,ScriptableObject被用作数据容器,用于存储对话行、声音预设和库存项等。它们还可以作为创建Unity功能层的强大工具,以公开和重用公共功能并桥接系统。
如何使用ScriptableObject创建灵活的事件系统?
-通过使用ScriptableObject作为通信的中间点,可以创建灵活的事件系统。这些ScriptableObject被称为'channels',它们像广播电台一样,可以广播事件并监听它们,从而在两个或多个通信部分之间建立连接。
为什么使用ScriptableObject作为事件通信的中间点可以提高模块化和项目可维护性?
-使用ScriptableObject作为事件通信的中间点可以提高模块化和项目可维护性,因为事件的发送者和接收者不需要事先知道对方的存在,它们可以在隔离的层中独立存在,代表单一的入口点。
如何使用ScriptableObject来改进游戏的音频系统?
-通过创建一个名为'AudioCueEventChannelSO'的ScriptableObject类,可以将UnityAction包含在其中,请求三个参数:两个ScriptableObject引用和一个Vector3。这使得音频系统模块化,并且可以在不同的场景中播放声音。
如何使用ScriptableObject来实现游戏的加载系统?
-通过使用名为'LoadEventChannelSO'的ScriptableObject类,它包含一个接受GameSceneSO数组和布尔值的UnityAction。这允许从任何地方、任何时间请求场景变换。
为什么在所有channel脚本中在引发事件之前检查空值很重要?
-在所有channel脚本中在引发事件之前检查空值很重要,因为如果没有活动订阅者,会引发空引用异常。检查空值可以确保在没有监听器的情况下在控制台中显示通知,避免程序崩溃。
如何通过自定义检查器来改善ScriptableObject的工作流程?
-通过创建自定义检查器,可以以更舒适的方式编写ScriptableObject,有时还可以创建自定义编辑器窗口,将它们聚集在一个地方,减少设置不同资产中存储的值所需的繁琐来回操作。
如何避免在ScriptableObject中手动插入场景名称时出现的错误?
-通过使用自定义检查器,例如GameSceneSOEditor脚本中的例子,可以从真实场景引用的列表中选择场景名称,避免手动插入,这样可以减少错误并防止在更新场景时破坏ScriptableObject中使用的引用。
Outlines
😀 Unity'Open Project'游戏架构设计
本视频日志介绍了Unity首个开源项目'Chop Chop'的游戏架构设计。'Open Projects'允许创作者社区自由协作,贡献于整个游戏开发过程。项目使用Unity 2019.4 LTS确保开发稳定性,并升级至Unity 2020.2以展示最新Unity版本。项目目标是展示有价值的开发模式和技术,分享实用工具,创建灵活、模块化、可扩展的游戏架构。介绍了Singleton模式的局限性,并提出使用ScriptableObjects作为解决方案,它们是数据容器,可在Play Mode之外存储全局访问和场景独立的数据。
😉 ScriptableObjects的高级应用
ScriptableObjects不仅作为数据容器,还用于创建Unity特性之上的层,以公开和重用共同功能,连接系统。例如,InputReader ScriptableObject作为中央事件中继器,监听玩家输入并触发UnityActions。这种解决方案提高了模块化和项目的可维护性。进一步使用ScriptableObjects创建灵活的事件系统,通过'channels'作为通信中介,避免了场景内对象之间的直接依赖。项目中实现了多种类型的channel ScriptableObjects,用于不同用途,提高了事件的通用性和可重用性。
🎉 组织和优化ScriptableObjects工作流程
随着项目功能的日益丰富,保持组织和易于访问变得至关重要。团队计划创建自定义检查器和编辑器窗口,以更舒适的方式编写ScriptableObjects,减少设置不同资产中存储的值所需的来回操作。例如,GameSceneSOEditor脚本通过列表选择场景名称,避免了手动插入和潜在的错误。视频鼓励观众了解ScriptableObjects的优势,并参与Open Project的开发,下载项目进行实验,提出新功能,或在自己的游戏中重用其部分。项目将持续更新,观众可以通过GitHub仓库的路线图、日常变更和双周直播流来跟进项目进展。
Mindmap
Keywords
💡ScriptableObjects
💡Open Projects
💡Unity 2019.4 LTS
💡Singleton模式
💡数据容器
💡InputReader
💡UnityAction
💡事件系统
💡AudioCueEventChannelSO
💡GameSceneSO
💡LocationExit
💡自定义检查器
Highlights
使用ScriptableObjects设计Unity游戏架构,为游戏开发提供灵活性、模块化和可扩展性。
介绍“Open Projects”,即开源游戏项目,允许创作者社区自由协作并积极参与整个游戏开发过程。
首个项目名为“Chop Chop”,采用经典动作冒险游戏类型。
项目使用Unity 2019.4 LTS版本以保证开发稳定性,并升级至Unity 2020.2版本以适应最新Unity版本。
ScriptableObjects与MonoBehaviours的区别,前者作为数据容器,不依赖于游戏的播放状态。
ScriptableObjects作为数据容器,存储对话行、声音预设和库存项等,提高数据的全局访问性和场景独立性。
ScriptableObjects用于创建Unity特性之上的层,以公开和重用公共功能并桥接系统。
介绍InputReader ScriptableObject,作为集中事件中继器,监听玩家输入并触发相关的UnityActions。
使用ScriptableObjects创建灵活的事件系统,避免场景内对象之间的直接依赖。
ScriptableObjects作为通信的中间点,称为“channels”,用于广播和监听事件。
实现“VoidEventChannelSO”,一种通用事件通道,用于传播不带数据的事件。
通过ScriptableObjects模块化游戏音频系统,使用“AudioCueEventChannelSO”进行音频播放请求。
创建不同的事件通道有助于组织和逻辑上划分功能。
介绍基于ScriptableObjects的加载系统,允许从任何地方请求场景更改。
使用“LoadEventChannelSO”进行场景加载请求,包含场景信息和是否显示加载屏幕的布尔值。
设计LocationLoader管理器,将所有场景添加性地加载到名为Initialization的场景上。
在所有通道脚本中检查空引用以避免null引用异常,并在没有监听器时在控制台显示通知。
目标是创建自定义检查器以舒适地创作ScriptableObjects,并有时创建自定义编辑器窗口以集中管理它们。
通过GameSceneSOEditor脚本示例,避免手动插入场景名称,减少错误并自动更新场景引用。
鼓励观众参与Open Project的开发,下载项目进行实验,提出新功能,并在自己的游戏中重用部分代码。
Transcripts
In this second devlog, we're going to share how we used ScriptableObjects
to design the game architecture of the first Unity "Open Project."
"Open Projects" are small, open-source games where the creator community
is free to collaborate and contribute actively to the entire game development journey.
You can make a contribution to them according to your expertise, or learn something new.
For this first project, titled “Chop Chop,” we went with a classic game genre: the action-adventure.
You can find the link to the GitHub project and all the other relevant links
to learn more and get involved in the video description below.
We are making the game using Unity 2019.4 LTS,
which will guarantee us the best stability during development.
But since we wanted to make the contents of this devlog series available to you in the latest Unity version,
we upgraded the project to work on Unity 2020.2,
the latest version available when recording this devlog.
You can find this 2020.2 version of the project on a Git branch of the original repository
called devlogs/2-scriptable-objects.
We conceived Open Projects with the goal of exposing valuable development patterns and techniques,
and to share useful tools.
One of our goals is to create a game architecture that is flexible, modular, and extensible.
When developing a game, it’s very common to have multiple GameObjects or Scenes
that need to share information or states with each other.
It’s handy and easy to create hard-coded connections between these entities, like,
referencing objects by string or running a search in the scene for a specific type of component.
But, as soon as the project grows,
rigid connections can lead to serious scalability problems
and introduce unexpected errors that can be hard to track down.
A popular solution is the Singleton pattern.
This programming pattern uses a single, globally-accessible instance of a class, available at all times.
This is useful to make global managers that hold variables and functions that are globally accessible,
achieve a persistent state across multiple scenes, and are fast to implement.
With a smaller project, this approach can be useful.
The problems come with larger projects.
When we’re referencing the instance of a Singleton class from another script,
we’re creating a dependency between these two classes.
Let’s assume we want to test the functionality provided by a certain Prefab,
and we drag and drop it into an empty scene.
If the Prefab contains code that calls and runs something inside a Singleton class,
we also need this Singleton GameObject in the new scene.
And, if the Singleton Class has other dependencies,
we’re basically recreating the full game logic to test an isolated function.
One system can’t exist without the other.
There are other issues with Singletons that we won’t go into right now.
We encourage you to independently look into the Singleton pattern to understand the pros and cons of it.
For our case, we went with quite a different approach,
which relies on ScriptableObjects - used in a specific way.
Before we dive into the specifics, let’s see what ScriptableObjects are
and how they differ from classic MonoBehaviours.
MonoBehaviours, commonly referred to as just “scripts”,
are components that are attached to GameObjects.
Changes made to their values are reset when you exit Play mode.
ScriptableObjects, on the other hand,
are assets just like a Material or a 3D model, and not components.
They don’t follow the MonoBehaviour lifecycle and don’t depend on the application’s play state.
Therefore, we can say that ScriptableObjects are data containers
that can hold values that also exist outside of Play Mode.
Plus, since they are assets,
the data we store inside them is globally accessible and scene-independent.
If you want to dive more into the basics for ScriptableObjects,
you can find links to other dedicated videos in the description below.
Our project indeed uses them as data containers for many features,
like to store dialog lines, sound presets, and inventory items.
You can find them in the Assets folder under ScriptableObjects.
If you select a DialogLine ScriptableObject,
you can see how it represents a fragment of a conversation between characters,
easy to be modified by a designer thanks to all the Inspector-exposed fields.
Like a databank ready to be accessed and available to all scripts.
Data storage is not the only use case for ScriptableObjects, though.
For instance, they can be a powerful tool to create layers on top of existing Unity features
to expose and reuse common functionality and bridge systems.
An example of this is our InputReader.
This ScriptableObject is not a simple data container
but an object capable of listening to the Inputs received from the player,
invoking the relevant UnityActions.
You could say it’s a centralized event-relayer.
For example,it implements a function called OnMove,
which listens to the Move input provided by Unity’s Input system.
We support both keyboard and gamepad in Chop Chop,
and each time Unity reads the value of any of these physical devices,
the OnMove function of the ScriptableObject is executed.
Then, this function invokes a UnityAction
so that all the objects subscribed to it will, in return, execute their callbacks.
Receiving input messages and propagating them from the InputReader ScriptableObject
allows us to reuse this logic across the project easily.
From here, an object like the Player script can reference the InputReader ScriptableObject
to tap into its UnityActions and implement a specialized response.
This solution also improves modularization and project maintainability.
The InputReader doesn’t need to know in advance who uses and accesses it
since it lives in an isolated layer and represents a single entry point.
This means that there’s only one place to modify if we want to add or remove inputs,
and the way different objects consume input is consistent and unified.
Almost like a Singleton, but without the cons!
Let’s take ScriptableObjects a step further,
and see how we used them to create a flexible event system in Chop Chop.
The idea is again to take advantage of the independent nature of ScriptableObjects.
Usually, when we communicate directly through events and delegates,
we reference the class that owns the data structure
and subscribe to start listening for messages.
This approach creates in-scene dependencies between the objects involved
since the event subscribers need to reference the senders to work.
We solved this using the ScriptableObjects as an intermediate point of communication.
We call these ScriptableObjects “channels” since, like in radio,
they represent channels to broadcast events on and listen to them,
creating a connection between two or more communicating parts.
We have several types of channel ScriptableObjects in the project, for different uses.
In general, the number and types of parameters that travel via the UnityAction contained in them
define how much a channel type is generic and reusable.
Starting simple, we implemented a very generic type called "VoidEventChannelSO",
which can propagate events with no data attached to them.
For instance, we use this to provide an event channel to close the game.
If any object raises the event on that channel, the game shuts down.
But let’s see some more interesting examples from our game!
We modularised the audio system in the game
thanks to a ScriptableObject class we called "AudioCueEventChannelSO".
The UnityAction contained in it requests three parameters: two ScriptableObject references and a Vector3.
The two ScriptableObjects are just data.
They reference the AudioClips we want to play and some playback settings.
The Vector3 informs the message receiver where we want to play the audio in the 3D space.
With this structure in place, we created two of these ScriptableObjects:
one where we propagate sound effects requests, and the other for music requests.
Creating different channels helps to organize and keep features logically ordered and subdivided.
Our AudioManager script then references both these ScriptableObject channels
and subscribes to their UnityActions.
Now each script that wants to play audio can reference these channels too
and perform a request by calling the ScriptableObject’s RaiseEvent method.
The AudioManager will listen to this request and activate one or more AudioSources from its pool.
It’s important to note that with this architecture,
the AudioManager and the GameObjects that request the sounds can live in different scenes,
and in fact, they do.
This allows us to play sounds across scenes, even when we load a new one.
In the demo assets we provide, look for a scene called AudioExamples
to find different examples of looping, one-shot, and composite sounds using this system.
Another feature we built on top of a ScriptableObject channel is the Loading System.
The logic is very similar to the Audio system
and allows, by referencing the right ScriptableObject channel,
to request a scene change at any time, from anywhere.
In this case, the class we use to create ScriptableObjects is named "LoadEventChannelSO"
and contains a UnityAction that accepts an array of GameSceneSO and a boolean value.
The GameSceneSO is a simple ScriptableObject asset that stores information on the scene we want to load.
The boolean value tells us if, during the loading phase, we want to show a loading screen.
In our scenes, we can now place a GameObject with a LocationExit script attached,
that calls the ScriptableObject’s RaiseEvent method when the player triggers the collider.
This trigger zone is just one of many possibilities we can implement to request a scene switch.
As a general note, we designed the LocationLoader manager
to load all the scenes additively on top of a scene called Initialization.
This separation allows us to use the Initialization scene as a container for scripts
with functions useful to all scenes,
like the same LocationLoader or AudioManager,
and then taking advantage of the flexibility of ScriptableObjects,
have these communicate with gameplay scenes.
If an event is invoked without active subscribers, we’d get a null reference exception.
For this reason, in all our channel scripts we check for null before raising the event.
If there aren’t listeners, a notification appears in the console.
Thanks to your contributions, the project is becoming richer in features and content every day.
For the team to stay productive, it’s important to keep everything organized and easy to access.
Especially with ScriptableObjects used as data containers that reference each other,
it can be easy for things to get messy.
One of our next goals is to create custom Inspectors to author ScriptableObjects in a comfortable way,
and sometimes custom Editor windows to gather them in one place.
This aims to reduce the annoying back and forth actions necessary to set up the values stored in different assets.
You can find an example of a custom Inspector in the GameSceneSOEditor script.
We made this enhancement to avoid inserting the scene’s names manually.
This action is error-prone, and if we update the scenes, we may break references used in the ScriptableObjects.
Now it’s possible to choose the scene’s name from a list containing the real scene references.
We hope that after watching this video,
you will get a good idea of the advantages that ScriptableObjects can bring
besides being data containers.
We’d also like to invite you to participate in the development of this Open Project,
and maybe help us improve these ScriptableObjects workflows.
Feel free to download the project and experiment with it,
propose new features, and reuse parts of it inside your own games!
Keep yourself updated on the first Open Project by following the roadmap,
the daily changes in the GitHub repository, and the bi-weekly live streams.
The project will keep upgrading and changing during development on the main branch,
but remember that you can always find this 2020.2 version
in a branch on the repository called devlog-2-scriptable-objects.
We can’t wait to see your feedback and contributions!
Thanks for watching!
Weitere ähnliche Videos ansehen
Why I Love Odoo as a Developer | Multiple Reasons
How I built 21 startups in 2 years
I made these 23 websites (and earned $562,943)
How to use Microsoft Azure AI Studio and Azure OpenAI models
Create a List of Todo items in SwiftUI | Todo List #1
How to make a DOOM CLONE in Unity || E1M1 First Person Player
5.0 / 5 (0 votes)