Before you read on, I'd like to forewarn that this project involves flashing, bright lights, repeating patterns, and loud noises, which could be an issue for people who have photo and/or audio sensitivity. Please exert discretion when going forward!
A tool I developed in Unity to study procedural generation. Through a simple set of rules, but which are flexible enough to give rise to multiple possible outcomes, I came up with a pseudo particle system that utilizes scriptable objects to spawn particles that have unique individual behaviors. I called these particles "atoms".
Another major aspect of code I had the opportunity to explore here was Unity's custom UI code. It taught me how to make custom tools in Unity more legible, by doing simple things such as showing and hiding fields depending on what options the user inputs.
Engine: Unity 2021 LTS.
Production Period: Created for a class during my time in RMIT, made in around a month.
Notable Features:
A single general purpose particle system capable of rapidly iterating through different particle behaviors without needing to create a custom particle each time.
User friendly UI that empowers artists to be creative without needing to read tons of documentation about how the tool works.
In short, each particle's (or atom's) properties are specified by creating a scriptable object for the corresponding particle you want to spawn. Each of said scriptable objects will give the user an event/action that the particle is able to perform (ex: spawn another particle, destroy itself, parent to another particle, etc.) This is followed by a trigger, which dictates when said action is to be performed (ex: when colliding with another particle, when it first spawns, when it becomes parented, etc.)
With this simple set up of input/output for each individual particle, you can create a myriad of scenarios pretty rapidly. In the context of a videogame, this would be quite a powerful tool to not only quickly prototype a desired particle system, but to be able to create very different results using a single system instead of multiple.
This tool works by combining 3 scripts (actually 4, but more on that later). I will break each of them down in detail (lots of text incoming!):
The majority of the user input comes from the "Atom" script. It inherits from the ScriptableObject class, because I wanted to make it so the user can create and store each separate atom's settings as a file inside of Unity itself, instead of keeping all the settings in the scene itself. I did it this way with scalability in mind, in case this tool would ever be used in the context of a larger project, it would be easier to store the atom's settings and reutilize it in other contexts by simply having a file it can reference, instead of having to copy an object from a scene to another.
To use it, the user can right click on the project files in an appropriate folder, and create an Atom ScriptableObject to be stored in the files, as seen below:
The user can then click on the newly created atom and see its settings in the inspector, and at first will look fairly barren, not many fields to fill out with data:
This is where the main trick of this script kicks in, which is that it is not actually just 1 script, but actually 2. The main script is as I said, is the ScriptableObject inherited "Atom", where all the main variables of the Atom's behavior are stored for later. The second script is called "AtomEditor", which instead inherits from the Editor class. It is this script which I am about to focus on at first, then I will return to the Atom script later. This second script does not store any variables, and its sole function is to modify the way the UI of the Atom's scriptable object presents itself to the user when being modified. Below is an example of this happening:
Here you will notice that as soon as I added an item to the Atomic Events array (more details about this array on the next section), a new section of inputs below is added with a corresponding number. The AtomEditor script is responsible for showing the fields that matter to the user's chosen input, while hiding everything else that does not pertain to the user's current chosen settings. In the above case, because the user has chosen for the trigger to be a Collision, and the output to be ChangeScale, the UI only shows the user the variables that allow them to define how said collision takes place (Collision Tag), and to what scale the atom should change into once the collision does happen (Change Scale).
In a nutshell, the Atom Editor script simply shows fields appropriate to what the user needs, while hiding that which they do not need, improving legibility and ease of use.
As mentioned before, this is where the variables that make up the behavior of an Atom get stored so they can be given the appropriate instructions to follow upon being spawned. Below I shall go through the main variables that this script stores and what each does:
Atomic Events - An array of structs where each item has two enums, the first representing a trigger/event that must take place to begin some form of resulting reaction. The second field determines what the output/reaction that comes as a result of the trigger is.
I will first show what each of the "Trigger Events" enum do, then will each of the "Output Events":
Start - When Play Mode is first entered, trigger this once.
Always Update - While in Play Mode, trigger this once per frame.
Becomes Child - When this atom becomes a bonded to another atom (becomes a child object of it), trigger this once.
Becomes Parent - When another atom bonds to this atom (becomes a child object of this object), trigger this once.
Has Children Amount - When this atom has a specific number of other atoms bonded (children objects) to it, trigger this once. The number can be specified in the integer field "Required Number of Children".
Collision - When this atom collides with something, trigger this once. You can choose with what it will collide with based on object tags, which you can specify in "Collision Tag" (if left empty, it will trigger when colliding with anything). Furthermore, if you want to trigger this event only when colliding with another particular atom, you can type "Atom" in the tag, which will show you a hidden field "Specific Atom", which receives an integer. This integer corresponds to another Atom scriptable object, and you can choose what each scriptable object's "Bond Num" is on the field indicated above.
I will go into "Bonding Chart" later. First, let's go through the Output Triggers enum:
Change Scale - Change the scale of this atom to the specified Vector 3.
Change Speed - By default, Atoms have a default movement speed in a random direction. This change said speed of movement based on this float
Change Color - You can set the colour of this atom to a fixed colour, as well as its emissive intensity. You can also instead enable the "Set Color Based On Position", which will instead allow you to set the colour of the atom based on its Transform position, where X=R, Y=B and Z=G. The positions are calculated based on the Vector 3 dimensions specified in "Color Based On Position". You get to see a gizmo preview of what this looks like too for visual guidance.
Atomic Bond - In case the "Parent/Unparent" is ticked off, it makes this atom becomes a child of the other atom being collided with, but this will only work - as the warning suggests - this output will only work if the trigger is a Collision. This is where the "Bonding Chart" comes in. Each atom can only bond to specific other atoms, and this can be filled out in the "Bonding Chart" array of ints. Any ints representing the desired atom to be bonded with need to be written here. Any atom not noted here will NOT cause a bond to take place, even if the collision does happen.
In case the "Parent/Unparent" is ticked on however, the collision causes this atom to stop being a child of any other atom, and be by itself. When it detaches from a parent, you can also specify the force with which it detaches, launching itself in a random direction, based on the specified Vector 2.
Change Kinematic - Make this atom kinematic (for all intents and purposes in this case, it makes the atom freeze midair and stops registering physics interactions) or stop it being kinematic.
Hide Unhide - Make this atom active or inactive (not just the visbility, the entire object and its children become enabled or disabled).
Apply Force - This is not the same as Change Speed, because this instaed applies a force with a magnitude specified in the float "Apply Force" only once in a random direction, instead of constantly updating like Change Speed does.
Particles - Each atom has a particle system attached as a component. This makes it so that this particle system is played.
This is the script that actually spawns the Atoms and sets up the space where they are spawned into. It begins working only when you enter play mode, being tied to the Start function. Below is a breakdown of each main field:
Spawn Radius and Spawner Gizmo Color - The area in which the atoms will be spawned into. Said spawn area is the volume of a cube, and atoms can spawn at random in any part inside of it. There is gizmo which serves as a visual representation of what that will look like, as seen in the blue box, whose colour can be changed as well. The box also responds to changing the radius size accordingly in real time.
Atom Prefab - A simple reference to the prefab of a blank Atom, which serves as a basis for all atoms to be spawned and given their special behavior according to the scriptable objects.
Atom Spawner - An array of structs, where each array item has 2 fields. Each item of this array represents an Atom that the Game Controller will spawn. This can be defined with the first field, which is a reference to the scriptable object that corresponds to the Atom you want to spawn. The second is an integer that indicates how many of the specified atom you want to spawn in.
Arenas - A simple array that references objects in the scene. These objects are the collision boundries in which the atoms stay within. The objects themselves are simple meshes with a physics material attached, which ensures that the atoms will bounce back when colliding against the walls.
As mentioned in the first section, the Atom script stores the information for each atom once they are spawned in through the Game Controller. However, neither the Game Controller nor the Atom itself are responsible for actually executing the input received by the user in order to make the atoms move around and perform their actions. This is where the "Atom Behavior" script comes in.
This script's function is the most robust from all of the other scripts, yet is the one that requires the least interaction with the user. It has no inputs at all, and it does not need to be added by the user, this is already automatically added as a component in each atom when the Game Controller spawns them.
It's purpose is to intake the Atom scriptable objects which the user has specified in the Game Controller, and use them to guide the atom's behavior accordingly. Internally, this script follows a simple logic, it consists of a bunch of switch cases and many if/else statements, where it checks the "AtomicEvents" enum field of the Atom script one by one, then assigns the appropriate response accordingly. Despite being simple, it becomes complex in that it results in so many different permutations.