Periodic Deliveries Postmortem - Time-Saving Tricks
December 9th, 2019 (edited November 3rd, 2022)I'm releasing a Unity 2018 game called Periodic Deliveries! Development of any game is fraught with little speedbump tasks that slowly soak up dev time and make it a little less fun, too. Here are a few tricks I did to knock out some of those bumps in my process that you can easily implement in your Unity projects, too.
Use the MenuItem attribute to create developer cheats
In a larger game with a larger team, you might implement a developer console you can activate with a key press and type commands into. That will scale well if you have a lot of commands, and it will work in standalone builds. But if your game is small, this is a quick and effective approach. Quick implementation is important because it will encourage you to add cheats more often, and those will save you time as development goes on.
I put them all in a static class called CheatCommands. Don't forget to add a validation function, which will disable the option when it doesn't make sense and improve your own experience.
public static class CheatCommands
{
[MenuItem("Cheats/Infinity Money", true)]
public static bool ValidateInfinityMoney()
{
// Disables the item in Edit Mode and in the Main Menu
return InGameManager.Instance.PlayerCompany != null;
}
[MenuItem("Cheats/Infinity Money")]
public static void InfinityMoney()
{
InGameManager.Instance.PlayerCompany.AddMoney(PeriodicInt.PositiveInfinity);
}
...
}
Make custom property drawers for structs that show up in the Inspector
Using Serializable structs for common data patterns can help you avoid writing similar code multiple times. But you'll have to expand a foldout every time you want to edit this data in the Inspector. If you can write a custom property drawer quickly, you can save yourself a lot of clicks over the entire development of your project.
Here is a base class for a common property drawer I used several times. You can inherit from it to create a custom property drawer for any struct that represents a "quantity" of a "thing" (like a stack of items in a loot container, or an ingredient requirement in a recipe).
using UnityEditor;
using UnityEngine;
// Author: Brian MacIntosh (The Periodic Group)
// MIT License
/// <summary>
/// Base class that can be extended to quickly create a property drawer for a structure containing
/// data like "quantity" of "thing".
/// </summary>
public abstract class ItemQuantityPropertyDrawer : PropertyDrawer
{
/// <summary>
/// Name of the quantity/stack size property.
/// </summary>
public abstract string QuantityPropName { get; }
/// <summary>
/// Name of the item/object property.
/// </summary>
public abstract string ItemPropName { get; }
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
Rect quantityPosition = position;
quantityPosition.width = 40f;
Rect resourcePosition = position;
resourcePosition.xMin += quantityPosition.width + 4f;
EditorGUI.PropertyField(quantityPosition, property.FindPropertyRelative(QuantityPropName), GUIContent.none);
EditorGUI.PropertyField(resourcePosition, property.FindPropertyRelative(ItemPropName), GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
Alternatively, get it from Github.
Here's an example use:
//RecipeComponent.cs
[Serializable]
public class RecipeComponent
{
[Tooltip("The resource.")]
public ResourceData Resource;
[Tooltip("The quantity of the resource.")]
public int Quantity = 1;
public RecipeComponent(ResourceData resource, int quantity)
{
Resource = resource;
Quantity = quantity;
}
public override string ToString()
{
return Quantity.ToString() + " " + Resource.name;
}
}
//RecipeComponentPropertyDrawer.cs
using UnityEditor;
[CustomPropertyDrawer(typeof(RecipeComponent))]
public class RecipeComponentPropertyDrawer : ItemQuantityPropertyDrawer
{
public override string ItemPropName
{
get { return "Resource"; }
}
public override string QuantityPropName
{
get { return "Quantity"; }
}
}
Separate your code files into 'Game' and 'SDK' folders
Put any code files that are specific to this game in the 'Game' folder. Put any code files that might be useful on your future games in the 'SDK' folder. The idea here is to create a folder that you can literally copy and paste into your next project (or you can use a git submodule if you jump back and forth between a lot of projects).
You need to follow one rule to make this work: code in the 'SDK' folder can never reference code in the 'Game' folder. If this happens, either (1) rework your SDK code so the Game code can insert its own behavior - for example, provide an event it can subscribe to, or virtual methods it can override - or (2) put that code in the Game folder.
My SDK folder contains things like:
- Static classes full of utility functions (MathUtility, ListUtility, etc).
- The abstract property drawer above (in an Editor subfolder).
- My generic input context system.
- Generic helper components, like PositionAtMouse, MirrorColor, GameObjectPool...
- etc.
If do this, you will write better, reusable code and save yourself time debugging this project and implementing these functions on the next.
Use a GlobalData ScriptableObject
ScriptableObjects are a great tool for organizing data. I like creating a scriptable object for each item, ability, and other such thing in my game to hold all the data associated with it. On Periodic Deliveries, I also created a monolithic 'GlobalData' object. This object holds all those global configuration options that usually end up throughout the project on various manager components and code constants. Putting them in one place meant I never had to spend time tracking down where so particular value was. The scriptable object also diffs much better in source control than a component in a scene, in case you need to look back through the history.
The GlobalData object can be held on a global singleton component somewhere, or maybe loaded with the new Addressable Asset system (I haven't played with it yet).
I hope some of these tips can make your development a little faster and more fun. If you're into space, simulation, or management games, don't forget to check out Periodic Deliveries on Steam!