Search Unity

Hit and Damage System with interfaces

Discussion in 'Scripting' started by erebel55, May 27, 2015.

  1. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    I have been trying to use the Single Responsibility Principle, interfaces, and abstract classes to write code with good structure.

    The situation I am trying to code is as follows

    Objects can be hit and not damaged (ex. a wall is hit but not damaged).

    Objects can be hit and damaged (ex. player is hit and damaged).

    So I created two interfaces.
    Code (CSharp):
    1.     public interface IHittable
    2.     {
    3.         void Hit();
    4.     }
    5.  
    Code (CSharp):
    1.     public interface IDamageable
    2.     {
    3.         void Damage(int damageTaken);
    4.     }
    So, in the player's case he can be hit and damaged.

    Therefore, I thought a hit should drive the damage.

    Which led to the following implementations of my interfaces.

    Code (CSharp):
    1.     public class PlayerHit : MonoBehaviour, IHittable
    2.     {
    3.         IDamageable damageable;
    4.      
    5.         void Start()
    6.         {
    7.             damageable = (IDamageable)GetComponent(typeof(IDamageable));
    8.             if (damageable == null)
    9.             {
    10.                 throw new MissingComponentException("Requires an implementation of IDamageable")
    11.             }
    12.         }
    13.      
    14.         public void Hit()
    15.         {
    16.             damageable.Damage(/*how should I pass this in?*/);
    17.         }
    18.     }
    19.  
    Code (CSharp):
    1.     public class PlayerDamage : MonoBehaviour, IDamageable
    2.     {
    3.         Stats stats;
    4.      
    5.         void Start()
    6.         {
    7.             stats = GetComponent<Stats>();
    8.         }
    9.      
    10.         public void Damage(int damageTaken)
    11.         {
    12.             stats.health -= damageTaken;
    13.         }
    14.     }
    However, since everything that is hit isn't also damaged I didn't think it would be right to pass the damageTaken as an argument of Hit().

    Therefore, how should I pass damageTaken to Damage()?

    I thought about consolidating these two interfaces into one interface and only applying the damage if the object had health.

    But I was hoping there was a better way?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    ok, first off... you're going to have to throw out some OOP design structure ideas. Interfaces and abstract classes can have their place in a component based design world... but you design with COMPONENTS first, than those design patterns second.

    Next, lets talk about components. The component design pattern is an extension of the composite design pattern:
    http://en.wikipedia.org/wiki/Composite_pattern

    Your GameObject is a basic object that can have functionality composited to it by components.

    This you will build what I like to call 'entities' on. An entity can be a collection of GameObjects that should be considered as a whole. This collection of GameObjects can be sorted hierarchally down to some root gameobject and all its children. I often refer to this as the 'root'. I'll tag it as such to make finding the root easy. Now we can composite and sort components in the entity fairly easily and use methods like 'GetComponentsInChildren' to access components on both the root and in the children.

    Example:
    organized_entity.png
    Note how my player is made up of various gameobjects like CombatMotor and MobileMotor that organize out all my code.


    Now, lets get back to your signaling damage and the sort to various things.

    Interfaces
    One easy option here is we do have a 'IStrikable' interface, and an 'IStrikingForce' interface. IStrikable is something that wants to receive struck messages, and IStrikingForce is what is doing the striking. We can pass the IStrikingForce to the IStrikable on strike.

    Code (csharp):
    1.  
    2. public interface IStrikingForce
    3. {
    4.     float DesiredDamage { get; }
    5. }
    6.  
    7. public interface IStrikable
    8. {
    9.     void Strike(IStrikingForce force);
    10. }
    11.  
    In this case the striker when hitting something can find the 'root' of what was hit (look through parent list, find 'root' tagged gameobject... if none found assume that the thing hit is the root). Now search the entity for IStrikable components, call 'Strike' on all of them, and pass in the IStrikingForce that struck it.

    The receiver can now deal with it accordingly. If it wants to take damage, it can. It can scale that damage. You can include other information with the IStrikingForce object, like the collider hit, or the collider that did the striking... treat it like an EventArgs or something.


    Messaging System

    So another option is to use a messaging system. Mine is a type safe system I call 'Notification'.

    Any notification I want to send inherits from a 'Notification' class. These act as EventArgs. An entity may dispatch a notification on some event.
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Notification.cs

    There is a another class called Signal that inherits from Notification.
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Signal.cs

    These represents a notification being sent TO an entity (a weapon to a struck entity).

    A script can register to listen for some notification/signal by type on a specific component, a gameobject, or the root of an entity (to receive all notifications for that entity).

    You could have a special StrikeNotification:

    Code (csharp):
    1.  
    2. public class StrikeNotification : Signal
    3. {
    4.     public float DesiredDamge {get; set;}
    5. }
    6.  
    Then your weapon on strike can just dispatch this notification to it:

    Code (csharp):
    1.  
    2. //... on collision
    3. var root = hitCollider.FindRoot();
    4. Notification.PostNotification<StrikeNotification>(root, new StrikeNotification() { DesiredDamage = 5f });
    5.  
    Then just like the interface system, only those scripts that want to receive this signal do so. This removes the need for coupling 2 interfaces together, and instead decouples the sender and receiver data types so that it's just a StrikeNotification that is sent out.

    Receivers can register for them at will (and unregister at will).
     
    Last edited: May 27, 2015
    eisenpony and erebel55 like this.
  3. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    You don't literally use a GameObject.tag of "root" right? There is Transform.root for that http://docs.unity3d.com/ScriptReference/Transform-root.html
     
    Last edited: May 27, 2015
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Transform.root gives the top most transform in that hierarchy.

    What if my entity is inside another GameObject?

    Say I have a sword (an entity) that is inside the prop bone of my Player (another entity) that is inside some other GameObject that maybe scales or transforms all its children for some measure (like how some people implement moving platforms by placing the player inside a GameObject for the platform).
     
    erebel55 likes this.
  5. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    That makes sense, thank you.

    Your Notification.cs is quite a bit more heavyweight than I am hoping for (however, thank you for sharing). I definitely have a lot to learn when it comes to messaging systems.

    Is there a typo in your interface code? You define the IStrikable interface twice.

    I had also thought about doing something like that. Where I had IHitter and IHittable.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    yeah, one of those is supposed to be IStrikingForce... edited and fixed.
     
  7. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    Okay here is the best I could do using the concept of an "agent" and "patient" model. http://en.wikipedia.org/wiki/Agent_(grammar)

    IHitter (agent) hits IHittable (patient)

    Code (CSharp):
    1. public interface IHitter
    2. {
    3.     int hitDamage { get; set; }
    4. }
    5.  
    Code (CSharp):
    1. public interface IHittable
    2. {
    3.     void Hit(IHitter hitter);
    4. }
    Code (CSharp):
    1. public class RaycastAttack : RangedAttack, IHitter
    2. {
    3.     public int hitDamage
    4.     {
    5.         get
    6.         {
    7.             return damage;
    8.         }
    9.         set
    10.         {
    11.             damage = value;
    12.         }
    13.     }
    14.    
    15.     public override void Shoot()
    16.     {
    17.         RaycastHit hit;
    18.         if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hit, range, hitMask))
    19.         {
    20.             ActivateHittables.HitAll(hit.collider.gameObject, (IHitter)this);
    21.         }
    22.     }
    23. }
    24.  
    Code (CSharp):
    1. public class ActivateHittables
    2. {
    3.     // Hit all of the hittables attached to a game object
    4.     public static void HitAll(GameObject gameObject, IHitter hitter)
    5.     {
    6.         // Get the hittable components
    7.         Component[] hittables = gameObject.GetComponents(typeof(IHittable));
    8.  
    9.         if (hittables == null)
    10.         {
    11.             return;
    12.         }
    13.  
    14.         // Hit all of the hittables
    15.         foreach (IHittable hittable in hittables)
    16.         {
    17.             hittable.Hit(hitter);
    18.         }
    19.     }
    20. }
    Code (CSharp):
    1. public abstract class RangedAttack : MonoBehaviour, IShootable
    2. {
    3.     public float range = 500f;
    4.     public int damage = 10;
    5.    
    6.     public abstract void Shoot();
    7.    
    8.     void Update()
    9.     {
    10.         if (CrossPlatformInputManager.GetButton("Fire1"))
    11.         {
    12.             Shoot();
    13.         }
    14.     }
    15. }
    16.  
    I cut out everything that didn't pertain to this subject.

    What do you think? Any improvements?
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    looks good to me

    only concern is if you're not on Unity 5, GetComponents doesn't work with interfaces. But if you're in Unity 5, it does.
     
  9. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    Yes, I'm in Unity 5. Also, I was debating on whether to make my RaycastAttack or RangedAttack class implement the IHitter.

    Also, what is the advantage of having IHitter as an interface?
     
    Last edited: May 27, 2015
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Personally I'd allow the specific hitter that will be call 'HitAll' to decide.

    You may for some reason want the "IHitter" to be a token object. Say a small object that you pass in in case the information isn't the same every time you call HitAll.

    Lets say you have a Bow that is the hitter and it's damage is determined by the pull strength of the shot.

    Code (csharp):
    1.  
    2. public class BowAttack : RangedAttack
    3. {
    4.  
    5.     public float pullStrength;
    6.  
    7.     public override void Shoot()
    8.     {
    9.         RaycastHit hit;
    10.         if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hit, range, hitMask))
    11.         {
    12.             ActivateHittables.HitAll(hit.collider.gameObject, new BowHit() { hitDamage = this.damage * pullStrength );
    13.         }
    14.     }
    15.    
    16.     public class BowHit : IHitter
    17.     {
    18.         public int hitDamage {get;set;}
    19.     }
    20.    
    21. }
    22.  
     
    erebel55 likes this.
  11. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    I like this a lot, thank you lordofduct :)
     
  12. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    One more thing, would it be better to have IHitter like I have or combine it with IShootable?

    By the way you examples help me understand :)
     
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I wouldn't combine them.

    You might have an IShootable that doesn't hit, and a IHittable that doesn't shoot.

    Examples:

    A zero damage 'nerf' weapon that doesn't 'hit', but does shoot.

    A sword hits, but doesn't get shot.
     
    erebel55 likes this.
  14. erebel55

    erebel55

    Joined:
    Jun 14, 2014
    Posts:
    43
    Thank you kind sir :D