Simplifying Wave Creation with Custom Attributes in Unity

Ben Mercier
6 min readJun 9, 2022

--

Objective: Easily create and edit the spawn sequence of objects in a wave based on a dynamic list of objects specifically available to that wave.

The Spawn Objects for the Waves

When approaching this scenario, the initial reaction may be to gravitate towards using enums, lists, or arrays. Obviously, enums are an excellent way to create a selectable dropdown list in Unity’s Inspector, and lists and arrays can be added to with ease.

Enums, and lists, and arrays, oh my!

Unfortunately, using an enum would define the list of objects in code instead of acting as a dynamic list of objects for users to add or remove from, and creating and maintaining a list or an array of objects for each wave could quickly become tedious and prone to errors. Each making it difficult to design, test, and modify how individual waves should perform.

The Setup

In the Hierarchy, a Wave Manager game object holds a singleton WaveManager script which maintains a reference to each wave used in the game via a list. The manager also controls the logic for how and when to process them. An enum is used for the two types of sequences each wave may select from, block and random, which either requests objects in groups (e.g., 5 of x, 3 of y, etc.) or randomly from the list of objects available to that particular wave, respectfully. This block wave sequence is where the editing challenge lies.

WaveManager with list of wave scriptable objects (WaveDataSO)

The waves themselves are scriptable objects (WaveDataSO) which use the enum declared in the WaveManager to dictate their sequence type and hold the information for those sequences to use, including the list of objects to choose from. These wave objects (WaveObjSO) are also scriptable objects in order to make it easier to add and remove them from the wave.

WaveDataSO showing in Unity Inspector with Wave Type enum

The Solution

To map the block sequence’s values to what objects are available within the wave, a custom property attribute and drawer can be created which invokes the EditorGUI.Popup() method. This solution is based off a tutorial by URocks! on YouTube which shows a list as a popup, and has been extended to work with this wave system.

As mentioned, the goal of the block wave sequence is to select an object from the wave’s list of available objects and then provide the information for how many and how often those objects should be spawned per block. A string (waveObj), int (waveCount), and float (waveFrequency) variable are used within a BlockWaveSequence class to store the selected object, amount to spawn, and spawn frequency, respectfully.

“tempList” is a static list of strings within the WaveDataSO to hold the names of the wave objects

The custom property attribute (ListPopupAttribute) is created by inheriting from PropertyAttribute and is applied to the waveObj variable to link the popup selection with the object list. The attribute has a constructor with parameters of type Type and string which reference the WaveDataSO and static list within it containing the popup items (tempList).

myType = WaveDataSO; propertyName = “tempList”;

On the WaveDataSO, the ISerializationCallbackReceiver interface is implemented in order to receive a callback before Unity serializes the object.

WaveDataSO inheriting from ScriptableObject and implementing the ISerializationCallbackReceiver interface

The interface method, OnBeforeSerialize(), sets a private list of strings (_popupList) equal to all the names of the wave objects currently associated with that wave using a GetAllWaveItems() return method and then passes that list into the static tempList. This list must be static in order to use it with the ListPopupAttribute’s constructor.

With the ListPopupAttribute created and successfully applied within the BlockWaveSequence, a custom property drawer (ListPopupDrawer) can then be created to edit and customize how the property is shown in the Inspector.

Note that property drawers are Editor-only code so their scripts should be placed within an Editor folder. If not, the #if UNITY_EDITOR… #endif directive must be used to encapsulate and conditionally compile the code.

The ListPopupDrawer inherits from PropertyDrawer in the UnityEditor namespace, and uses the CustomPropertyDrawer attribute in order to designate it as the drawer for the ListPopupAttribute. It also only overrides one method, OnGUI(), which can be divided into three parts which are themselves wrapped between EditorGUI.BeginProperty() and .EndProperty().

First, a ListPopupAttribute variable (lpAttribute) is created and set equal to the PropertyDrawer.attribute getter while a string list (stringList) is initialized to null. This stringList will be responsible for holding the values of the tempList passed into the ListPopupAttribute’s constructor.

Second, the tempList is checked to see if it’s null, and if not, the previous stringList is set equal to its values.

Third, the stringList is checked for a non null value and count greater than 0. If so, the index of the option shown in the field is stored in a selectedIndex variable which is then passed as the index for the stringList. The stringList[selectedIndex] can then be used by the property to set its string value.

EditorGUI.Popup()

However, if the stringList is null or the count is 0, then the string value of the property is set to the message, “No Objects Available” to prevent a string value from being entered in the Inspector.

“No Objects Available” vs. being able to manually enter a string value in error

With each piece working together, modifying the sequence of a wave system should be easy, efficient, and painless!

Easy, right!

Querying the Selection

The last item to address involves the WaveManager knowing which object/item was selected in each block sequence for each wave.

Since these wave objects will be spawning over time, a coroutine can be used which takes a WaveDataSO object as a parameter. A foreach() loop then iterates over each BlockWaveSequence object, that object is queried to match with and return a wave object, and then that wave object is spawned using its pool and item ID along with the count and frequency defined in the current block sequence.

The Result (with a few unmentioned features)

--

--

Ben Mercier

I’m an emergent software developer with a longtime love of games and a growing understanding of how to actually build them.