28
\$\begingroup\$

In any collision, there are two GameObjects involved right? What I want to know is, How do I decide which object should contain my OnCollision*?

As an example, let's suppose I have a Player object and a Spike object. My first thought is to put a script on the player that contains some code like this:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Spike")) {
        Destroy(gameObject);
    }
}

Of course, the exact same functionality can be achieved by instead having a script on the Spike object that contains code like this:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Player")) {
        Destroy(coll.gameObject);
    }
}

Whilst both are valid, it made more sense to me to have the script on the Player because, in this case, when the collision happens, an action is being performed on the Player.

However, what's making me doubt this is that in future you may want to add more objects that will kill the Player on collision, such as an Enemy, Lava, Laser Beam, etc. These objects will likely have different tags. So then the script on the Player would become:

OnCollisionEnter(Collision coll) {
    GameObject other = coll.gameObject;
    if (other.compareTag("Spike") || other.compareTag("Lava") || other.compareTag("Enemy")) {
        Destroy(gameObject);
    }
}

Whereas, in the case where the script was on the Spike, all you would have to do is add that same script to all the other objects that can kill the Player and name the script something like KillPlayerOnContact.

Also, if you have a a collision between the Player and an Enemy, then you likely want to perform an action on both. So in that case, which object should handle the collision? Or should both handle the collision and perform different actions?

I have never built a game of any reasonable size before and I'm wondering if the code can become messy and difficult to maintain as it grows if you get this sort of thing wrong in the beginning. Or maybe all ways are valid and it doesn't really matter?


Any insight is much appreciated! Thank you for your time :)

