Architecturally Deep

I could show you code towards the navigation and Camera switching I have in place, but I want to go over a much more impactful issue to the success of the game; Architecture. Don't worry,  I will go over how I got camera control and character movement working in later posts.

Architecture

Source: https://pixabay.com/illustrations/blueprint-technical-drawing-4056027/

So, in programming, we quickly become our own worst enemy, by overly complicating things. So now as the production code is falling into place, I have to seriously consider the long term effects of my early choices. So to do this, I'll address several key issue, and talk about how to handle them.

I want to preface any architecture explanations I give to say I DO NOT BELIEVE there is a perfect architecture or pattern to solve everything. I believe that you should look into many different architectures and learn their strengths and weaknesses, so you can figure out what is needed.

Spaghetti

So we all know about spaghetti code, and we all know about OOP as well, but maybe we don't. I remember the first time I heard someone talk about my code with classes and interfaces barely being OOP, and thought he was nuts and annoying. But I learned pretty quick that he was right. These classes knew all about each other. Almost to the point of circular code. I couldn't change anything without potentially breaking stuff. Test automation was a pain, and took huge steps to write.

Code needs to be automatable for testing. It should not have two way references. It should be SOLID. Here is a good starting reference, check out the first 30 minutes of my talk on SOLID at Unite.


