Get Struct – Designing for Designers

TLDR: This got long fast. Here are quick jumps to the things I’m talking about:


A recent VR project I worked on (a gallery shooter named Too Many Bunnies) required me to develop a simple interface to allow designers to easily customize and iterate upon the game’s very simple AI.

Calling the enemy behaviour ‘AI’ in this game is a bit of a stretch. It’s a shooting gallery: enemies enter the scene, they pathfind along a rail, and then they exit the scene. Its simplicity left me free to focus on an aspects of programming I’d not really needed to think much about before: the interface of my systems.

I find designing tools and plugins to be a lot of fun. It engages the parts of my brain that I exercised frequently back when I was an editor. You have to find compromises between the intent of the author—their vision, to be shamelessly poetic—and the embodied experience of a reader engaging with that text. You’re thinking about the capabilities of eyes, brains, and memory.

Sitting alone bashing out a script isn’t unlike being an author. You (hopefully) have a clear mental map of what you’re trying to achieve and writing things in such a way that other programmers (including your future self) can understand what’s going on. You need to be frequently putting on your editor hat, in other words.

But making something that you want designers to work with on the front end (e.g. Unity’s inspector) presents different problems. In addition to creating performant systems that meet a given specification, you’re making decisions about what functionality to expose for other people. Which parts of your intricate mental map will you surface as serialized variables? How much information is too much? How will you name, sequence, and group variables? What can you do to support rapid iteration and mitigate costs associated with changes in scope?

Like in all group projects, regular communication plays a big role. It’s a UX design process, and game designers are your users. There’s a lot of fun to be had here for a detail-oriented programmer. Here are a few things I found gainful while working on this project:

Talk To Designers About Your Interface While You’re Building It

This one is a no-brainer, but if you’re a reticent and introverted sort of programmer it’s worth keeping in mind. You can’t work in a vacuum. Talk to the designers, for heaven’s sake. Talk about the properties you’re choosing to expose. Ask what you should name them. Ask about anything else that comes to mind. The worst that can happen is you come across as finicky, and there are far worse things in the world than being a finicky programmer.

Little changes can make a difference. In this project, I had an integer variable named ‘InitialEnemies’. When a designer and I were reviewing the spawning system at a relatively early stage, we had an exchange that went something like this:

Designer: ‘Initial Enemies.’ What does that do?
Me: It sets the number of enemies in the first wave.
Designer: Can you call it ‘Enemies in First Wave’?
Me: Yes.

Easy as. There were a handful of instances like this—where what I thought made sense didn’t make sense, and three seconds of conversation solved the problem.

Make Your Interface Eminently Tweakable

If you’re making an enemy spawning system (as I was), then it goes without saying that it needs to be customizable. More than that, it should invite tweaking; experimenting with your system should be fun as well as frictionless.

Making a tool fun is a powerful thing. More than just working to spec, you’re creating something that could reshape the game’s core design. My enemy spawner could spit out a slow trickle of spongy bruisers, a rapid parade of squishy pawns, or anything in between. These options feel completely distinct from one another during gameplay and, depending which ones get chosen, might lead to further shifts in design (e.g. more enemy types, enemy colour coding).

Use Structs Rather Than Prefabs (Where Appropriate)

This will obviously depend entirely on the scope and design of a project. Prefabs, Prefab Variants, and all things related are big, useful features of Unity. Use them freely and use them well.

It’s just that there are certain times, particularly in smaller projects, where you might default to using Prefabs in situations where they’re really not necessary. They might even end up being hindrances when time is tight and you’re wanting to make rapid changes toward the end of the project.

Take the enemy types in this project, for example. We didn’t know early on how many enemy types we wanted to have, or even whether we’d want to have multiple enemy types. We began with a single ‘Enemy’ Prefab: a mesh, some sounds, rigidbody, collider, nav mesh agent. Simple functionality for simple entities. The spawner grabs it, chucks it into the scene, gives it a navigation path, and away it goes.

At some point, early in the process, I was asked to support multiple enemy types. The assumption was that we’d make multiple Prefabs for each of these, but it became clear to me (through talking to the designers—see above!) that there wasn’t going to be much variation between these enemy types: different speeds, more hit points, different colours. The transforms, meshes and colliders would be identical.

And I was reminded of a Sebastian Lague tutorial I’d gone through some time before this project. In setting up his coloured Perlin noise map, he uses a struct to allow the user to set up the different coloured regions of the map. When you create an array of these structs, Unity surfaces their properties in a nifty manner; you can create and reorder multiple types to your heart’s content.

It’s easy enough to do:

using UnityEngine;

 public class StructTest : MonoBehaviour
 {
     public NiftyStruct[] niftyStructs;
 }

 [System.Serializable]
 public struct NiftyStruct
 {
     public string niftyName;
     public float niftyFloat;
     public Color niftyColour;
     public Texture niftyTexture;
     public AudioClip niftyAudioClip;
     public Gradient niftyGradient;
 }

… giving you this in Unity’s inspector, once you’ve added a couple of elements to the array:

… which you can then fill as you please:

This pleases me. And it benefited the project in a few ways (both expected and unexpected):

  • We could quickly add and remove enemy types.
  • We were able to quickly implement emission materials very late in the project when (for reasons we couldn’t figure out) URP lighting suddenly resulted in all the enemies being dark and washed out.
  • We didn’t have to waste time replicating changes to components across different Prefabs (e.g. the NavMeshAgent speed property); using a struct in this way meant we could make changes in the same place, without having to step in and out of different Prefab views.
  • It prevented source control complications as a result of having multiple, frequently-updated Prefabs.
  • It allowed us to easily update important variables in real time in the Game view (to test out different agent speeds, for example).

Format and Spell-check Your User-Facing Variables

The latter is something you should do all the time, forever, for every single thing you ever write because you don’t want to look silly, particularly in a professional context.

Formatting is about making things simple and readable for designers and just generally being an organized person who cares about an end-user’s experience. As an added bonus, grouping variables gives you another chance to consider the clarity of their naming. You might also pick up on serialized fields that don’t need to be exposed, or variables that are no longer used and can be deleted. It’s just getting your ducks in a row, and it’s not terribly difficult to do in Unity:

using UnityEngine;

 public class FormattingTest : MonoBehaviour
 {
     [Space(5)]
     [Header("Spawner Settings")]
     [Space(15)]
     [SerializeField] private float spawnCooldown;
     [SerializeField] private int numberOfEnemies;

     [Space(5)]
     [Header("Agent Settings")]
     [Space(15)]
     [SerializeField] private string agentName;
     [SerializeField] private float agentSpeed;
 }

Unfortunately, this formatting can be rather unintuitive. I don’t fully have the hang of it (how and why the Space Attribute works the way it does, for example), but the above code should give you this in the inspector:

Lovely.

Thanks probably all I got. Thanks for sticking around!