The second law of thermodynamics vs. a bitchin’ Camaro
Customization applications can be easy or they can be complicated. For the most part, you either have rules guiding which parts can be matched with others, or you don’t. While limitless customization can lead to more unexpected and entertaining results, you shouldn’t assume that’s the best and only way to do it. If your app needs to have any measure of realism at all, then certain parts simply shouldn’t match others.
Don’t shy away from the challenge of a rule-bound app! Not just because wackiness can be a crutch (especially when you can justify doing less work and writing less intricate code), but because you’re a competent professional who doesn’t hide fear of the unknown behind easy-win interactions. Rules won’t make your customizer boring. Only you have the power to make something boring. Not every part-swapping experience needs to be Exquisite Corpse in order to be worthwhile.
This is a primer for the more complicated customizers, which include specific rulesets for combinations. Working alongside Commonwealth, Detroit, we recently released our Camaro Six App for Android and IOS, and we learned a great deal along the way.
The Camaro Six Customizer app is available for iPhone and Android.
Building an app like this can be a beastly task. We chose to build it with Unity and C#, but code snippets will be kept to a minimum, as each app will have different needs. We just want to point you in the right direction and tell you a little about how we did it. Hopefully this high-level architecture primer will be both helpful and insightful. We would have loved to have had one.
So we’ve established you’re not a coward and you’re working with rules. Where do you start thinking about your app’s overall framework? I’ll tell you where: with planning to write yourself some rock-solid validation logic, preferably wrapped in a state-machine.
It’s sooooo tempting to just start firehosing if-statements everywhere to handle your basic rules, especially when prototyping.
Boom. Logic. Why should we do more than this? Soon enough, you (or someone who gets to tell you what to do) will want to add more options. You’ll have to account for errors, rejections and default options for chain reactions (i.e., you switch an important component that invalidates other choices recursively). Your simple if-statement will start to look like:
Not great. Not. Great. Inefficient and ugly, this simply won’t fly in a production environment. Much like a charming vampire, you have invited entropy into your home. Not only will it be an error-prone mess that only you can navigate, but it’s going to make changes and updates extremely difficult. If you’re thinking these rules won’t ever change or be updated, you’re adorable. Baby’s first project over here. We abide by an unspoken ethos to not over-engineer anything we create. With this in mind, we set out to build something lean, functional, and compartmentalized. What we learned was that our natural inclination for simplicity was not serving our need for a robust system.
Thankfully, we had the chance to work with a talented engineer named Brian Kehrer. These learnings are derived from his architecture and we think they should be written down.
Our prototyping revealed early on that, given the complexity and depth of customization, we couldn’t just replace things on the fly and expect to have a clear idea of what our creation actually was. The scene had to render something, the UI had to highlight something, and here comes entropy knocking on our door.
A single, fixed point of reference
This seems obvious, but sometimes we don’t realize when we’re fragmenting our logic. Rolling with the UI scenario above, if you needed to update the interface to reflect a change, you might do something like:
Two or more places need to be updated to reflect what the current creation is? Dangerous. We want to point them to the same thing. A point of reference implies that things refer to it, and that’s exactly what our code should do. The (one) UI controller should refer to the creation-model at appropriate times, rather than checking for selective input or being set externally.
Drill into your head the notion that this creation-model (as data) will be the only point of reference for anything and everything related to what the creation is or should be. This will give you much needed consistency and exponentially fewer unknowns when debugging. Here’s what composed our creation model.
Great. With a concrete reference in place, we can think about swapping parts. This is a huge and central part of your app’s logic. Those words (huge, central) in the context of programming can set you on a dangerous path to a god-object. Since we’re going to need specialized logic with each type of part swapped (more on this later), we should avoid building such an object.
Dedicated functions to swap parts
Each respective function will do its own error checking, making your code neatly compartmentalized, navigable and much more efficient. Much like surgery, if your work appears well, it likely functions well.
Note how you can still write your logic with names, despite the fact that actual part variables are numbers. Enumerator lists are ideal, since you’ll avoid string comparison but still be writing readable code. These lists should be added to as needed, but take a blood-oath to not remove or reassign anything. Your app is complex and numbers are used all over the place. Don’t open the door for the vampire. Take notes from functional programming languages and keep static references forever.
Earlier, I mentioned we’d need part-specific logic. Here is where we make the app ready to abide by rulesets. Is a part swap valid? There’s only one way to know.
If you select something that, given your current creation, is invalid (or experience a chain reaction described earlier), a poor user experience would be a silent denial of their input. Touching a button and having nothing happen reads like a broken app. Interface is like dialogue: the user will ask a question, and the interface is supposed to answer it. You can answer elegantly or you can be a curt jerk and simply say “no.” Ignoring the user is trash. Don’t be trash. You’re better than trash.
To really make the user feel like the interface isn’t trash, you should include why the option they selected can’t be applied. You’re welcome to do this visually, through motion or otherwise. What matters is that you do it and it doesn’t look like trash.
Now that’s all well and good, but how do we code this “answer to the user’s question” cleanly? We need the validation to include whether or not this part can applied at all, plus the rationale if it can’t. We’re going to do something else here that will hopefully blow your mind: using [System.Flags]
Bit fields are generally used for lists of elements that might occur in combination, whereas enumeration constants are generally used for lists of mutually exclusive elements. Therefore, bit fields are designed to be combined with a bitwise OR operation to generate unnamed values, whereas enumerated constants are not. Languages vary in their use of bit fields compared to enumeration constants.
In short, we assign information about invalid choices to integers increasing by a power of two, which then allows us to break a number down into detailed information. This is awesome because at times there isn’t just one error you need to address. They can be added and result in a number comprised of the sum of several other numbers, telling us what problems we’re having (all in a single number!) …
So looking back on the previous example, the check against the result of IsValid() would come back false if it wasn’t a valid swap (as defined by IsValid() depending on the input data type)! Sorcery, right? All of the detailed information we need is wrapped up into what is essentially a labeled number. This means that these checks are lightweight and save us much needed processing power from a sinkhole of comparisons from each part to each of its restricted parts. Most importantly, we can reject the user gracefully and informatively, like a patient parent instead of an unprepared dog owner.
We’re on a roll. Failure is impossible, right?
As mentioned earlier, you’re going to have two basic types of corrective action when invalid swaps are attempted. Either the user selected something unavailable due their current options, or they changed something that would invalidate multiple choices (such as turning a coupe into a convertible after racing stripes were applied across the roof — pfft). We can account for both by considering how we would correct parts if an overarching part was changed, then applying that same concept to rejections (essentially considering them single-frame corrections).
Assuming we have flagged our erroneous choice as described above, let’s parse it and see how we can handle both cases.
So we take the user input, run it through our validator, tag it appropriately, then attempt to apply the change pending the tag from the validator. Should it be invalid, we write in our specific fallbacks. Elsewhere, in the UI controller, we can call the same IsValid() function in order to know (and communicate) why a selection wasn’t enacted.
These things will make your validator rock solid. The more time you spend making your validator clear and functional, the less time you’ll spend wrestling with tying in the UI, giving you time to focus on design.
The Big Takeaway
The big takeaway here is that a customizer, more so than other types of projects, deals with a whole lot of checking, combining, modifying and correcting. Once you consider yourself a “decent” programmer, you’re probably familiar with the tingling feeling you get when you’re hacking something together. Some part of you deep down knows that this will ruin you later; that technical debt is real. Do it right the first time. Build a bulletproof part swapper. Don’t ever invite anyone into your home.