\$\endgroup\$
  • \$\begingroup\$ First why the string literals for tags? An enum is much better because a typo will turn into a compile error instead of a hard to track down bug (why isn't my spike killing me?). \$\endgroup\$ – ratchet freak Aug 9 '16 at 11:32
  • \$\begingroup\$ Because I've been following Unity tutorials and that's what they did. So, would you just have a public enum called Tag that contains all your tag values? So it would be Tag.SPIKE instead? \$\endgroup\$ – Redtama Aug 9 '16 at 12:12
  • \$\begingroup\$ To get the best of both worlds, consider using a struct, with an enum inside, as well as static ToString and FromString methods \$\endgroup\$ – zcabjro Aug 10 '16 at 6:47
  • \$\begingroup\$ Very related: Zero behavior objects in OOP - my design dilemma & Eric Lippert's Wizards and Warriors series: ericlippert.com/2015/04/27/wizards-and-warriors-part-one \$\endgroup\$ – user89642 Aug 10 '16 at 16:36
48
\$\begingroup\$

I personally prefer to architect systems such that neither object involved in a collision handles it, and instead some collision-handling proxy gets to handle it with a callback like HandleCollisionBetween(GameObject A, GameObject B). That way I don't have to think about what logic should be where.

If that's not an option, such when you're not in control of the collision-response architecture, things get a bit fuzzy. Generally the answer is "it depends."

Since both objects will get collision callbacks, I'd put code in both, but only code that is relevant to that object's role in the game world, while also trying to maintain extensibility as much as possible. This means if some logic makes equal sense in either object, prefer to put it in the object that will likely lead to fewer code changes in the long run as new functionality is added.

Put another way, between any two object types that can collide, one of those object types generally has the same effect on more object types than the other. Putting code for that effect in the type that affects more other types means less maintenance in the long run.

For example, a player object and a bullet object. Arguably, "taking damage on collision with bullets" makes logical sense as part of the player. "Applying damage to the thing it hits" makes logical sense as part of the bullet. However, a bullet is likely to apply damage to more different types of objects than a player (in most games). A player won't suffer damage from terrain blocks or NPCs, but a bullet still might apply damage to those. So I'd prefer to put the collision handling for imparting damage into the bullet, in that case.

\$\endgroup\$
  • 1
    \$\begingroup\$ I found everyone's answers really helpful but have to accept yours for its clarity. Your last paragraph really sums it up for me :) Extremely helpful! Thank you so much! \$\endgroup\$ – Redtama Aug 9 '16 at 8:57
12
\$\begingroup\$

TL;DR The best place to put the collision detector is the place where you consider it is the active element in the collision, instead of the passive element in the collision, because perhaps you'll need additional behavior to be executed in such active logic (and, following good OOP guidelines like SOLID, is the responsibility of the active element).

Detailed:

Let's see ...

My approach when I have the same doubt, regardless the game engine, is to determine which behavior is active and which behavior is passive with respect to the action I want to trigger on collection.

Please note: I use different behaviors as abstractions of the different parts of our logic to define the damage and -as another example- the inventory. Both are separate behaviors I'd add later to a Player, and not clutter my custom Player with separate responsibilities which would be harder to maintain. Using annotations like RequireComponent, i'd ensure my Player behavior had, also, the behaviors I am defining now.

This is just my opinion: avoid executing logic depending on the tag, but instead use different behaviors and values like enums to choose among.

Imagine this behaviors:

  1. Behavior to specify an object as being a stack on the ground. RPG games use this for items in the ground able to be picked up.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
    
  2. Behavior to specify an object capable of producing damage. This could be lava, acid, any toxic substance, or a flying projectile.

    public class DamageDealer : MonoBehaviour {
        public Faction facton; //let's use it as well..
        public DamageType damageType = DamageType.BLUNT;
        public uint damage = 1;
    }
    

To this extent, consider DamageType and InventoryObject as enums.

These behaviors are not active, in the way that by being on the ground or flying around they do not act by themselves. They do nothing. E.g. if you have a fireball flying in an empty space, it is not ready to do damage (perhaps others behaviors should be considered active in a fireball, like the fireball consuming his power while travelling and diminishing its damage power in the process, but even when a potential FuelingOut behavior could be developed, it is stil another behavior, and not the same DamageProducer behavior), and if you see an object in the ground, it is not checking the whole users and thinking can they pick me?

We need active behaviors for that. The counterparts would be:

  1. One behavior to take damage (It would require a behavior to handle life and death, since lowering life and receiving damage are usually separate behaviors, and because I want to keep this examples as simple as possible) and perhaps resist damage.

    public class DamageReceiver : MonoBehaviour {
        public DamageType[] weakness;
        public DamageType[] halfResistance;
        public DamageType[] fullResistance;
        public Faction facton; //let's use it as well..
    
        private List<DamageDealer> dealers = new List<DamageDealer>(); 
    
        public void OnCollisionEnter(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && !this.dealers.Contains(dealer)) {
                this.dealers.Add(dealer);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && this.dealers.Contains(dealer)) {
                this.dealers.Remove(dealer);
            }
        }
    
        public void Update() {
            // actually, this should not run on EVERY iteration, but this is just an example
            foreach (DamageDealer element in this.dealers)
            {
                if (this.faction != element.faction) {
                    if (this.weakness.Contains(element)) {
                        this.SubtractLives(2 * element.damage);
                    } else if (this.halfResistance.Contains(element)) {
                        this.SubtractLives(0.5 * element.damage);
                    } else if (!this.fullResistance.Contains(element)) {
                        this.SubtractLives(element.damage);
                    }
                }
            }
        }
    
        private void SubtractLives(uint lives) {
            // implement it later
        }
    }
    

    This code is untested and should work as a concept. What's the purpose? Damage is something someone feels, and is relative to the object (or living form) being damaged, as you consider. This is an example: in regular RPG games, a sword damages you, while in other RPGs implementing it (e.g. Summon Night Swordcraft Story I and II), a sword can also receive damage by hitting a hard part or blocking armor, and could need repairs. So, since the damage logic is relative to the receiver and not to the sender, we only put relevant data in the sender, and the logic in the receiver.

  2. The same applies to an inventory holder (another required behavior would be one related to the object being on the ground, and the ability to remove it from there). Perhaps this is the appropriate behavior:

    public class Inventory : MonoBehaviour {
        private Dictionary<InventoryObject, uint> = new Dictionary<InventoryObject, uint>();
        private List<Treasure> pickables = new List<Treasure>();
    
        public void OnCollisionEnter(Collision col) {
            Treasure treasure = col.gameObject.GetComponent<Treasure>();
            if (treasure != null && !this.pickables.Contains(treasure)) {
                this.pickables.Add(treasure);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer treasure = col.gameObject.GetComponent<DamageDealer>();
            if (treasure != null && this.pickables.Contains(treasure)) {
                this.pickables.Remove(treasure);
            }
        }
    
        public void Update() {
            // when certain key is pressed, pick all the objects and add them to the inventory if you have a free slot for them. also remove them from the ground.
        }
    }
    
\$\endgroup\$
7
\$\begingroup\$

As you can see from the variety of answers here, there's a fair degree of personal style involved in these decisions, and judgement based on the needs of your particular game - no one absolute best approach.

My recommendation is to think of it in terms of how your approach will scale as you grow your game, adding and iterating on features. Will putting the collision handling logic in one place lead to more complex code later? Or does it support more modular behaviour?

So, you have spikes that damage the player. Ask yourself:

  • Are there things other than the player that spikes might want to damage?
  • Are there things other than spikes that the player should take damage from on collision?
  • Will every level with a player have spikes? Will every level with spikes have a player?
  • Might you have variations on the spikes that need to do different things? (eg. different damage amounts / damage types?)

Obviously, we don't want to get carried away with this and build an over-engineered system that can handle a million cases instead of just the one case we need. But if it's equal work either way (ie. put these 4 lines in class A vs class B) but one scales better, it's a good rule of thumb for making the decision.

For this example, in Unity specifically, I'd lean toward putting the damage-dealing logic in the spikes, in the form of something like a CollisionHazard component, which upon collision calls a damage-taking method in a Damageable component. Here's why:

  • You don't need to set aside a tag for every different collision participant you want to distinguish.
  • As you add more varieties of collidable interactions (eg. lava, bullets, springs, powerups...) your player class (or Damageable component) doesn't accumulate a giant set of if/else/switch cases to handle each one.
  • If half your levels end up having no spikes in them, you're not wasting time in the player collision handler checking whether a spike was involved. They only cost you when they're involved in the collision.
  • If you later decide that spikes should kill enemies & props too, you don't need to duplicate code out of a player-specific class, just stick the Damageable component on them (if you need some to be immune to spikes only, you can add damage types & let the Damageable component handle immunities)
  • If you're working on a team, the person who needs to change the spike behaviour and checks out the hazard class/assets isn't blocking everyone who needs to update interactions with the player. It's also intuitive to a new team member (especially level designers) which script they need to look at to change the spike behaviours - it's the one attached to the spikes!
\$\endgroup\$
  • \$\begingroup\$ Entity-Component system existe to avoid IFs like that. Those IFs are always wrong (just those ifs). Additionally if the spike is moving (or is a projectile), the collision handler will be triggered despite the colliding object is or not the player. \$\endgroup\$ – Luis Masuelli Aug 8 '16 at 23:53
2
\$\begingroup\$

It really doesn't matter who handles the collision, but be consistent with whose job it is.

In my games, I try to choose the GameObject that is "doing" something to the other object as the one who controls the collision. I.E. Spike damages the player so it handles the collision. When both objects "do" something to each other, have them each handle their action on the other object.

Also, I would suggest trying to design your collisions so that the collisions are handled by the object that reacts to collisions with fewer things. A spike will only need to know about collisions with the player, making the collision code in the Spike script nice and compact. The player object can collide with many more things which will make a bloated collision script.

Lastly, try to make your collision handlers more abstract. A Spike doesn't need to know that it collided with a player, it just needs to know that it collided with something that it needs to deal damage to. Lets say you have a script:

public abstract class DamageController
{
    public Faction faction; //GOOD or BAD
    public abstract void ApplyDamage(float damage);
}

You can subclass DamageController in your player GameObject to handle the damage, then in Spike's collision code

OnCollisionEnter(Collision coll) {
    DamageController controller = coll.gameObject.GetComponent<DamageController>();
    if(controller){
        if(controller.faction == Faction.GOOD){
          controller.ApplyDamage(spikeDamage);
        }
    }
}
\$\endgroup\$
2
\$\begingroup\$

I had the same Problem when I started out so here it goes: In this particular case use the Enemy. You usually want the Collision handled by the Object that performs the action (in this case - the enemy, but if your player had a weapon, then the weapon should handle the collision), because if you want to tweak attributes (for example the damage of the enemy) you definitely want to change it in the enemyscript and not in the playerscript (especially if you have multiple Players). Likewise if you want to change the damage of any weapon the Player uses you definitely don't want to change it in every enemy of your game.

\$\endgroup\$
2
\$\begingroup\$

Both objects would handle the collision.

Some objects would be immediately deformed, broken, or destroyed by the collision. (bullets, arrows, water balloons)

Others would just bounce off and roll to the side. (rocks) These could still take damage if the thing they hit was hard enough. Rocks don't take damage from a human hitting them, but they might from a sledge hammer.

Some squishy objects like humans would take damage.

Others would be bound to each other. (magnets)

Both collisions would be placed on an event handling queue for an actor acting on an object at a particular time and place (and velocity). Just remember that the handler should only deal with the what happens to the object. It is even possible for a handler to place another handler on the queue to handle additional effects of the collision based on the properties of the actor or the object being acted on. (Bomb blew up and the resulting explosion now has to test for collisions with other objects.)

When scripting, use tags with properties that would be translated into interface implementations by your code. Then reference the interfaces in your handling code.

For instance, a sword might have a tag for Melee which translates to an interface named Melee which might extend a DamageGiving interface which has properties for damage type and a damage amount defined using either a fixed value or ranges, etc. And a bomb might have an Explosive tag whose Explosive interface also extends the DamageGiving interface which has an additional property for radius and possibly how quickly the damage diffuses.

Your game engine would then code against the interfaces, not specific object types.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.