Help to make my game SOLID.

Hi, I want to refactor my game using SOLID principles. I’m trying to learn it, but lots of question comes to my mind.
Right now I’m wondering how one system can read values from other in a decoupled way. How to achieve that in the “best” way.

My game is a Truck Simulator. The truck have a Vehicle class (monovehaviour), and that Vehicle class have other subsystems (plain classes): Engine, Gearbox, Differential, Cockpit, Brake, Battery, OnboardComputer, Clutch, Chassis.

VehicleSpecs are just a scriptableobject that define vehicles data, like Brand, Model, Name, Price…
And every other subsystems also have their fixed specs in their own scriptableobjects: EngineSpecs, GearboxSpecs, and so on…

using UnityEngine;
public class Vehicle : MonoBehaviour
{
    [SerializeField] private VehicleSpecs _specs;

    private Engine _engine;
    private Battery _gearbox;
    private Differential _differential;
    ...
}

Questions:
1) How to make subsystems ask things between them? My Chassis class need to know things about the Engine class.

The approach Im thinking is this:

using UnityEngine;
public class Vehicle : MonoBehaviour
{
    ...
    private void Update()
    {
        _engine.Run();
        _chassis.Run(_engine);
    }

}

Is this the Facade pattern? Shoul I send to Chassis all the engine object? or just send the variables that need? _chassis.Run(_engine) vs _chasis.Run(_engine.motorTorque, _engine.brakeTorque).

2) How to make subsystems ask things with other classes outside Vehicle? My Engine class need to know things about the Weather class. Wheater class is a world class that process and define things like the ambient temperature. My engine need to know ambient temperature for simulate heating parts.

The approach Im thinking is this:

Creating a ScriptableObject for store weather data of the world. Weather system have a reference to that scriptableobject and write to it. Engine have a reference to that scriptableobject and read from it.

But in this way, Engine class will have a variable called WeatherData weather. And I don’t pretty sure if engine should have a weather variable inside on it.

The thing Im sure:
I want be able to test the truck in empty scene without the need of a WeatherController on scene.

I’d say just don’t overthink this. SOLID are guidelines, not a religion to follow (which TBH is what it is becoming lately…).

If you’re creating different MonoBehaviours and attaching them to a game object, you are already following the “S” (Single Responsibility). When you create [SerializedFields] and assign their values in the Editor via drag and drop, you are following the “D” principle (Dependecy Injection).

For the “Weather” system, I will just create a Singleton “WeatherSystem” or “WeatherManager” and that’s it. No need to overcomplicate things.

Just make your game work, that’s the only thing you should think about. Game dev is a special kind of programming, mainly because the code is highly coupled, rarely reusable and have specific constraints not found elsewhere (game loop at 60+ fps, assets, shaders…etc), thus applying Enterprise Architecture™®© just for the sake of it won’t take you far.

I’m not saying you should write bad code, far from this. But just following a standard, sane coding style and rules will take you very far, very quickly, without all the doubt, refactoring, googling and wasted time that could have been put into the game itself.

When in doubt, just take any successful Unity game, buy/download it, decompile it, and see how it was made, you’ll just be shocked on how a lot of parts are literally school cases of not what to do, but still, it worked, the game got released, is stable and made $$$!

3 Likes

Refactoring WILL introduce bugs ranging from trivial to absolutely bafflingly subtle, such as timing dependencies.

How good is your unit test coverage? Any part of your current setup NOT covered by unit tests should be absolutely off-limits to a massive refactor like this.

