Search Unity

In need of Advices on some Design Principles

Discussion in 'Scripting' started by symorian, Jul 9, 2018.

  1. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    Hi,

    Currently I'm working on a project and I'm a newbie developer.
    Yet, I'm having hard times understanding some principles and mostly how they are applied in real-world examples.

    1) I've heard a lot about Singleton patterns and also using them here and there in my current code. But, that "here and there" part is the question. I don't know if I'm using them properly as how they should be or abusing their usage.

    Can someone please point me to some "useful, non-abstract" tutorials that explain in layman terms and gives examples of real world usage especially in Unity & game-making?

    2) This quesiton is related to the above. When you use "Instance" keyword while building a Singleton or in any other manner in Unity/game developing, do you refer to "footprint allocated in memory" or literally a visual/script instance that lives on the Unity scene or both depending on the structure?

    I'm asking this because, in some cases I get errors like where childCount is greater than zero however I can't seem to find the said instance. Or is this totally specific to <List> types where the list isn't cleared properly?

    3) I've recently started to use DontDestoryOnLoad method. Are there any cons related to this? I mean does the memory get bloated in time if I use too many persistent objects? I guess, yes? When creating persistent objects, do you have some common practices? i.e. most people I see are using this approach on GameManager. Is it also efficient to apply this for i.e. a player object or HUD? Which is harder to maintain? Destroying and instantiating objects frequently or making them live through the lifetime of application?

    4) What kind of a relationship is present between the Instances of different objects/scripts/prefabs? or even if there is any. I mean can they communicate regardless of their lifetimes? i.e. can an Instance instantiated in the 1st scene communicate directly with an instance of another object created in the 2nd scene? Do they need any other pointers other than referencing each other in the Start() or Awake() ?

    5) What is the best practice to implement references of objects to each other? I know this is quite a generalised question and may heavily depend on the overall goals and structure. But want to make sure if there are some rule of thumbs.
    I usually use GameObject.Find kind of functions which are string dependant. But this comes hard in terms of maintenance and looks tedious. On the other hand, I frequently use [SerializedField] and drop a reference in the inspector.
    Which is better and if there is any, better approach than these two, can you please point me to it?

    Most people keep telling that "you better try to make objects live independently as much as possible". While I can understand this (look below 6th question) , how do you maintain this concept? I'm trying to automate everything by binding them each other on huge scripts. Should I create a piece of script per object where I can? is this the nature of OOP? But the quantity of scripts does not guarantee this approach and in time, things become worse.

    Are delegates and events there to overcome that part of developing? Or should I stay away from them for now?

    6) Finally, I gotta learn how to use debugging efficiently. Because as the number of scripts increase and the code becomes spaghetti-like because of those above weak points of mine, things get complicated and I often find myself patching something non-sense, which makes it worse for the future.
    Any good and simple tutorials on "debugging" in Unity environment using Microsoft Visual Studio?

    Thank you all!

    PS: I use C#.
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    That's a lot of questions, some of which are too general to answer with more than "it depends."

    But I'll give you some general tips that, based on your questions, I think will help you if you can really embrace them.

    1. Encapsulation.
    As you write any script, be very stingy about what information (methods and especially data) you make public. Your attitude at every point should be "it's nobody's business how this works." And to that end, you keep all the details private. A typical script might have only 2 or 3 public methods, and everything else is private.

    The reason for this is twofold: first, it discourages spaghetti code, where scripts are referencing each other's implementation details in a big tangled mess that you can no longer understand. And second (and more importantly), it frees you to change those implementation details whenever you feel like it, without fear of breaking the rest of your app. Refactoring is our main defense against code entropy, and encapsulation is what makes refactoring practical.

    2. Unity events. Use 'em. They're not just for UI. They plug what is otherwise a big design flaw with Unity's component architecture, which is: how do all those nice encapsulated components interact? Without events, you tend to do a lot of finding other objects via Find or GetComponentOfType or related methods, and then your scripts aren't really independent at all.

    Instead, each script should mainly interact via (a) public methods that can be invoked by an event, or (b) events it invokes when anything interesting happens.

    Then you hook these up in the Inspector. (Pro tip: if you use Script Inspector 3, the inspector for events is substantially improved, including the ability to collapse them or jump to the referenced code. Certainly not necessary, but very nice!)

    Widespread use of this pattern basically enables you to do tight encapsulation, which in turn avoids spaghetti code and makes it easier to do refactoring. It also quickly leads to a library of truly reusable components you can rely on.

    HTH,
    - Joe
     
    symorian and FernandoHC like this.
  3. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    I don't have any tutorials for you, but I can give you examples of the classes in my current project which I've implemented as singletons, to give you some idea of times you might consider singletons. Each of the following features is a singleton in my project:
    • AudioDirector - A class which manages playing soundtrack audio. Other classes access it and request a new track start playing, and this class crossfades over to it.
    • InputDirector - My implementation of a user input handler, to receive user input and direct it to other classes.
    • SceneTransisionDirector - A class that is used when moving from one scene to another to ensure a smooth transition and a loading screen.
    • AudioSourcePoolManager - A class which manages my pooled audio sources, to play audio clips at different positions in the world.
    • HelpTextManager - A class which manages a UI canvas for displaying on-screen text to help the player.
    • PauseOverlayManager - A class which manages a canvas to blur the screen when paused.
    • GameObjectTrackingManager - A class which tracks the creation and destruction of all game objects in the scene in order to create collections of similar game objects, for performance reasons.
    Anyway, the point is that each of these classes has a pretty small scope, and only one needs to exist, or should exist. If it wouldn't make any sense for there to be two instances of a given class in your game, Singleton might be the way to go.


    Here's how I do it: For each singleton component, I create a GameObject that contains just that component, and add the object to Resources. When I call `Instance`, and it doesn't exist yet, the resource is instantiated, which adds a GameObject to my scene that has the appropriate component on it. So, for my AudioDirector class, I have a GameObject in Resources called `AudioDirector`, which has the AudioDirector component on it. When I called AudioDirector.Instance, that GameObject will be instantiated and added to my scene if it doesn't already exist.


    I don't think there's any meaningful downside if you're using DontDestroyOnLoad on the right components. Obviously you don't want things hanging around in memory, and actively using resources in your scene, if it's not needed. And you can make a judgment call on whether it's more expensive to repeatedly create and destroy instances of an object as needed versus keeping an instance alive forever.

    In my case, some of my singletons get DontDestroyOnLoad applied to them, and some don't. The "Directors" I listed persist between scenes, while the "Managers" don't. The main reason for this is because some functionality really does need to keep working when transitioning between scenes. For example, I don't want my soundtrack to stutter when I load a new scene. So that's a "Director", which stays alive while loading. But some things, like my Help Text, really don't need to stay alive when I'm loading a new scene, so I'm happy to just let that die and create a new one as needed.

    There are different approaches to this. I tend to only use two approaches, to maintain sanity.

    Approach 1: Access Singleton methods. This is the simplest approach, as it means that any object can feel pretty safe about calling SingletonClass.Instance.SomeMethod(). It's a one-directional communication, however. This is fine when objects need to tell the core system about something, but it's not useful when objects themselves need to be told about something.

    Approach 2: Unity Events. When I have a gameObject that needs to be told that something interesting happened, I have it register for a UnityEvent (AddListener). (Be sure to RemoveListener in the OnDisable method to avoid leaks.) For example, in my game the player can fire a weapon/device. Normally that just shoots a projectile. But occasionally other objects want to know whether the player has fired the weapon nearby. So, this class has a UnityEvent on it, which other classes can register for. Every time the player fires, it tells any registered objects that the player has fired. Those classes can then decide what to do with that information.


    This ends up being a pretty critical detail in my game. I have a game mechanic where I need to apply physics forces to most objects in the scene every FixedUpdate. Calling FindObjectsOfType<Rigidbody>() every FixedUpdate is too expensive for this. Instead, I have a Singleton class called GameObjectTrackingManager, which manages the creation and destruction of objects, and maintains some lists/caches of objects. Every object that should be tracked is given a component called a LifecycleTracker. On Awake() in that component, it accesses the GameObjectTrackingManager singleton, and tells the singleton it exists. OnDestroy, it tells the singleton it doesn't exist anymore. That way, other code that wants "all the rigidbodies in the scene" just get the list from the GameObjectTrackingManager. I have a few other similar collections on the GameObjectTrackingManager, all of which exist in order to simplify and improve the performance of accessing objects of certain classes.

    For other kinds of object, I generally only maintain references when the objects are in kind of a parent-child relationship, otherwise things can get messy. For example, my Player class has access to my PlayerVisor class, which is a class dedicated to managing the UI that shows up on the player's visor. I'm fine with accessing that from the player. But I wouldn't want any other class having a reference to the PlayerVisor as one of its properties.

    Start small. Add a breakpoint to your code, attach the debugger, and make sure you hit the breakpoint. A lot of beginners are scared away by this, but it's critical, and makes life so much easier.

    After that, you can start using the tools available in the different debug windows. When stopped at a breakpoint, you can just mouseover a variable to get its value. Beyond that, you can open the Watch window in order to see the values of many variables at once. That's most of the debugging process; just seeing the values and realizing something doesn't have the value you were expecting.

    The other big part of debugging is understanding how to move through the code as its running. This is called "stepping" in Visual Studio. The basic idea is that you can either Step Over a line of code, which means that you tell Visual Studio to execute that line of code, and move on to the next one. Or you can Step Into a line of code, which means that you drill down into the methods on that line and see what code is called within those methods. That takes a little getting used to. Just google for some Visual Studio Step Into tutorials, and I bet you find plenty of examples.
     
    symorian and JoeStrout like this.
  4. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    Hey JoeStrout,

    FoA, thank you for spending your time answering my questions.

    Indeed, I had the bad habit of making everything public as most of the novice developers do in their new journey.
    However, these days I pretty much try to make a clean distinction between what is private and what is not.

    You've said 2 or 3 public methods... is this the only "method" part? I mean are you especially emphasizing also methods should private as much as possible just like fields etc?

    But probably with lack of experience and set of mind, most beginners like me having hard times how to create independent methods within classes on objects which are supposed to communicate with other entities at the same time. This sounds a little controversy while I can fully undertstand that this is the trick that should be gained to step furthermore through becoming an advanced developer.

    At this point events are coming into the play? Right? As you've also pointed out.
    That work like a bridge and connects those independent structures?
    Maybe I should first design the whole thing in my mind or on paper and decide what parts are really dependant to what and what should stay as much isolated as possible. Actually and eventually I do this. But maybe I should pay more attention or save some time on this phase?

    Exactly. This is where I'm stuck at. :)

    Indeed, I've almost watched all videos on official Unity tutorials including events, yet the problem is I can hardly or barely imagine where and how I would use them. This also depends on amount of practice and comes by time I think? Because the hardest part with programming is, the newcomers like me usually don't have any idea how to apply those disciplines in real-life without many trials and errors and refactoring.

    BTW, I'll check Si3 in more details. Thanks for that.

    Can you recommend a nice tutorial on Debugging or an more comprehensive tutorial in Events?

    Cheers,
     
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    JoeStrout likes this.
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Actually I meant that 2 or 3 methods should be literally the only things public in the class. But that's a bit of an exaggeration. Obviously your events also have to be public.

    So, aim for just a handful of methods, and some events, as the only public members. No public fields at all.

    Yes exactly.

    Maybe. I don't actually do that, but it could be that I've been doing this for so long, I don't really need to. It certainly doesn't hurt. But also beware of trying to be too general from the beginning. Always choose the simplest solution that gets the job done, as long as you honor the principle of encapsulation so you can change it later.

    The official Unity tutorials are mostly junk. Did you watch mine?

    As for where and how to use them: when designing any component, just ask yourself two questions:

    1. What should other components be able to make this thing do? (Make a public method for each answer.)
    2. What interesting things should this component notify others about? (Make a UnityEvent for each answer.)

    For example: I have a component that simply moves its transform from position A to position B. There are public methods to tell it to GoToA and GoToB. And then there are events for when it starts moving, and for when it stops moving.

    Pretty much all my components are like that. Detected a collision? Fire an event. Ran out of fuel? Fire an event. Script to spawn visual effects? Triggered with a public method. And fires an event when the effect is done.

    Best,
    - Joe
     
    symorian likes this.
  7. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Everyone's covered everything so I'll just give out some general thoughts which are my own:

    1. Coding patterns for indies aren't necessarily the same coding patterns a large corp would use. So find what works and stick to it until you have breathing space and time to worry about doing something better. Get on with it.

    2. You don't really need many managers. Keep them to a minimum for now. When I have to use a manager, I will give it a static instance most of the time.

    3. Do your whole game using instantiate and destroy. Right at the end, check if it's harming performance. If it is (usually it's not) then pool them. This is trivial.

    4. For the love of all that is actually finishing anything, don't bother worrying about how to do it. Go right ahead and do whatever. Unity is designed for that.

    The best thing about Unity is, it's very loosely coupled so It's not hard to go back and optimise or fix stuff. It's way more important to keep moving otherwise, like most failures, you'll just end up going around in circles. I predict it wouldn't be too long before you discovered ECS and started worrying about that too. I appreciate you're just doing your best but you do not yet know what the game's most optimal approach is without having made it. So make it :)
     
    Munchy2007, symorian and JoeStrout like this.
  8. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    dgoyette,
    Thank you for your detailed response, while I need to digest some parts, I'll dropping little notes below.

    I've structured my managers/directors nearly as same as yours. From this angle I'm on the correct track but I think I really need to decide and be picky about which methods go into which class as @JoeStrout has also mentioned.

    Approach 1 : This is where I thought I'm abusing Singleton concept in overall. I mean is there any harm to make all huge classes singletons? As long as I decide to have 1 instance of it, is it fine to make it a singleton?

    Approach 2 : THE MOST CRITICAL PART FOR ME about understanding the usage of events.
    When you can already inject some if-else conditions to said subscribers, why do you replace this approach with events? Is it because to get a tidy code with less lines? Because instead of creating events and subscribers/broadcasters we can also write some conditionals in "receivers/subscribers" right?
    Is this being avoid to use frequent Update() calls? To make the code lightweight on system resources?

    Actually I've lately restructured my code around this and I have two Managers. One is Item manager the other is FX Manager. Each of these are persistent singletons and contain array of GameObjects within them for instantiation purposes. I don't if this is what you've meant and whether I'm on the right track or not? :)

    Actually I know little about how debugging operates but the main thing I don't get is, how do you decide at which line or point to place a breakpoint? Do you just suppose and say "yes this code should run up to here and pass this point so let's see if it really does?"


    Thank you again for sharing detailed information! Much appreciated! :)
     
  9. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    Thank you for the link! I have it opened now and see if I can get better with Events! =)

    Cheers,
     
  10. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that's basically it. When the program doesn't behave as you expect, there is a mismatch between what you assume is happening and what is actually happening. So you just have to check all your assumptions until you find the one that isn't true.
     
    symorian likes this.
  11. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    Thank you for those valueable thoughts!

    1) This is the way I advance for now on =)
    2) I'm trying to keep "Managers" as general and as low in quantity as possible as opposed to specific little components.
    3) Check.
    4) Yeah, I've learned to be more brave about this part and trying to preserve my persistence on this. =)

    Cheers,
     
  12. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    My general approach, and a common practice which is consistent with JoeStrout's advice, is to keep things as simple as possible. I'd rather have two classes that do two different things than one class that does both things. This takes a little discipline, since the moment you want some new functionality it's probably easier to slip it into an existing class rather than set up a whole new singleton for that new behavior. But it simplifies everything in the long run to isolate the code. So in that sense, having lots of singletons doesn't concern me at all. My default approach is to create a new singleton/manager/director when new functionality is needed, rather than trying to fold it into another class.

    Generally I don't think there's cause for concern with having any number of singletons. I'm sure it could be abused in some ways, if you have lots of code settings values on a singleton, which could get very confusing. It's already been mentioned, but most singletons I have will have a pretty small public API, with just couple of things they do, to make it dead simple to use the singleton. You'd probably want to be more concerned if your singleton class has a lot of methods and properties, as at that point it could start to feel like a global dumping ground for data and behavior that really belongs somewhere else. The decision to use a particular singleton class isn't really motivated by what would be the most convenient thing to do. It's motivated by what makes the most sense in terms of separation of functionality.

    To your second statement, I'm not 100% clear on what you mean by "subscribers" here. I consider the use of UnityEvents to be a subscriber/publisher approach itself. Maybe you're comparing two approach: A pub/sub approach versus a "Check a value every frame to see if it has changed" approach? If so, the former just feels cleaner to me, but it's probably effectively the same. I come from a coding background that has relied on subscription-based workflow, so to me that feels natural and elegant. To others, it can seem like a mess, as it separates out the class into a bunch of event handlers, which could run at various times. It's probably a matter of preference at that point. In terms of performance, it's almost certainly faster to wait for an event rather than to check a condition every frame.

    Sounds fine, but in my case the objects I'm keeping track of are objects that have already been instantiated in the scene. Think of my tracking manager as a big cache/lookup object, to make it faster to get certain kinds of objects quickly without having to rely on FindObjectsOfType.

    JoeStrout covered it. Yup, you basically use your intuition to decide where to put your breakpoint. The first thought might be: "Is my method even getting called?" So you put a breakpoint on the first line. Then you can skip further down and say, "Then why isn't the value changing?", so you put a breakpoint in the conditions above some code that should be getting called, but isn't getting called. And you look at the values and deduce why the condition is false. That sort of thing.
     
    symorian and JoeStrout like this.
  13. symorian

    symorian

    Joined:
    Jun 7, 2017
    Posts:
    69
    Thank you for all the valuable know-how. :)

    With Events, I can hardly grasp the idea of their usage. I think this will take me a while to get used to them since I have never used them.
    I have watched @JoeStrout 's video and it makes it more meningful for me yet, as I said, it will take me sometime to embrace Events in all aspects and develop a habit of using them.
    Events are like Interfaces to me, pretty sure it's useful if you know what to do but need some mind-pouring in it.

    After playing around with debugging in the last 1-2 days, I'm more comfortable now and getting used to it :)

    And finally, thank you all four @JoeStrout , @dgoyette , @TonyLi and @hippocoder for sharing some useful info and encouraging me. For the last 2 days I've been busy with cleaning/reorginizing/rearranging/refactoring my overall code and project which gave some pretty good results and now I have twice more control over everything and not getting random errors once I change things around.

    As a beginner I can suggest the other beginners like me to save some time and examine their overall structure and understand the script execution orders and decide carefully on dependencies and pay attention to encapsulation as the others above have suggested. These make things real easy.

    Cheers,
     
    JoeStrout and dgoyette like this.