Since Unity doesn't play well with Dependency Injection, (well it does with Zenject, which many places stand behind, but I have some general disagreements with their approach) I have chosen to use a Locator Service pattern. The downside, is that my classes become aware of the service locator, so it can't be swapped without finding all the references to it and changing the code. So I added a service locator. (see it here: https://dotnetfiddle.net/COGZIp)

The value of a Locator is that instead of setting the reference for log to be 'var log = new Log();' or 'var log = GameObject.Find<Log>();", (the latter actually being a form of Locator) is that a simple locator is more performant than ".Find();" and more flexible than "new". We create one point where we call out in an initializer function to what classes are called out. Because they are interfaces, we can easily hide more of the workings that don't matter to us. 

They are also changeable. I.e. Lets say you have a class that handles two interfaces, ISave & ILoad. Perhaps that class saves everything to a file. Then you change it to work with a service, and store the user's save game online. If your code worked directly with File to save things, then each area you called on it, would need updating to work with a service. But if we have ISave, and ILoad, and ask for it from a service, then we don't actually know what concrete class it uses. Instead of changing existing code, we can create a new class, called OnlineSaveLoad, or OnlineSave and OnlineLoad, and the initialize the Locator to provide that concrete type for the ISave and ILoad. With this setup, none of the other classes need to be updated, just create a new class and alter the initializer.

KISS

Keep It Simple Stupid - Almost every one remembers this but its easy to forget. Every time we face a new problem we, at least I, forget to consider any previous support architecture that isn't an exact match. The key thing to consider, is if it is close to an existing architecture, think about what you can do to re-use it, and when we do look at it, we might go about it wrong.

Source: https://pixabay.com/illustrations/arrow-chip-board-computer-hardware-1784155/


I faced a similar problem, in one of my previous constructions of a Unity Locator Service. So the intention of a Service Locator is to provide references to where to find certain classes by abstraction (interfaces most commonly). But I had some classes, such as Player, or LevelInfo that many things would need access to, and it made most sense to use the service locator for it. 

But the Service Locator is not made for continuously updating what concrete type to return. So I figured I would add a little functionality. I made sure that if I set something up that already existed, it would replace it. But I soon realized another difficulty; many classes that asked the locator needed to know if the level details had been updated, and rather than require they check for something new, I put an event on the Locator to trigger for changes, or additions, to the locator, based on type. 

This worked great, but it also meant that my Locator Service no longer was a Locator Service. It may not stand out why that is a problem right away, but think about this. Patterns are commonly known pieces of architecture. So if someone joins your team, they will already know how to work with a Locator Service, and if they haven't before, there are plenty of resources online to help them not only understand how to use it, but why. And if there are difficulties, they can ask questions about a locator service on a forum, and lots of other people will just know how to use it, even if there is a few changes to their naming. 

But with that Locator Service, those options were lost. No one who had not looked at that piece of code in specific would be able to help on a forum post about why the event for [blah] type is not being triggered in the locator. What I really needed, was an Observable pattern. I still needed the Service Locator, but the Service Locator would provide access to the Observable. 

An Observable pattern is like Dictionary<object, object>. It takes a key and allows you to get or set data for a unique object. But it also will trigger an event when data changes, to notify anything using the data to update.  

Back to KISS, it might be arguable that one more flexible class means that it can handle more things, and is there for less to learn and simpler. But, then any new person on the project would have to learn it. Maybe not that tough, but what about the several hundred other things they will also need to learn as the project grows? Even if people need to learn the patterns, those are language agnostic and can be carried to other projects with ease; they can be discussed without having to walk through how the whole thing works; they can have questions posted to forums with very little details needed.

Wikipedia has a pretty good list to learn, at least the few lines of how they work on the link below, if not learning their strengths and weaknesses.  https://en.wikipedia.org/wiki/Software_design_pattern

Addition by Subtraction

I have not added the Observable Pattern to my code, even though I'm pretty sure I will need it, because I have not needed it yet. I remember dozens, if not hundreds of projects where I spent the first few days, weeks, even months, building architecture to support the expected requirements. (ah the good old days of Waterfall...) But all to often, that planned architecture falls apart at the seems. 

Source: https://pixabay.com/vectors/plus-minus-icons-symbols-red-24572/


As the features get developed, we find requirements don't match up with the planned architecture, and suddenly an absolute requirement of the end product means we have to change things. That will ALWAYS happen in projects. So, don't waste time on things that are not needed, because its almost impossible to understand all the needs until you get into the weeds.

So, you might ask, "So I'm not supposed to have any architecture in place at the beginning? Just dive right into the code?" - A question that aims to poke holes in the concept. But the answer is yes. Go with this rule of thumb. Consider what patterns might help you solve a problem. Then consider what patterns you already have in place, and if any can already solve it.  If you do need a new one, but it is only for one thing, DO NOT put it in place. Just write the code to directly circumvent it. Feel free to put a //TODO in the code to comment about the plan to use [blah] architecture though.

The point is, to wait until you have two or more things that will need it. I don't write a factory to construct something until I need more than one way to generate an instance. I don't write a Locator, until I have more than one class that will benefit. I don't write an observable until I have multiple points in the code that will need updated. 

This does something interesting to our perception of code progress. Once we have to rewrite the few portions of code that will need it, (which should only be two to three), we feel like we are not accomplishing anything, because that code already worked. And its true. The code worked. It also means we are writing code in the next sprint to improve the resiliency of our past code. It feels off.

But, it only feels off, because it feels like wasted time. Why didn't we just write the architecture in the first place? And the simple answer, is that we weren't positive. Once we have two or three classes that could use it, we are at a better point to deal with deciding which pattern to use.

In Closing

I would recommend you learn SOLID, if you haven't, get at least a high level understanding of all the patterns listed on the Wikipedia link above, and then only use what you need. Architecture happens plan it or not, and since it strongly dictates how well your code does over time, its worth while to take some time to understand how.

Screen Capture @ 3X Speed









Comments

  1. I've had another reason to go with Locator over Injection. A locator service is basically a dictionary look up, a single check to find a service of interest. On the other hand, a DI typically requires doing reflection against a class as it loads, and detecting any unset fields and calling them, or looking up methods with attributes, and calling them. That lookup is considerably more expensive. This is not a huge physics processing spike, or giant loading file, but still takes a little weight at the beginning. It can also be harmful if you have DI loaded classes at run time with a lot of members.

    With a locator, you can also choose to wait to get what you want until the first time you need it, or initialize it with the class loading, your choice. DI doesn't have that feature built in.

    ReplyDelete

Post a Comment

Popular posts from this blog

C# Parser and Code View

A Momentary Lapse of Passion

How to Setup a Multi-Scene Unity Project for Startup