Search Unity

Code organization - inheritance / components based / interfaces - requesting opinions

Discussion in 'Scripting' started by honprovet, Feb 27, 2015.

  1. honprovet

    honprovet

    Joined:
    Mar 4, 2014
    Posts:
    23
    Hello dear forumers,

    I've been a silent reader of these forums for a while now, and i'm much thankfull for all the support that I got from them.

    Now, I'd like to ask you a question. Here's the panorama:

    • I'm about to start a new project. Simply put, its an RTS.
    • I'm not an experienced programmer, nor am I a professional (duh) or a student. I do it for the boner of getting stuff to work properly. So I might be saying a lot of stupid stuff.
    • I'm also not a kid, even though i wish i was.
    Here's the dilema:

    • I've been struggling with finding a way to organize my code.
    • I read about full inheritance, full component based, a mix of both, interfaces and a mix of all.
    • I love the idea of virtual methods.
    • I don't get what would be the benefit of interfaces in my scenario.
    • I'm prone to go by using a simple inheritance system and mostly component based coding. The example below shows what I mean (it's just an example, not the full stuff).
    Apresentação1.jpg

    And, without further ado, here are the questions:

    1. What's your opinion on this aproach? Any sugestions?
    2. Can you please enlighten me on a good reason to add interfaces to this scenario? Simple examples are most apreciated. How is that diferent from turning the (2) Classes into abstract Classes? How is that better than wasting the super hot virtual methods that have basic functionality?
    3. What would be a good way to go for the comunication between components? Let's say the Truck wants to use RangedAtt to reduce Player's Health. I thought of theses options:
      1. RangedAtt knows the Player GameObject (say we need to right click it to start attacking, then it gets a reference to the GameObject), therefore it could:
        1. AddComponent his Health, use a method, then destroy the link (null the var?) - repeat everytime the attack happens;
        2. Send a message to use a method - repeat everytime the attack happens.
      2. Ok that's all I thought about.
    Thanks a bunch!

    Edit.

    Ok i thought of a good reason to use an interface.

    ISelectable.

    Has the Selected property. Is implemented on the classes that (guess?) are selectable.

    Say i want a region selection, then i check for ISelectabe, if it is, it gets selected.

    Is that the idea???

    P.s. Sorry for bad grammar, I'm not a native speaker.
     
    Last edited: Feb 27, 2015
  2. Zaladur

    Zaladur

    Joined:
    Oct 20, 2012
    Posts:
    392
    The 'Destructible' portion seems a bit off to me, personally. It seems that health, integrity, and durability could potentially inherit the same parent - they all are simply value managers, keeping track of a value, capping it at some maximum, and probably triggering some sort of action when reaching 0.

    Similiarly death and collapse could make sense to be grouped together - they are in charge of destroying the object in potentially different manners. I would be curious to see the contents of the Destructible Class which cause it to be a parent of all 5.

    If the AttackMethod knows its target gameobject, getting the Health component with a GetComponent and then calling a public method on it is certainly one way to do it. Here is one place I would make use of inheritance. Since your target could be a truck or a player (or whatever has Durability?), you grab the abstract parent of them and call a generic 'DealDamage' method. Then the classes can handle this method in whatever way they need to. This is another reason I would separate out Death and Collapse from the group- they wouldn't have a method to add or subtract damage.

    Finally, interfaces vs abstract classes - Interfaces act as contracts for implementation. One of the benefit of interfaces is that you can have more than one. Lets say you have the interfaces iMovable, iTargetable. You might have a fireball that you ensure has the Move method, so you give it iMovable. You might have a stationary Turret that can be targeted and attacked, so you give it iTargetable. And you might have a zombie that should both move and be targeted, so you give it both iMovable and iTargetable. This might not be a good example for component based design.... but hopefully you get the idea. Interfaces are slightly more flexbile because you can use more than one, and generally don't need to be too tied to the class itself.

    Abstract classes, meanwhile, allow you to actually define some implementation details to pass onto the children class, but don't want any instances of the parent class. They are more useful for defining a subset of classes. 'Vehicle' would make a good abstract class - you will never have a generic vehicle - it will always be some type, whether it is a truck or a boat or a plane. But maybe you want all vehicles to have an identical HonkHorn() method, where you simply play the assigned horn sound clip. With an iVehicle interface, each vehicle would need to implement its own HonkHorn method - something you can just do in the abstract class once.

    The choice between interfaces and abstract classes isn't always black and white, and there is certainly some overlap where either one would work fine. In Unity I personally tend to have many more abstract classes than interfaces, but thats me.
     
    Last edited: Feb 27, 2015
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Get ready for the flood of opinions! :) I'll try to substantiate mine.

    Are you sure you need the inheritance? Why do Health, Death, Collapse, etc., need to be a subclass of Destructible? If they share common code, can they instead have a reference to a separate Destructible component?

    You'll find that your code is shorter and simpler with a flat hierarchy where all of your scripts inherit directly from MonoBehaviour. It helps reinforce the one script-one purpose principle. And since you don't have to trace where inherited methods come from, the code is much easier to understand. Virtual methods seemed nice in the 1990s, and they have their uses, but composition is often a better choice in modern game design.

    Also, since you're just getting started with the project, use the new event system in Unity 4.6+ instead of legacy Unity messages. It's as efficient as C# events but works really nicely in the "Unity way" while providing contracts similar to C# interfaces. In what you described, I don't think interfaces would provide any significant benefits over the event system.
     
    Deleted User likes this.
  4. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    FWIW - in our project (also an RTS):
    - if a thing is a unit it gets a UnitManager : MonoBehaviour
    - if a thing is a structure it gets a StructureManager : MonoBehaviour

    That's really it. Those two classes handle the interactions for those types of "things" in the world.

    To enable modding we serialize a series of XML files at start up that build the specific properties for units and structures. You're better off being more data driven in an RTS than building deep hierarchical models.

    I posted more about our approach in this thread: http://forum.unity3d.com/threads/rts-starter-kit-wip.293305/#post-1945595
     
  5. Dameon_

    Dameon_

    Joined:
    Apr 11, 2014
    Posts:
    542
    Events, abstract classes, and interfaces are all different ways of decreasing coupling. They all have their own benefits and drawbacks, and their own scenarios where they excel.

    Unfortunately, Unity isn't too friendly with interfaces, so I find myself avoiding them in a lot of scenarios I'd use one. For example, interfaces don't show up in the editor as something you can assign or assign to. Interfaces are normally something that I would use much more heavily than Unity allows.

    All three are methods you should be familiar with, and it's rare that I don't use all three in a project as ways to decrease coupling and invert control. When I want to have base functionality that I can extend, I use an abstract class. When I want ensure a class implements certain methods and properties but leave it up to the class how that's done, interface. When I want to have an object communicate with a child in a completely decoupled way, I set up an event.
     
  6. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I use a structure superficially similar to the OP, the implementation details are cleaner, but I think the idea is the same. Works well enough for me.

    Level 1 is MonoBehaviour

    Level 2 is all built of abstract base classes. In many projects the list runs something like this.
    • Engine
    • Steering
    • AI
    • Weapon
    • Life
    All of the dependencies are in this level, using [RequiresComponent]. The AI requires a Steering and a Weapon. The Steering requires an Engine. The Engine requires a RigidBody. And so forth. GetComponent is not used at lower levels

    All of the public API lives on this level. There are no exposed members at lower levels.

    Level 3 holds the actual implementation. This is where you have Cannon, Rifle, BlasterPistol and WaterGun and NoAttack.



    The big advantage of this is the modularity. I can combine any Engine with any Steering and any AI. Want my player to pilot an enemy fighter? That's a two second change. Want to convert from a Blaster to a MegaCannon? Another two second change. Making an NPC? All you need to do is switch out the PlayerInput for an AI, and nothing else changes.

    I typically use interfaces heavily with the EventSystem. Interfaces are a great way to pass data between scripts that know nothing about each other. Classic example is IDamagable. A bullet sends out the damage message to everything it hits. On an enemy this might be picked up by the Life component. But it doesn't always make sense to have a life component on a destructible object. Definitely not on a switch that's meant to be triggered by taking damage. Using an interface its also possible for other components to 'listen in' for the events. Say you have a particularly timid AI that will panic on the first sign of damage. Implementing IDamagable may be a better solution then polling life frequently.

    Another way to implement interfaces would be to scrap component references altogether. Have each component call ExecuteEvents on itself, and let it be picked up by whoever cares. This would be the ultimate in decoupling. Most of my projects tend to go for the loose coupling I explained above over complete decoupling.

    I also have a bunch of utility components running around. These are meant to do things like return the object to a pool if its destroyed. Or count the number of bullets fired for the quest system. Or display little numbers on the UI when something takes damage. These generally stay completely independent of the main scripts, and don't do much interaction at all.


    TL;DR? I described a system using dozens of components on each GameObject, and some good cases for interfaces.
     
    sluice likes this.
  7. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I would further split your Destructible up. Keep how an entity dies (Health, Integrity, Durability) separate from what happens when the entity dies (Collapse, Death). This will make things cleaner.

    Put the interaction and communication on level 2. That means you don't have any dependencies between classes on level 3. A huge boon in terms of modularity and reusability.
     
  8. honprovet

    honprovet

    Joined:
    Mar 4, 2014
    Posts:
    23
    Thank you all for the replies.

    Oh, i think I get it. If I know the GameObject (say i called it go) that must be damaged, i can just do go.takedamage(), and that will work like a charm, right?

    Thank you, that's very clear. This was posted on answers by kromenak, seems to make sense;

    'In our game, we use abstract classes when we have multiple items that share some base functionality, but that base functionality is not enough to be an object itself. For example, a Weapon class can contain functionality that is common to a crossbow and shotgun, but an instance of the Weapon class is pretty useless by itself - so making it abstract might be a good idea.

    Interfaces are useful when you want to refer to several disparate objects by a common attribute - this makes it possible to put things into lists and iterate through them, even if they are very different classes. For example, a Robot and a Crate are very different objects in a game, but if both implement a Breakable interface, they can be considered equal objects, at least in the respect that they are both breakable. You can then define a

    1. ListBreakable
    for example, and keep track of a lot of breakable things."

    That's the point of this thread! I noticed this is a very discussed topic, and opinions are of all kinds. Maybe that's a good thnig, 'cause it shows that both ways actually work, and maybe it's all personal preference.

    I'll do some reading on the new event system.

    :):):)

    Maybe this is a stupid question, but I'll ask anyway. That would go something like this?

    Code (CSharp):
    1. abstract class Desctructible: MonoBehaviour
    2. {
    3.     public abstract int Current { get; }
    4.     abstract public int TakeDamage(int amount);
    5. }
    6.  
    7. class Health : Destructible
    8. {
    9.         private override int Current // overriding property
    10.         {
    11.             get
    12.             {
    13.                 return Current;
    14.             }
    15.         }
    16.  
    17.         private override void TakeDamage(int amount)
    18.         {
    19.             Current -= amount;
    20.         }
    21.  
    22. }
    Then if I needed to deal damage, all I had to do was:

    Code (CSharp):
    1. GameObject target =(someway to take the target);
    2.  
    3. target.TakeDamage(damage);
    Is that how you do it?
    Will try it!
     
  9. honprovet

    honprovet

    Joined:
    Mar 4, 2014
    Posts:
    23
    Ok, i tried some coding and I got this.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public abstract class Hurtable : MonoBehaviour {
    5.  
    6.     private int max = 100;
    7.     private int current;
    8.     private bool hasZero;
    9.     private bool hasMax;
    10.     private bool setMaxAtStart = true;
    11.  
    12.     public virtual int Max { get{return max;} set{max = value;}}
    13.     public virtual int Current { get{return current;} set{current = value;}}
    14.     public virtual bool HasZero { get{return (Current <=0);}}
    15.     public virtual bool HasMax { get{return (Current >=Max);}}
    16.     public virtual bool SetMaxAtStart { get{return setMaxAtStart;} set{setMaxAtStart = value;}}
    17.  
    18.     public virtual void Heal(int amount){
    19.         Current += amount;
    20.         if (HasMax)
    21.             Current = Max;
    22.     }
    23.  
    24.     public virtual void TakeDamage(int amount){
    25.         Current -= amount;
    26.     }
    27.  
    28. }
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Health : Hurtable {
    5.  
    6.  
    7.     void Start(){
    8.         if (SetMaxAtStart)
    9.             Current = Max;
    10.     }
    11.  
    12. }
    13.  
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. [RequireComponent(typeof(Hurtable))]
    5. public abstract class Killable : MonoBehaviour {
    6.  
    7.     private float destroyDelay = 2f;
    8.     private bool isDead = true;
    9.     protected Health _health;
    10.  
    11.     public virtual float DestroyDelay { get{ return destroyDelay; } set{ destroyDelay = value; }}
    12.     public virtual bool IsDead { get{return isDead;} set{isDead = value;} }
    13.  
    14.     void Start(){
    15.         _health = GetComponent<Health>();
    16.     }
    17.  
    18.  
    19.     public virtual void CheckIfIsDead(){
    20.         IsDead = _health.HasZero;
    21.     }
    22.  
    23.     public virtual void Die(){
    24.         Destroy (gameObject, DestroyDelay);
    25.     }
    26.  
    27. }
    28.  
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4.  
    5. public class Death : Killable {
    6.  
    7.  
    8.     void Update(){
    9.         CheckIfIsDead ();
    10.         if (IsDead)
    11.             Die ();
    12.     }
    13.  
    14. }
    15.  
    Is this a sustainable way to keep going?

    Edit:

    Current plan:

    Apresentação1.jpg
     
    Last edited: Feb 28, 2015
  10. passerbycmc

    passerbycmc

    Joined:
    Feb 12, 2015
    Posts:
    1,741
    Why check each frame if dead? You can check if something got killed in your method that actually lowers the health while taking damage. Also if something has health I would automatically assume it is killable and hurtable
     
  11. honprovet

    honprovet

    Joined:
    Mar 4, 2014
    Posts:
    23
    I agree that checking each frame if is dead is bad. It was just a quick thing to test the idea. This post is to help me understand the coding architecture. The actual code is not the important part.

    Ok now that's a good thing.

    If it has Health, is IS A hurtable, and if it has Death it IS A killable.

    But if it is killable, it doesnt necessarily have health, it can have integrity or w/e.

    That's how i think the 2nd layer helped me.

    EDIT:

    Wow now i changed some stuff and ended up with HEALTH and DEATH classes both empty, only inheriting.

    All the basic code was in the ABSTRACT BASE CLASSES.

    I could use the 3rd layer only for the actual diferentiation of behaviours, using overrides when necessary.

    I like this idea....
     
    Last edited: Feb 28, 2015
  12. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Looks like you are on the right track. I would have built the dependencies the other way around. Hurtable requires Killable. I would also nix the Update function on Killable and have the Hurtable call Killable.Die(); This is all implementation details, its entirely up to you how to do it.
     
  13. honprovet

    honprovet

    Joined:
    Mar 4, 2014
    Posts:
    23
    Yeah i think it's starting to work. I got the player controlled movement to work, it is child of MovableInMesh. Since the parent has the basic, implementing the AI movement will be so easy, all i'll need to do is set the desired destination and THATS ALL.

    Thanks for your support.

    I'll work some days then update this a bit with the current situation.