Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Interact with inheriting class methods from "casted" base class (collisions, etc)

Discussion in 'Scripting' started by kendallroth, Dec 15, 2018.

  1. kendallroth

    kendallroth

    Joined:
    Nov 4, 2015
    Posts:
    20
    I want to implement a damage system that works across both living items (Entity) and environment items (Environment). The key components of this is that I need to be able to trigger something to take damage from another script, be it a collision with a projectile or a blast radius trigger, etc.

    The two approaches I see involve interfaces or inheritance (or maybe a mix). While the inheritance appears necessary to copy the similar code between all items from a base class, it does lead to some issues where only the parent class method is called (defined in collision example). Although interfaces would solve this, I would also wind up duplicating the common code that inheritance helps with.

    Obviously this issue is much deeper than a damage system, but it was the best example I could think of that illustrated this problem.

    TL;DR: I want to create a system of classes/interfaces that enable the following:
    Collision (Problem)

    This underlying issue (calling specific subclass methods through cast parent class/interface) is most noticeable in the Projectile collision script. As noted in the script comments, I understand that since the Entity class is what implements the IDamageable interface, it is what is returned.

    I'm also assuming that if I moved the IDamageable interface to each item that inherited from Entity it would then be called on the subclasses? But in that case I would have to implement the method separately each time, even if there was duplicate code. At the same time, that would enable some entities to take damage differently than others (ie. if wearing armour, etc)...

    Code (CSharp):
    1.  
    2. public void Projectile : MonoBehaviour {
    3.     private float damage = 5f;
    4.  
    5.     private void OnCollisionEnter(Collision collider) {
    6.         // Check if on layer
    7.  
    8.         // This properly returns the colliding object, but it currently returns the base
    9.         //   class, "Entity" for example (makes sense).
    10.         IDamageable damageableObject = collider.gameObject.GetComponent<IDamageable>();
    11.         if (damageableObject != null) {
    12.             // Now I totally get why this is calling the "TakeHit" method of "Entity", but I
    13.             //    am searching for a way to call the "TakeHit" of the "Enemy" itself.
    14.             //    Is this only possible by checking the type? If so, isn't that losing the point
    15.             //    of the interface?
    16.             damageableObject.TakeHit(damage, collider);
    17.         }
    18.     }
    19. }
    20.  
    Code

    Both entities and environment items can be damageable (ie. entity death or environment destruction), which (to me) requires an interface to make the projectile collision (and other) system easier.

    Should the interface be applied to just Entity and Environment, or to each subclass?

    Code (CSharp):
    1.  
    2. public interface IDamageable {
    3.     void TakeHit(float damage, Collision collider);
    4.  
    5.     // Future method that only deals damage (no collision)
    6.     //void TakeDamage(float damage);
    7. }
    8.  
    Entities all have health (and some other common attributes), as well as a shared but extendable (through inheritance) death mechanic.

    How do I properly handle the relationship between taking damage and dying, if one is an interface and the other an overridable base class method?

    Code (CSharp):
    1.  
    2. public class Entity : MonoBehaviour, IDamageable {
    3.     public float Health = 10f;
    4.     public bool IsAlive = true;
    5.  
    6.     // Implement IDamageable (this is what gets called if using interface, but I want child class method to be called)
    7.     // Unsure if it should be common method, what if children may check for armor,
    8.     //    etc, to reduce damage taken?
    9.     public void TakeHit(float damage, Collision collider) {
    10.         Health -= damage;
    11.  
    12.         // It's nice having a shared "base" die method, as I don't have to replicate this
    13.         //    in each class that implements the interface (and then not sure where "Die"
    14.         //    method would be either, probably on interface which means more extending)
    15.         if (Health <= 0 && IsAlive) {
    16.               Die();
    17.         }
    18.     }
    19.  
    20.     // All entities should set IsAlive to false, which is why it is here
    21.     protected virtual void Die() {
    22.          IsAlive = false;
    23.  
    24.         // Child objects should handle destroying themselves (particle effects, etc).
    25.         //Destroy(gameObject);
    26.     }
    27. }
    28.  
    Enemies are basically Entity with pathfinding and simple attacks, but also a more customized death.

    Code (CSharp):
    1.  
    2. public class Enemy : Entity {
    3.     // Death should play particle effects, sound, and destroy object
    4.     protected override Die() {
    5.         base.Die();
    6.  
    7.         // Do particle effects and sound
    8.         // Destroy game object
    9.     }
    10. }
    11.  
    Player is obviously way more complicated than a regular Entity, but also has a more customized death.

    Code (CSharp):
    1.  
    2. public class Player : Entity {
    3.     // Death should end the game, play sound, and destroy object
    4.     protected override Die() {
    5.         base.Die();
    6.  
    7.         // Play sound
    8.         // End game
    9.         // Destroy game object
    10.     }
    11. }
    12.  
    Please note, I am struggling with how to represent this best, let me know if it needs clarity in an area.
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,117
    I'm not sure how helpful this will be, but I have two similar use-cases in my game, and I've addressed it in two different ways. Examples of those use cases are:
    1. Depending on what kind of surface the player is walking on, different footstep sounds should play. For example, normal footsteps, walking on broken glass, walking in puddles of water, etc.
    2. Objects interact with other nearby object, such as an explosion potentially triggering other nearby objects to be destroyed or detonated themselves.
    In neither case have I resorted to interfaces, as I think it's slightly annoying to find instances of interfaces. In fact, in your code, I didn't even think that the following worked:

    Code (CSharp):
    1. collider.gameObject.GetComponent<IDamageable>();
    I thought instead you'd need to do something like

    Code (CSharp):
    1. collider.gameObject.GetComponents<MonoBehaviour>().OfType<IDamageable>().First();
    Anyway, I don't find interfaces too be very elegant in Unity projects. Here's how I addressed the two issues:
    1. For footsteps, and some similar applications, I just use tags. When I raycast and get a collider, I can check whether its tag is one of several, and do something accordingly. TagCompare seems faster to me than calling GetComponent, so this seems pretty good for cases where you're checking something every frame. The downside is ending up with a lot of tags you have to manage, but I organize this in Constant classes.
    2. For this, I use a caching approach. For example, I have objects that can explode, and when they do, other nearby explodable objects should also explode. It's pretty slow to get all objects in the scene of a given type, so instead I add a "tracking" component to all explodable objects. This tracker registers with a singleton tracking manager on Awake. Now I can just ask the tracking manager for all the explodable things it's tracking, and see which are near the explosion. I do this with a lot of objects depending on their behavior. It's much faster than finding components or game objects in the scene, especially for cases where I'm doing something to a set of objects every frame or every physics frame.
    Anyway, I don't mean to discourage interface use. It has its places. But I've always found them cumbersome in Unity.
     
  3. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Honestly, I cannot follow your concern completely.

    Reading this part though:

    it seems you're missing the fact that the interface will just be a normal method of the implementing type. The interface is just a contract that enforces its declared members will be available on any object that's assignable to the interface type.

    The rest is just up to that implementing class. If the implementing class is meant to work polymorphic when this method is called, you can mark the interface's method virtual or abstract when implementing it in the base class, so that derived classes can override it.

    You can as well implement the interface and internally call whatever you want, be it a polymorphic member or just a normal member.
     
  4. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    We solved this with composition in our game

    Any entity that can receive force from a weapon adds a component. On awake each instance adds themself to a lookup were the transform is the key. Both scanline ballistics and actual physica ballistics can then lookup these upp and then do the proper thing wich most often is AddForce or AddExplosiveForce. For example a players dead body will get effected by a handgrende going off nearby or being shot.

    We also have a hitzone component which handles logical damage (health reduction). These are two different system but the ballistics system is querying both
     
  5. kendallroth

    kendallroth

    Joined:
    Nov 4, 2015
    Posts:
    20
    Two of the comments above mentioned composition (dgoyette.1147741 and andersmalmgren.680298). Someone else also mentioned this to me on another forum, so it definitely seems like the approach I may try taking.

    suddoha.461337 The thing I was noticing is that since I was asking for the IDamageable interface (implemented by the Entity), the return type from the collision was an Entity. Although the Enemy class did override the virtual method declared in Entity, it wasn't called because the script was operating on an Entity.
     
  6. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Composition is definitely an important topic to read about and to take into consideration. It's often preferred to inheritance, because you're able to build small, reusable and easily testable components which are then used to compose more complex objects.

    The "return type" was definitely IDamageable if you used GetComponent<IDamageable>(). The actual instance is not of any interest for the caller, and if it was an actual Enemy, it would behave polymorphic when you call Die in the TakeHit, even though TakeHit is only implemented in the Entity class.

    With regards to the code you posted:
    When you use GetComponent<IDamageable>(), and there's an Enemy (which is an Entity in your current architecture) attached to the GO that you called it on, you'd receive an instance of Enemy as an IDamageable.
    You implemented TakeHit as a normal method, but you call the virtual method 'Die()' which you have already overriden in the Enemy class. This should work just fine, if it wasn't working like that, inheritance would be broken completely.