If any part of your simulation loop passes through Unity, you will be hog-tied trying to change it. The only possible way you could do a true complete refactor would be if your simulator already runs completely external to Unity (eg, you could compile it as a command line C# program and run it from the shell), and Unity just “tacks on” some Components to observe what’s going on and show it on screen.

If you have Unity doing any more, you’re immediately very tightly-constrained in how you can refactor.

That’s trivially easy: just make everything lazy-loaded. Call it whatever pattern you want, resource locator pattern, singleton, whatever. If you want, have mocks that load lazily for testing, such as clear weather always.

I’ll take it a step further. Enterprise Architecture stuff is a complete disaster because it cuts across the internal inherent design of the Unity API itself. Attempts to do this sorta refactor always leaves warty lumpy code that has to go to extraordinary measures to get things done.

Just use everything you learned making this game and work on your next game. Odds are you’ll do it better the second time around.

Otherwise, branch it in source control and get ready for a lot of hard frustrating work. I’m gonna be making more games in the meantime. :slight_smile:

4 Likes

Hey, thanks for the advices. My current game is already on stores, and is doing very well there. Im just trying to do a sequel. Taking things for the current version, and moving them to the next sequel. I have the time for that.

I will not refactor my current game. I will take all of the current game and try to do things better for the sequel.

One of the problems that I had is loosing to much time to test something in the truck, because I need to run the game entirely from the main menu just to drive the truck and that is the things I dont want anymore. The truck is pretty complex and right now is coupled to other systems that I think shouldn’t.

Thats the problem, I have several systems as Singletons. And when I want to test a truck, I dont want to have there all the other systems as requierements.

The game is working. The game is live on stores. The game is making good $. The players love the game. Thats not the problem. The problem is the time I spent just to test/tweak something in the truck. I want to load a truck and have it running without dependencies. In that way I can test/tweak/add features to the truck.

Don’t know about what lazy-load is. I will googling into it. Changing dependencies for “fake” scriptableobjects that simulate the data on that dependencies maybe should work?

Solve that by either a fine-grained state-saving mechanism that can instantly restore your entire game engine to that state, or else write some stub code to instant-mock the conditions you are trying to test.

I do the latter a LOT since it is far easier.

https://en.wikipedia.org/wiki/Lazy_loading

Loading a scene or an entire prefab is eager loading: every part of it loads and starts.

Accessing a weather service that didn’t exist and then suddenly brings itself into being is lazy loading.

Lifecycle is implied in many parts of the Unity engine, and you do NOT have direct control over all parts of lifecycle.

After all, your code is not the application. Unity is the application. Your code is just a minor guest at the party:

https://discussions.unity.com/t/882111/2

1 Like

As @Kurt-Dekker suggested, a good saving/loading system coupled with a console (https://assetstore.unity.com/packages/tools/utilities/quantum-console-211046) will give you excellent results. If you want to go down the path of SOLID, then I’d say Dependency Injection is the most important principle to uphold. Check some IoC frameworks for Unity (or write your own, like I did. It’s not that hard) then you can have something like this:

public class TiresController : DependencyBehaviour
{
    private readonly IWeatherSystem _weatherSystem;

    // Constructors are not used by Unity, but by IoC to inject registered services (and can also be used in Unit Tests)
    public TiresController(IWeatherSystem weatherSystem)
    {
        _weatherSystem = weatherSystem;
    }

    public void Method()
    {
      var currentWeather =  _weatherSystem.GetCurrentWeather();
 
        // etc...
    }
}

public interface IWeatherSystem
{
    Weather GetCurrentWeather();
}


public class WeatherSystem : IWeatherSystem
{
    public Weather GetCurrentWeather()
    {
        // Get real weather...
    }
}

public class TestWeatherSystem : IWeatherSystem
{
    public Weather GetCurrentWeather()
    {
        // Get test weather... maybe rainy?
    }
}


// Register the weather system you'd like to use
IoCContainer.Register<IWeatherService, WeatherService>();

// or
// IoCContainer.Register<IWeatherService, TestWeatherService>();

With this system you can also write Unit Tests that mocks IWeatherService:

void Rainy_Weather_Should_Make_Braking_Longer()
{
    var rainyWeather = new Weather { ... };
    var weatherSystem = Fake<IWeatherSystem>().With(x => x.GetCurrentWeather).Returns(rainyWeather);
    var tiresController = new TiresController(weatherSystem);

    Assert.True(tiresController.BrakePower < 5f);
}
2 Likes

As for
"How to make subsystems ask things between them?"

This could be resolved in a lot of ways, maybe the most straightforward and used a lot in gaming, is a “Blackboard”. A Blackboard is just a class that holds information shared between different systems, for eg:

public class VehicleBlackboard
{
    public Color Color { get; set; }
    public string Model { get; set; }
    public string Make { get; set; }
    public float BrakePower { get; set; }
}

public class Vehicle : MonoBehaviour
{
    private VehicleBlackboard _blackboard;

    private void Awake()
    {
        _blackboard = new VehicleBlackboard { ... };
        Engine.SetBlackboard(_blackboard);
        Tires.SetBlackboard(_blackboard);
        // Or instead you could just make VehicleBlackboard a MonoBehaviour and add it to the GameObject 
    }
}

And of course, this would be very helpful in Unit Tests as you can pass test Blackoards to the systems you want to test.

1 Like

Also really happy for you for your game success! carry on :slight_smile:

I’ve seen that before already with design patterns. Literally every generation gets their own obelisk of tenets to rub their noses against. But then when I see who else calls themselves a programmer I can’t help but notice that it’s the large part of the industry that fosters the “you can be a programmer too” mentality to import as many people as possible (and drive their cost down), then irons out the inevitable effect of “too many people” by tying them down to a set of religious beliefs and tight mid-level management. That’s the shortest path to getting the current pyramid state where managers only delegate tasks, but are being paid as much (or more than) the developers, but also tend to stick around for longer, until the bozo horizon starts rolling in.

Btw I still suck with dependency injection. I couldn’t find a good enough reason to think about it, other than through the architecture itself, which is the actual, original place where dependency should be taken into account. I’m thinking all this years that it has to be something only desperate professionals need after decades of “too many people”, to critically untangle the dependency mess in some commercial software out there.

The way I handle this in design time, and I regularly make expertly complicated systems (I never said anything about smart, don’t think of me as smug), is to visually document each part of it, usually once things settle down. I am good with Illustrator so it’s like pen and paper to me. This not only lets me visualize complexity and regulate traffic from the top-down, but also lets me remind myself of some critical information I might’ve forgotten about. And I really need reminding a lot. I sometimes think I suffer from “too many people” myself.

1 Like

The thing is, I’m a self taught programmer (started in '93 with Basic/Fortran/Pascal), and up until recently (6-7 years ago) I didn’t know nor care about all those “design principles”, including SOLID. I just wrote code that works. With years of experience, my code got better and better (clean, simple, readable, easy to refactor if needed…etc) still without caring about those principles. Then I wanted to find out what is all the fuss about SOLID and all the other principles, and to my surprise, I was doing all those things, instinctively.

Separation of Concerns? Well I was creating a class for each functionality of my program.
O/C, Liskov, Interface Segregation? I was using interfaces instead of abstract classes most of the time, thus not prone to those problems.
DI? I was passing around objects I needed as method parameters, or in constructors.

Trying to standardize and put hard rules on the way humans write code will never succeed, because there will always be different ways to do it, like any creative job.

2 Likes

Depends a lot on the exact problem. There is no one-size-fits-all.

You might be able to do a lot with interfaces. You shove in interface as a system-friendly type, and then worry about its implementor someplace else.

Then, there is the mediator approach, very similar to Nad_B’s blackboard example, where it’s a proxy pattern of sorts. You have an actual object whose job is to work with two disparate ends which don’t know about each other. I tend to use mediators in some of my solutions when I don’t particularly care about the outlook or the messiness of the actual business, especially with 3rd party stuff. If it works it works, I make sure that it’s reliable (if it’s not I kind of can already tell who is to blame), then I forget about it altogether, and my systems rely on the mediator’s API from that point on.

Third way would be to muster a quality event system. This is a must have for any system that has many moving parts but isn’t necessarily living on the hot path, i.e. frame by frame. It slowly tracks the state of the system, for example, either through player input, or through some artificial latency.

Now an event system doesn’t necessarily have to be the event system, you can still do a lot with callbacks. And that’s the preferred way of mine to shove the “blackboard” over there without here knowing about there and vice versa.

You can basically hand out the “entrails” and be like do what you want, here’s the template. I do that all the time for critical algorithms and things that need some service but shouldn’t necessarily snake around to look for it.

Then there is a hybrid solution: you plop down a service “kiosk” which is usually a singleton or even a static class. That’s your mediator. You use this kiosk so that even a stranger in this forest can “file a report”. This is really like a mailing system. The kiosk then ‘mediates’ this message across, but makes sure it has the ‘mailback’ info of the customer.

I use that for very very complex content production services to cross-communicate with each other, where almost every single piece is decoupled.

As I said there are numerous ways to organize your flow. I don’t believe in programming tenets of any sort, but sure, they can be eye-opening for grander designs out there. Just be smart, don’t shoot yourself in the foot, that sort of thing.

2 Likes

Same here. It’s really ridiculous. I had a situation once when the interviewer opened up with “can you name design patterns” and I was like “ok, should I order them alphabetically or by preference”. But also “what is your favorite part of C#” “umm, (I know it’s LINQ and lambdas, I just won’t say it, ohmygod he has 2 HR chicks sitting on both sides, this is literally crazy, I’m inside Jodorowsky’s The Holy Mountain), umm, actually I worked in a subset of .net framework, so my options there were very limited”.

I usually head straight for the door, can’t work with such people in a million years.

2 Likes

Well now it’s even more ridiculous with all those LeetCode and algorithms tests, as people are literally memorizing the answers! Hope I can continue working for my own and not needing a job in a company any soon…

2 Likes

Ooh I have a couple of adventures on that front. TopTal was something I tried recently. That’s was… Uhh, an experience.

Let’s just say it’s utterly undignified and completely against anything actually desirable in a programmer. I passed through two tests already with flying colors, and then the final guy was wasting my time with small talk only to run an unannounced live test where I had around 15 minutes for each solution, all of them being tricky just to think about, let alone implement in C# that quickly. And even though I verbally explained the entire solution for the last problem and managed to implement a third of it, that wasn’t good enough, and you also need to make the output identical to theirs. My wife was literally putting ice on my forehead for hours to calm me down, and this is likely the last time I tried this.

I think snowboarding naked on a ski trail made out of nails would make less adrenaline in my body.

@OP sorry about the derail. will stop with my coding adventures

2 Likes

:smile:

That does smell to me a bit like a potential violation of the interface segregation principle. If you used dependency injection, you could inject in an object that implements just a small subset of WeatherData’s properties like ITemperatureProvider. Using an interface could also help in restricting the Engine class to only have read access to these properties.

Unfortunately Unity doesn’t make it easy to inject objects into interface type fields, so you’d probably need something like a DI framework or a custom serialization system to help make this possible. Or you could just use GetComponent, but it’d be a lot less flexible.

While injecting only the members would result in a somewhat more decoupled code, I’d still usually prefer injecting the whole object via an abstract parameter.

I think it’s almost always more human readable to have just one parameter rather than several. Using an abstraction like IChassis can also help give more context and make debugging easier than if just more primitive types were passed around everywhere. This approach also helps avoid readability issues and bugs from code like DoSomething("Jack", "1234", false, false, true, false).

Also it might be easier to mock a bunch of higher level services in your game and reuse these for testing various things in the game, rather than having to write custom test code for each object you want to test. Especially if you use a service locator.

You can use dependency injection or a service locator to return different services based on the current context. So you could have a test scene for example in which all services are replaced by mock implementations automatically, making it possible to drop in your truck to the scene and just have it work automatically.

You could then even do stuff like expose the states of these mock services via slider and buttons in the HUD (or in the Inspector, if testing just in the Editor is enough) that allow you easily change things like the current weather on-the-fly.

You might want to check out this talk from Niantic Labs. They used a DI-based system that made it easy to do stuff like this. With a DI framework stuff like this becomes pretty easy to implement.