Search Unity

Game Architecture -- what is your approach?

Discussion in 'Game Design' started by BIGTIMEMASTER, Nov 18, 2019.

  1. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    I doing some basic "make a game in unity" type tutorials to learn coding, so bear with me.

    I see some people set up their projects in a modular way. At the center, you have GameManager scripts, which handle the calling of events. Within events, you can subscribe whatever scripts you want. The big idea here is you are building many little pieces that you can essentially drag and drop to fit whatever configuration you need.

    Is that a correct understanding of the event system? And is this like, a standard way most programmers are working? How wide a swath of genres can this cover? Any drawbacks?

    When youre planning how you are going to design the architecture of a game, what are the biggest concerns you are trying to plan for? Is it different every single time, or is their some general templates you usually fall back on?

    bonus: anybody keep trello for some game project showing the high level planning and mind sharing and expounding on that?

    example from GTGD:

    Screenshot (3).png
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, what you describe is a good starting point for just about any kind of game.

    I made this tutorial on Unity Events years ago, and since then I've built up a library of little components designed to be snapped together via UnityEvents like LEGOs. They're great for prototyping, but even in finished, polished games, I still use them (plus custom components that work in similar fashion) all over the place.
     
    Ryiah, iamthwee and BIGTIMEMASTER like this.
  3. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    thanks @JoeStrout, i'm gonna download and check that out once I get through the event section of this current tutorial I working on.
     
  4. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    That's one way to look at it, and they're certainly useful for that. Note that it doesn't have to be about drag-and-drop, I most often use events in code to allow communication between objects which aren't specifically aware of one another.

    Some bits of advice here:
    1 - Don't go too nuts on being able to reuse individual bits. It's useful to have that as a general aim, but being dogmatic about achieving it can make things more complicated than the otherwise need to be, eat up a bunch of time, etc. Also, as a beginner it can be hard to figure out what will actually be reused.
    2 - Raise events on objects for any internal state changes which other objects might care about. Other objects are then responsible for listening to (and unlistening from) any events on other objects on an as-needed basis. Eg: a Door might raise a DoorStateChangedEvent to announce that it is now in the open state, and a Navigator may listen to that event and then decide whether the door being open should cause it to update a path.
    3 - UnityEvents are great because you can hook them up in the Inspector. However, keep in mind that having layers or chains of these hooked up in the Inspector makes things really messy to debug. I use events in the Inspector to hook up presentation stuff, but all of my core logic is hooked up via code. Of course that suits me because I'm a programmer, so your mileage may vary if you're specifically looking at events to do things without code.
     
    Last edited: Nov 18, 2019
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    You need to write your code in a way that's execution order independent as much as possible, because you can't always predict what order events will fire in. As a result this might sometimes involve having code do some redundant work (eg: recalculating values just before they're about to get changed again anyway), but usually it's relatively lightweight, and you'll usually save more overall than it might cost in individual cases.

    You also need to watch out for event loops - Event A results in raising Event B, which then raises Event A again... and crash.
     
    Ryiah, Joe-Censored and BIGTIMEMASTER like this.
  6. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Lot of this is over my head but I think will make more sense once I get a bit further.

    In case of quoted, I believe that is the reasoning behind how in this tutorial I doing now, many methods begin with

    if( thing !=null)

    This is just failsafe, right? Make sure we aren't trying to run the thing when it isn't supposed to be there?
     
    Ryiah and JoeStrout like this.
  7. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    More or less. It's really: the thing is supposed to be there, but let's not crash if, in fact, it is not actually there.
     
    Ryiah and angrypenguin like this.
  8. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yes, but it's a failsafe for a different issue.
     
  9. iamthwee

    iamthwee

    Joined:
    Nov 27, 2015
    Posts:
    2,149
    I personally, think game management / state management is only useful after you've rapidly prototyped all the aspects of your game (rather ugly coding). Then begins the stripping down and organisation. Good luck!

    EDIT*
    Also, sometimes you spend so much time trying to modularise something and making it generic to handle all cases, you end up only using it once. Use this method carefully and sparingly IMO.
     
  10. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    How different?

    As I am learning, I try to translate everything into plain English.

    So I read this:
    Code (CSharp):
    1.  
    2.   if (inventoryUI != null)
    3.             {
    4.                 inventoryUI.SetActive(!inventoryUI.activeSelf);
    5.                 gameManagerMaster.isInventoryUIOn = !gameManagerMaster.isInventoryUIOn;
    6.                 gameManagerMaster.CallInventoryUIToggle();
    7.             }
    8.  
    as: if inventory is enabled, toggle inventory UI state; etc etc

    If we had not said "if inventory is enabled", then there is danger of this running when the inventory was supposed to be hidden. For instance, is the escape menu is enabled, we don't want inventory able to be called.

    This make sense?
     
  11. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    What? Do disabled objects return null? I didn't think that was the case.
     
  12. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    No. This says, if the inventoryUI reference is not null. It is probably not supposed to be null; it would be null if either (a) the inventoryUI reference were not properly assigned in the first place, or (b) the inventoryUI has been destroyed.

    Has nothing to do with whether or not it's enabled.

    Any more than this thread has anything to do with game design. :p
     
    Ryiah and angrypenguin like this.
  13. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    null is not antonym of enabled?


    (thread was originally about big picture how you think about designing game architecture. I thought that goes under design, since its big picture stuff you plan ahead of time)
     
  14. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Actually, if inventoryUI is null it means that there is no UI object at all. For example, if I write:
    Code (csharp):
    1. inventoryUI = getComponent<Canvas>()
    On an object that has no canvas, then inventoryUI would be null.
    or if I write
    Code (csharp):
    1.  
    2. Destroy(inventoryUI);
    3.  
    Then it would be null.

    If you want to check if inventoryUI is enabled youd have to check if
    (inventoryUI.enabled)
    .
     
    Ryiah, angrypenguin and BIGTIMEMASTER like this.
  15. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    gotcha. so null = it does not exist in any context, whereas enabled/disabled is just a toggle state of a thing that has been declared to exist?

    While that clears up these words, I am not sure I understand then why I would be saying if (thing != null) at all.

    Well whatever that is dragging away from main point of thread and its probably something I will come ot understand just by writing more code.
     
  16. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,706
    Maybe think of inventoryUI like a field in the Inspector:

    upload_2019-11-19_14-52-57.png

    The field can point to an Inventory UI, or it can point to nothing as in the screenshot above. When it points to nothing, its value is null.

    So if (inventoryUI != null) checks if inventory UI points to something.

    You only want to activate what it points to if it actually points to something.
     
    BIGTIMEMASTER likes this.
  17. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Something bad is happening if we are looking for something that's not there? Like an infinite search that waste resources?
     
  18. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Depends what you mean by "looking for" but if you attempt to access a member or function from a null reference, you just get an error message.

    Yeah, just search the scripting forum for "null reference exception". You'll find thousands of them. :D
     
  19. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,706
    Exactly. It isn't necessarily bad. It might just be that no one assigned anything to that "Inspector field" (variable), which might be perfectly allowed in the right context.
     
    Ryiah, Joe-Censored and angrypenguin like this.
  20. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    Code (CSharp):
    1.             if (inventoryUI != null)
    2.             {
    3.                 inventoryUI.SetActive(!inventoryUI.activeSelf);
    4.                 gameManagerMaster.isInventoryUIOn = !gameManagerMaster.isInventoryUIOn;
    5.                 gameManagerMaster.CallInventoryUIToggle();
    6.             }
    Code like this is something that most programmers do a lot when they start coding, then over time they realize that it's almost always a very bad thing to do.

    What happens is, they realize that NullReferenceException can happen very easily in many situations, so to "fix" the situation, they start putting these null checks everywhere.

    So when a method is called with bad data, it just silently does nothing, instead of allowing an exception to be thrown.

    The short term benefit of doing this is that you might turn bugs that used to be game-stopping bugs into lesser bugs.
    Maybe before the != null fix, your UI manager was getting completely stuck when OpenUI threw an exception, and now the inventory just never opens. So it's clearly better and makes for a more stable game, right?!

    The problem with this is that you are basically ignoring the real source of the bug by hiding one symptom of it. It's like shooting the messenger.

    Why was inventoryUI null in the first place? Should we assign the reference in Awake instead of Start? Did we forget to disable the player inputs between level loading? Was it destroyed as a side-effect of something?

    The more error hiding you do, the more likely it is that the real issues never get fixed. Also, it gets harder and harder to find the source of a problem, when you don't immediately see an error message when something is unexpectedly null.

    Being aware of possible exceptions and preparing for them is a good thing, but just adding if(x != null) is not a good method of preparation.


    If it should be possible to call a method and for it to fail to do it's intended task, you should always notify the caller about it, so that it can react accordingly. For example this would make a lot more sense:

    Code (CSharp):
    1.  
    2. /// <summary> Open the inventory if possible at this time. </summary>
    3. /// <returns> True if inventory was opened, false if not. </returns>
    4. public bool TryOpenUI()
    5. {
    6.     if(inventoryUI == null)
    7.     {
    8.         return false;
    9.     }
    10.     ...
    Now the caller clearly knows that the method can fail to open the UI from just the name of the method, and it has a way to detect when this happens, so that it can react accordingly. It is then for example less likely to get stuck waiting for the onInventoryClosed callback that never takes place, because the inventory was never opened in the first place.
     
    Ryiah, Socrates, angrypenguin and 2 others like this.
  21. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    awesome thanks guys.

    I think I understand it at least well enough to keep it working... for now.

    but yeah, back to the topic. uhhh, yeah. lol
     
  22. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @SisusCo

    awesome! thanks. that all seems to make sense. I am following a beginner how to make a game tutorial. Maybe for simplicity sake it is kept like this. Or maybe the author just does things that way and isn't aware.

    I don't think I know enough to really implement this right now, but the idea is there so once I can I start looking at how I can remove this sort of dangerous habit. Maybe to start I make a second project, using this one as a guide but rewriting the scripts to err away from !=null checks like this.
     
  23. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Actually, I curious so I gonna drag my own thread a bit more off topic here. Maybe it would be worth splitting into two threads, if a moderator feels inclined.

    @SisusCo , please forgive me I am noob with code and a bit dumb as well so I have to really try to break things down Barney-style, but, let me ask you if I got this more or less right:

    Right now, with these null checks I am saying, "if the thing isn't null, run the methods (or whatever is within the if statement)." The problem being that if it is null, I am overriding editors typical behavior of saying, "hey you forgot something!"

    But what you propose is to say, "Check and see if the thing is equal to null, and if it is, cancel the methods (or whatever you wanna do)." And this will not override the editors preferred behavior of saying, "hey you forgot something!"


    And so your code might be more like,

    if(thing is equal to null)
    {
    send a debug message
    }

    else
    {
    do what is intended
    }

    Is this right? I know that translating code to plain-speak isn't always possible but it helps me understand.
     
    Last edited: Nov 19, 2019
    angrypenguin likes this.
  24. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    Yes, that's right.

    It has a lot to do with how you name your methods, and what one would expect to happen when a method is called. If your method is named "OpenUI", but it doesn't open the UI, and it doesn't tell anybody about it, that is very unexpected behaviour.

    For such a method, throwing an exception would be better than doing nothing, because then at least we know that something went wrong.

    Consider this for a cautionary example:

    Code (CSharp):
    1. void SaveGame()
    2. {
    3.     if(saveManager != null)
    4.     {
    5.         ...
    6.     }
    7. }
    Imagine that your saving system broke down a week before the release of your game, but nobody noticed it, because there was no exception being thrown when you pressed the quick save button! As a result you now have hundreds of angry players with lost progress.

    So throwing an exception when saveManager is null is clearly much better in this situation.

    It might feel more intuitive if you do check for null and then manually throw an exception:

    Code (CSharp):
    1. void SaveGame()
    2. {
    3.     if(saveManager == null)
    4.     {
    5.         throw new NullReferenceException("Unable to save game because saveManager was missing!");
    6.     }
    7.     ...
    8. }
    Now you can perhaps see more clearly how the exception is just part of good communication.

    Player: Save the game, please.
    Save system: No can do! I can't find the save manager!
     
  25. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    that seems simple enough. Maybe instead of reinforcing a bad habit I go back and refactor this stuff.
     
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    I wouldn't sweat it too much at this point, you'll learn these things by heart over time. Getting things done is also a goal worth striving for - one might say it is almost as important as having zero error hiding ;)
     
    angrypenguin and BIGTIMEMASTER like this.
  27. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Too late. I already done it. It wasn't that much work and for me right now, I think is good thing as it forces me to go over my code very carefully and try to understand it a bit better. Only took ten minutes anyway. Everything working and I tested it for failure too, so I think it's good.

    thanks again!
     
    SisusCo likes this.
  28. Socrates

    Socrates

    Joined:
    Mar 29, 2011
    Posts:
    787
    It may just be one of those things that the author was taught ... or whomever they learned from was taught ... or on down the chain. When I took programming courses in college, in at least some I was taught "defensive coding" practices like that. Back then, the languages were not as robust as they are now (decades later), so part of the defensive programming was aimed at just keeping the programming from crashing entirely when something was unexpectedly null.

    Another defensive programming technique you may see is what I learned to call a "sanity check". This is when you sanitize the incoming or stored values before working with them, though again people often forget to make a log of what they're doing and so hide bugs. Sometimes it might be perfectly safe to just sanitize the value and keep going, but you have to be aware of what you are doing and why.

    Code (csharp):
    1.  
    2. ExampleFunction(int start, int end)
    3. {
    4.    // Sanity check.
    5.    if(start < someMinValue) start = someMinValue;
    6.  
    7.    ...
    8. }
    9.  
     
    SisusCo and BIGTIMEMASTER like this.
  29. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    I definitely agree with the overall vibe. I don't necessarily agree with always returning success/fail status, though, because that can over complicate things elsewhere. To a large degree this is what exception handling is for, though that's a topic for the scripting forum.

    I also use events a lot, and push the responsibility for this stuff onto the responder rather than the raiser. Eg: when I pause the game I don't have my pause controller specifically request for the pause menu to appear. Instead, I raise an event that says "the game is now paused", and my pause menu listens for this event and turns itself on when it receives it.

    There's a distinction between "software design" and "game design". Software design is about how software will be implemented and operate, ie: how the code will be written and how it will manage its data. Game design is about how people interact with your game, its rules, etc. You could have the same game design implemented using many different software designs.

    This is a pattern I use a heck of a lot. A couple of things to note. First, you can check for more than just null references. Any condition can be checked. Second, when you log error messages try to communicate so that other people can understand how to fix it.

    The downside is that this approach can result in pretty verbose code.
     
    BIGTIMEMASTER likes this.
  30. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    I know, but I thought this was better place than scripting subforum.
     
    angrypenguin likes this.
  31. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    So when I'm thinking about game architecture, it is usually different for each game.

    I'm trying to come up with an overall framework which will answer a bunch of different questions. Common questions include:
    - Does the game need explicit states or modes? If so, how can I best manage them?
    - How can I best support the player moving through the environment as needed? Eg: how will I load levels, etc.
    - What runtime data needs to be persistent, and how will that be managed?
    - What data is required by the game, and how will that be managed? This includes a lot of things, from writing and localisation to how you design and integrate levels.
    - What objects need to communicate with one another, and what approaches will be most effective to facilitate this?
     
    BIGTIMEMASTER likes this.
  32. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Runtime data meaning, things that are dynamic and changing? For instance, you call on some lightmaps one time. THat happens at level load. That is not runtime data, right?

    But you have realtime lights, the variables are constantly changing, and it also interacts with other objects like characters, props, etc. That is runtime data.

    And updating player score, or calling death UI. THat is all runtime stuff.

    So what isn't runtime data?
     
  33. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Pretty much. Anything data that your game generates or modifies while it is running.

    Light bakes are a good example of non-runtime data. They're generated by the Editor and only read at runtime, not created or modified.
     
  34. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    I was maybe a little bit unclear in my wording there. What I meant is that if you have a method that is expected to do something, but it is a valid possibility by design for it to fail to do this, then it is good practice to name the method in a way that clearly communicates that there's a possibility of failure, and for the method caller to be able to know whether or not it succeeded.

    So I'm not saying that you should always return success/fail status, but that if you were to abort a method in the case of a null reference without logging any errors, this is a way in which you can do it validly.

    E.g. both the indexer and TryGetValue in the Dictionary class are good, but if the indexer just silently returned null when the key was not found in the collection, that would be bad.

    Yeah, in these instances the Try pattern doesn't make sense, as it is almost always by design that listeners can be added and removed without the event broadcaster knowing (or at least caring) about it. As such the listener is left with the responsibility of deciding if errors, warnings or nothing should be logged when failing to do something.
     
    Last edited: Nov 20, 2019
    Socrates and angrypenguin like this.
  35. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    One very good practice when it comes to game architecture that hasn't been mentioned here yet is using interfaces.

    Interfaces can give you lot more flexibility to swap some things around as your game inevitably evolves and changes during the game development process. It can also help make your code more modular and reusable, so it is easier to reuse the same code in multiple places without having to write basically the same thing again.

    GetComponent works with interface types, so it is really easy to hook your components together using implemented interfaces instead of directly referencing exact component types.

    Unfortunately Unity can't currently serialize interface types or display them in the inspector out of the gate, but if you fetch your components when the game loads it works really well. Not having easy serialization and inspector support is the one big downside to using interfaces in Unity, so you need to decide if you are willing to live with or work around those compromises. In very small projects it might not be worth the trade off, but it depends a lot on how you prefer to establish connections between your components etc.

    I'll try to give a concrete example of the benefits of using interfaces. Say you have a Player class in your game that looks something like this:

    Code (CSharp):
    1. public class Player
    2. {
    3.     public int hp;
    4.     public int attackPower;
    5.    
    6.     public void Damage(int amount)
    7.     {
    8.         hp -= amount;
    9.         if(hp == 0)
    10.         {
    11.             Die();
    12.         }
    13.     }
    14.    
    15.     public void Attack(Enemy enemy)
    16.     {
    17.         enemy.Attack(attackPower);
    18.     }
    19.    
    20.     public void Die()
    21.     {
    22.         Destroy(gameObject);
    23.     }
    24. }
    After working on the game for some time you realize that the Player class has become really bloated with thousands of lines of code. You try get closer to following the single responsibility principle and decide to split the Player class into multiple components, and end up with something more like this:

    Code (CSharp):
    1.  
    2. public delegate void OnAttackedCallback(int attackPower);
    3.  
    4. public class Attackable : MonoBehaviour
    5. {
    6.     public OnAttackedCallback OnAttacked { get; set; }
    7.    
    8.     public void Attack(int attackPower)
    9.     {
    10.         if(onAttacked != null)
    11.         {
    12.             onAttacked(attackPower);
    13.         }
    14.     }
    15. }
    16.  
    17. public class Killable : MonoBehaviour
    18. {
    19.         public int hp;
    20.        
    21.         private void Awake()
    22.         {
    23.             var attackable = GetComponent<Attackable>();
    24.             if(attackable != null)
    25.             {
    26.                 attackable.OnAttacked += OnAttacked;
    27.             }
    28.         }
    29.        
    30.         private void OnAttacked(int attackPower)
    31.         {
    32.             hp -= attackPower;
    33.            
    34.             if(hp == 0)
    35.             {
    36.                 Die();
    37.             }
    38.         }
    39.        
    40.         public void Die()
    41.         {
    42.             Destroy(gameObject);
    43.         }
    44.        
    45.         private void OnDestroy()
    46.         {
    47.             var attackable = GetComponent<Attackable>();
    48.             if(attackable != null)
    49.             {
    50.                 attackable.OnAttacked -= OnAttacked;
    51.             }
    52.         }
    53.     }
    54. }
    You can now reuse the same components even for your enemy units, so you're really happy about the change!
    By using events to hook your components together, you were also able to decouple your components from each to some degree, again increasing flexibility.

    However the Killable component still only works if the exact component Attackable is found on the GameObject. You realize that you want your units to sometimes die for other reasons too, like if they collide with something at high speed.

    Now you could just keep adding more and more direct references to more and more components that can cause your units to take damage. However you could also use the power of interfaces to make it so that Killable doesn't care about the exact type of the component(s) that will send it callbacks to take damage. This way you can easily take away Attackable and replace it with Collidable, or add them both. Then later on maybe you add Burnable to the mix too.

    By using interfaces you can decouple your components in both directions, while with events you could only decouple them in one direction.

    So you create some new interfaces for your components:

    Code (CSharp):
    1. public interface IKillable
    2. {
    3.     void Die();
    4. }
    5.  
    6. public interface IAttackable
    7. {
    8.     void Attack(int attackPower);
    9. }
    10.  
    11. public delegate void OnHitCallback(int hitPower);
    12.  
    13. public interface IOnHitCallbackProvider
    14. {
    15.     OnHitCallback OnHit { get; set; }
    16. }
    Then you refactor your components a little bit to use these interfaces instead of specific types when referencing other components:

    Code (CSharp):
    1. public class Attackable : MonoBehaviour, IAttackable, IOnHitCallbackProvider
    2. {
    3.     public OnHitCallback OnHit { get; set; }
    4.    
    5.     public void Attack(int attackPower)
    6.     {
    7.         if(OnHit != null)
    8.         {
    9.             OnHit(attackPower);
    10.         }
    11.     }
    12. }
    13.  
    14. public class Killable : MonoBehaviour
    15. {
    16.         public int hp;
    17.        
    18.         private void Awake()
    19.         {
    20.             foreach(var hitCallbackProvider in GetComponents<IOnHitCallbackProvider>())
    21.             {
    22.                 hitCallbackProvider.OnHit += OnHit;
    23.             }
    24.         }
    25.        
    26.         private void OnHit(int hitPower)
    27.         {
    28.             hp -= hitPower;
    29.            
    30.             if(hp == 0)
    31.             {
    32.                 Die();
    33.             }
    34.         }
    35.        
    36.         public void Die()
    37.         {
    38.             Destroy(gameObject);
    39.         }
    40.        
    41.         private void OnDestroy()
    42.         {
    43.             foreach(var hitCallbackProvider in GetComponents<IOnHitCallbackProvider>())
    44.             {
    45.                 hitCallbackProvider.OnHit -= OnHit;
    46.             }
    47.         }
    48.     }
    49. }
    Now everything works just like it used to, but you are able to add as few or as many different components that implement IOnHitCallbackProvider to your units as you like. So you are now able to create your Collidable component also:

    Code (CSharp):
    1. public interface ICollidable
    2. {
    3.     void Collide(ICollidable other);
    4.     int GetForce();
    5. }
    6.  
    7. public interface IMovable
    8. {
    9.     float Speed { get; }
    10. }
    11.  
    12. public class Collidable : MonoBehaviour, ICollidable, IOnHitCallbackProvider
    13. {
    14.     public float mass;
    15.  
    16.     public OnHitCallback OnHit { get; set; }
    17.        
    18.     public void Collide(ICollidable other)
    19.     {
    20.         if(OnHit != null)
    21.         {
    22.             OnHit(GetForce() + other.GetForce());
    23.         }
    24.     }
    25.    
    26.     public int GetForce()
    27.     {
    28.         var movable = GetComponent<IMovable>();
    29.         if(movable != null)
    30.         {
    31.             return Mathf.RoundToInt(movable.Speed * mass);
    32.         }
    33.         return 0f;
    34.     }
    35. }
    etc...

    Sorry about the wall of text :D I find it easiest to communicate the benefits of interfaces via concrete examples.
     
  36. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    awesome @SisusCo!

    I don't think I've got enough prereq knowledge yet to really implement this -- I am still getting to grips with very basic syntax -- but once I finish this current tut I am going to try rebuilding it as a new game, and I think then it may be a good time to try out some more advanced ways of working like this.
     
    SisusCo likes this.
  37. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Just want to point out that the approaches mentioned here are not the only ways to go. In fact I believe they are very uncommon approaches for smaller projects. Using singletons, or otherwise just FindWithTag followed by GetComponent then caching the result is a lot faster to implement and works just fine for a lot of the projects a solo indie dev will jump into.

    It takes some extra time to build out an infrastructure for assembling a project like Lego bricks, which is arguably not worth it unless you're expecting a pretty extended development timeline or you're working with a group of developers. Don't turn a 6 month project into a year just to make it more modular. Modularity really shines where it helps you not turn a 3 year project into a 5 year spaghetti code nightmare.

    YMMV
     
    SisusCo and BIGTIMEMASTER like this.
  38. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Agreed. The purpose of these tools is to make development more efficient, not less. If a tool is making your work slower then either you're not using it correctly, or it's the wrong tool for the job.

    Which ones? Even for small projects I wouldn't hire anyone who wasn't competent with everything discussed in this thread. I wouldn't want them to apply every one in every case, but I would want them to be aware of this stuff to be able to make informed decisions.
     
    Joe-Censored likes this.
  39. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah I mean you got to know how to do the work correct in the first place in order to know when it's okay to do things fast/sloppy.
     
  40. iamthwee

    iamthwee

    Joined:
    Nov 27, 2015
    Posts:
    2,149
    I must say, I'm really impressed at your approach to coding. A lot of devs who specialise in one field won't dare venture into another. I also think it helps you appreciate how other members of your team operate and understand the challenges they face(d).

    Overall, when you can share and understand the process / workflow of others it helps you become better in general.
     
    angrypenguin and BIGTIMEMASTER like this.
  41. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah I agree completely. And since all these fields are interrelated, I've found that the more I learn about another field, it usually pays back directly to my main area of interest. For instance, learning about rigging and animation helps save me a ton of time modeling, because I have better understanding of the full pipeline, so I don't waste time on frivolous things like I did before.

    Of course it takes time, and if I had been only working on mastery in one narrow field I would be that much better. But that doesn't suit me.

    To be honest, I am really running my patience with trying to learn coding. It is not enjoyable at all for me. I really hate it. But there is some work I must do, so that's that. Thankfully there is so many great teachers out there it makes the task possible for me. If I was God in heaven I'd be throwing out blessings left and right for all the educators out there. Every one of them is a Jesus to me.
     
    Socrates and iamthwee like this.
  42. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    I think a clear indicator that something isn't for you on a fundamental level is how much you procrastinate when doing it. I spend a lot more time procrastinating right now as I am working on coding. When I am doing my art, I get up early in the morning all excited to work, and hardly stop all day. There is never enough time to do everything I want to do.
     
  43. iamthwee

    iamthwee

    Joined:
    Nov 27, 2015
    Posts:
    2,149
    It's not for everyone, every small indie has an area they gravitate to. But I think if you stick it out long enough, with everything it gets easier. Think about when you started Maya, and you take a picture you sketched 'side view, top view, front view' -> create sculpt -> bake high res normals -> retopo -> create weight painting -> rig -> animate dozens of poses.

    There was a time when that would seem daunting, now it is pretty much second nature. Trust me, coding will yield a similar result in time. And yes, youtube etc is a godsend, I'm completely self taught in all disciplines of game dev.
     
  44. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Yeah, lol. I remember when I first started in maya I was afraid of the channelbox/layer editor. It looked so complex I just never wanted to have anything to do with it.

    With coding, I have literally only been at it for a few weeks. I am kind of getting where I can visualize and understand the theory of what I want to do, but the syntax doesn't come to me at all. I have to look it up every time, which is very slow and annoying.
     
  45. iamthwee

    iamthwee

    Joined:
    Nov 27, 2015
    Posts:
    2,149
    And just to echo what everyone was saying, you don't have to get involved in high abstract paradigms right away. In fact, I think it is more of a hinderance then anything else, just concentrate on making workable prototypes, the rest will fall into place.
     
    BIGTIMEMASTER likes this.
  46. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah that is a good point. I had to check myself this morning because wanting to do things "right" was stressing me. Now at end of day I have a very ugly but semi-functional character controller.
     
  47. MrArcher

    MrArcher

    Joined:
    Feb 27, 2014
    Posts:
    106
    The place I work at has a couple of people like yourself - artists who're self-training to code. This was a mindset with them, and I think it might stem from being accustomed to the 3D art workflow. A good portion of what you do in 3D (unless you're incredibly careful and/or meticulous) is a destructive workflow. Once something's done, it's hard to undo it once the file's saved.

    With art, each asset can be a 1-and-done experience. You work hard to make sure it's right the first time as it's difficult to change later (messed up normals? Rebake and retexture the asset). A particular asset might need tweaks later, but for the most part it's complete, modular and ready to get in-game.

    Code's the opposite. Sure, you can try to make every script completely modular and write it in one go, but you'll waste more time than you'll save. You can chop and change, refactor and even restructure months later. You've always got all the source files on hand because the source files are the end product. Better to get something working and messy than perfectly structured and 1% complete. You can always refactor and clean it up later.
     
    Socrates, iamthwee and BIGTIMEMASTER like this.
  48. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Great point! I am not sure I fully agree about 3d art though... I mean having to rebake something isn't more than 10 minutes if you set your workflow up properly. And redoing things is just a fact of the job. It is going to happen almost everytime. In fact... I mean I think if you aren't working experimentation and do-overs into your art you are missing so much opportunity.

    Just my experience, which may be less than some people you work with though. But I agree completely with the sentiment.
     
  49. Socrates

    Socrates

    Joined:
    Mar 29, 2011
    Posts:
    787
    This is probably diverging even further from what this thread was about in the first place, but: Have you tried some of the psychological tricks to reward yourself into coding? For example, assuming that the procrastination is wasted time (as opposed to doing something useful that isn't the coding), you can fill that time with something you enjoy as an inspiration or reward.

    Having trouble getting out of bed and working? Give yourself an hour of art every morning to get yourself moving, then go on to coding. Just make sure you lock it into an hour with a timer. This gets your butt in the chair and your brain working.

    Just need a reason to get the slog started? Pick a portion of the procrastination time and assign it as time for art, but only at the END of the coding day. For every minute you spend procrastinating the coding at the start of the day, you cut out a minute of this reward time. This trains you to think, "I have to start so I do not lose any of the time I am looking forward to."

    There's other things you can do, of course. The techniques that work for me tend to boil down to keeping myself from wasting time by training myself to recognize that the wasted time is coming from the pool of time I have for things I would enjoy doing.

    These kinds of things also work for stuff like, "I can't watch the movie unless I put in X hours of Y work first."
     
    BIGTIMEMASTER likes this.
  50. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @Socrates oh yeah this is what it's all about really.

    I don't like to switch gears so much and go into art because that does have it's own technical considerations you got to "load" into the brain. I find writing, filling out GDD, drawing diagrams and stuff for people I want to hire for certain task, menial stuff like that is a good break if I want to be useful but not code.


    Lately I've taken to ice cold baths and exercise. Shocks the body, get's my hyped, doesn't give a hard crash like sugar/caffeine.