Base class with knowledge of Derived? (Trigger warning: contains C++)



  • So I have a Collidable interface for various physics objects which has a single method, onContact(Collidable *other) which gets called whenever one Collidable hits another.

    Projectile implements Collidable, and is different from other Collidables in that different things happen based on what the other Collidable is. For example, if it hits a Player, that player needs to take damage, but if it hits a Building, it simply deactivates.

    Presently, the body of Collidable::onContact() looks something like this:

    Player *player = dynamic_cast<Player *>(other);
    Building *building = dynamic_cast<Building *>(other);
    Projectile *proj = dynamic_cast<Projectile *>(other);
    
    // Is it a Player?
    if(player != nullptr)
    {
      // deal damage to player, deactivate and reset projectile
    }
    // Or a Building?
    else if(building != nullptr)
    {
      // deactivate and reset projectile, until we make buildings destructible or something...
    }
    // Or another Projectile?
    else if(proj != nullptr)
    {
      // test whether this projectile and the other projectile should receive collisions from each other
      // for example, flamethrower fire should not stop bullets, etc.
    }
    
    

    Now, while this gets the job done, it's not very OOP. I was considering adding a virtual method to Collidable such as onProjectileContact(Projectile *other), in which each of the other Collidables specifies how they should respond to a Projectile contact. The problem is that each if block above requires manipulating Projectile's internals to lookup information about the type of projectile, reset it to an un-fired state, etc. such that it'd require a few new methods to be accessed externally, and extracting each if block to an onProjectileContact(Projectile *other) method in each other Collidable just pushes that complexity outside of the class without reducing it (and perhaps increasing it given the supporting methods that would have to be added to Projectile).

    Is there a better way?



  • You could have the projectile have some code for when it collides with a player and the player have code for when it collides with a projectile. Each one only needs to know about itself and the other's public interface. Player takes damage from the projectile, projectile resets and deactivates itself.



  • @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    You could have the projectile have some code for when it collides with a player and the player have code for when it collides with a projectile. Each one only needs to know about itself and the other's public interface. Player takes damage from the projectile, projectile resets and deactivates itself.

    That's plausible as I call a->onContact(b) as well as b->onContact(a) in my collision dispatcher. Most of what happens at this stage is Projectile-centric, so I'm not sure how much could be extracted, but there is some common ground between the if blocks that could possibly be condensed.


  • BINNED

    Not entirely about your question, but if this is C++ why you have so many raw pointers?



  • How about instead of checking for the type of object that the projectile have collided with, use tags and check against them? You might be able to build a hash table of actions that has the various tags as the keys so that onCollide will only run the respective actions for the tags. The hash table of actions can be further serialized or deserialized to some form of data outside the source code to make it easier to introduce new behaviours.
    That was how I sort of implemented my game AI system, so I shove the responsibility of designing AI behaviors to game designers.



  • @dse said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Not entirely about your question, but if this is C++ why you have so many raw pointers?

    Because I didn't know about shared_ptr in 2010!

    Probably about a third of the application has since been migrated to using shared_ptr. To migrate the rest would involve re-evaluating the ownership semantics of a lot of classes that get used everywhere. I'm not terribly concerned about memory leaks right now as just about everything is created by factories that keep track of the objects they have created.

    It would probably cost me a few days' work to do the switchover (this application is only ~50kLOC), so it's not a huge deal in the grand scheme of things, but there's a ton of other things I'm working on at the moment. Thus it sits on the back burner, along with upgrading my VS solution and rebuilding my dependencies for VS2015, and a bunch of other housekeeping tasks.


  • Trolleybus Mechanic

    @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Each one only needs to know about itself and the other's public interface. Player takes damage from the projectile, projectile resets and deactivates itself.

    See, it's design decisions like this that end up ruining everything in the long run. It starts with "oh, it'll be so OOP to let the player decide itself how to handle a bullet hitting it", and it ends up with some dumb motherfucker in a trenchcoat stopping bullets with his mind and killing Agents.

    Way to destroy everything, Ben.



  • @Lorne-Kates said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Each one only needs to know about itself and the other's public interface. Player takes damage from the projectile, projectile resets and deactivates itself.

    See, it's design decisions like this that end up ruining everything in the long run. It starts with "oh, it'll be so OOP to let the player decide itself how to handle a bullet hitting it", and it ends up with some dumb motherfucker in a trenchcoat stopping bullets with his mind and killing Agents.

    Way to destroy everything, Ben.

    I hope you're not letting your players run arbitrary C++ on the game server.



  • @WPT said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    How about instead of checking for the type of object that the projectile have collided with, use tags and check against them? You might be able to build a hash table of actions that has the various tags as the keys so that onCollide will only run the respective actions for the tags. The hash table of actions can be further serialized or deserialized to some form of data outside the source code to make it easier to introduce new behaviours.
    That was how I sort of implemented my game AI system, so I shove the responsibility of designing AI behaviors to game designers.

    How would you access the tags for a given colliding pair? Implement Collidable::getTags() or something? If there was a matrix for every possible pairing, something like that could work, and it would also appease those Unity guys who favor composition over inheritance.



  • @Groaner
    Yeah. I simply would ignore the collision event if the tag cannot be found in the hashtable. Can't be bothered to come up with a default behaviour/reaction.



  • @WPT said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Groaner
    Yeah. I simply would ignore the collision event if the tag cannot be found in the hashtable. Can't be bothered to come up with a default behaviour/reaction.

    Ah, gotcha. I'd think that something like:

    void Collidable::getTags(CollisionTagList &list)
    {
     list.clear();
    }
    

    would probably suffice for a sensible default. Or, populate the list with items that the majority of the subclasses use.



  • @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Lorne-Kates said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Each one only needs to know about itself and the other's public interface. Player takes damage from the projectile, projectile resets and deactivates itself.

    See, it's design decisions like this that end up ruining everything in the long run. It starts with "oh, it'll be so OOP to let the player decide itself how to handle a bullet hitting it", and it ends up with some dumb motherfucker in a trenchcoat stopping bullets with his mind and killing Agents.

    Way to destroy everything, Ben.

    I hope you're not letting your players run arbitrary C++ on the game server.

    The only control players have is the ability to send RPCs to the server to request movement, chat messages, etc. As long as there aren't any zero-days in RakNet, and as long as the network serialization layer is watertight to buffer overflows and DoS, things ought to be pretty solid.

    Yeah, I'm pretty sick of games where the client can say, "My position is (x, y, z)!" to the server and the server goes, "Sure, sounds about right!"


  • BINNED

    @WPT said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    The hash table of actions can be further serialized or deserialized to some form of data outside the source code to make it easier to introduce new behaviours.

    Inner-platform effect?

    @Groaner said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Thus it sits on the back burner, along with upgrading my VS solution and rebuilding my dependencies for VS2015, and a bunch of other housekeeping tasks.

    CMake can make VS solutions just fine

    @Groaner said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Unity guys who favor composition over inheritance.

    Composition and message passing, loose coupling.


  • Trolleybus Mechanic

    @ben_lubar said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    I hope you're not letting your players run arbitrary C++ on the game server.

    Is this a :whoosh: or have kids these days literally never heard of the classics?


    Filed under: Whoa



  • @Groaner Eric Lippert had a series of blog posts about this sort of problem in C#. Might be worth a read just to see a few alternative perspectives.



  • @clatter

    Interesting article.
    Sadly he does not go on to creating a rule system in c#.
    I have been exploring a similar problem around my own project as i hone my OOP skills, and found this:
    http://www.richardlord.net/blog/what-is-an-entity-framework


  • Discourse touched me in a no-no place

    @Helix said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    Sadly he does not go on to creating a rule system in c#.

    It's not really an OOP problem, but rather a rules engine problem. Since the solution is to stop torturing the type system with the complexity required to solve things, it's no longer so interesting for the author.


  • area_pol

    @Groaner I work on a similar system, my solution is:

    Collision detection is based first on channels - channels would be Player Projectile Building and each object would specify which of them it belongs to and with which of them it collides.
    In that case, the bullets would collide with projectile channel but flamethrowers would not. So when 2 bullets meet they collide but if fire meets another projectile it does not.

    The logic can also be mostly made using polymorphism:
    Projectile: on collision call other->receiveDamage( ... ) and destroy itself
    Player::receiveDamage: gets damaged
    Building::receiveDamage and Projectile::receiveDamage: nothing



  • @dkf
    Yes, I see that. But I am lazy and want everything done for me.



  • @Adynathos
    Sounds like Tags as described above



  • @Groaner Instead of checking for the real identity of the Collidable *other, I would have the Collidable class specify all the possible externally visible behaviour of colliding objects. That way, the Collidable::onContact() function just runs through every possible interaction, most of which are null actions. This way the most generic behaviour stays in the base class and only the specialized behaviour is in the derived classes. For a made up example (not too dissimilar to what @Adynathos suggested with the polymorphism):

    class Collidable
    {
        void onContact(Collidable *other) // not virtual
        {
            // When two entities collide, run through all interactions without casting
    
            other->takeDamage(this->dealDamage());
    
            if(other->isSolidBarrier())
            {
                this->stopMotion();
            }
    
            // etc.
        }
    
        virtual int dealDamage()
        {
            // most entities don't damage players
            return 0;
        }
    
        virtual void takeDamage(int healthHit)
        {
            // most things don't have health, so do nothing
        }
    
        virtual bool isSolidBarrier()
        {
            // Most things are solid and so stop projectiles and other motion
            return true;
        }
    
        virtual void stopMotion()
        {
            // Nothing special happens for most things after a collision
        }  
    } 
    
    class Player : public Collidable
    {
        void takeDamage(int healthHit)
        {
            this->hitPoints -= healthHit;
            if(this->hitPoints < 0)
            {
                die();
            }
        }
    }
    
    class Projectile : public Collidable
    {
        int dealDamage()
        {
            return this->damage();
        }
    
        void stopMotion()
        {
            // deactivate and reset projectile
        }
    }
    
    class FlameThrowerFlame : public Projectile
    {
        bool isSolidBarrier()
        {
            return false; // allow projectiles to pass through
        }
    }
    
    class Building : public Collidable
    {
        void takeDamage(int healthHit)
        {
            if(healthHit > 0)
            {
                explode(); // implement for environmental damage
            }
        }
    }
    


  • @clatter said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Groaner Eric Lippert had a series of blog posts about this sort of problem in C#. Might be worth a read just to see a few alternative perspectives.

    I've heard of approaches like this. While useful for more well-defined and rigid behavior, my Projectile implementation is heavily data-driven, so that everything from sword slashes to fireballs to flamethrower fire can be modeled as single Projectiles, so a compile-time type-based solution would be too restrictive.



  • @Helix said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @clatter

    Interesting article.
    Sadly he does not go on to creating a rule system in c#.
    I have been exploring a similar problem around my own project as i hone my OOP skills, and found this:
    http://www.richardlord.net/blog/what-is-an-entity-framework

    Interesting. I've managed to unwittingly implement most of this already, up until the section on "Abandoning good object-oriented practice."



  • @Adynathos said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Groaner I work on a similar system, my solution is:

    Collision detection is based first on channels - channels would be Player Projectile Building and each object would specify which of them it belongs to and with which of them it collides.
    In that case, the bullets would collide with projectile channel but flamethrowers would not. So when 2 bullets meet they collide but if fire meets another projectile it does not.

    Yeah, my physics library (Bullet) does something similar by giving each body a bitmask, and two bodies collide only if an AND returns non-zero. I had considered leveraging that (and already do to meet other requirements), but that seems better suited to implementing universal rules.

    The logic can also be mostly made using polymorphism:
    Projectile: on collision call other->receiveDamage( ... ) and destroy itself
    Player::receiveDamage: gets damaged
    Building::receiveDamage and Projectile::receiveDamage: nothing

    One tricky aspect is that the damage/effects that get dealt are properties of both the colliding objects. For example, a fireball knows what level it is and can thus calculate its own raw damage, but it needs to take the victim's fire resistance into account to calculate actual damage (which isn't just a number, sometimes the Projectile is applying a buff/debuff or DoT, for example. I have ProjectileEffect to take care of that).

    I may be able to separate out much of the collision logic into the respective classes, but these shared interactions make it tricky. We'll see.

    Side note: if you're a one-man team, don't make an RPG unless you're a glutton for punishment and enjoy solving an endless stream of problems like this one.


  • area_pol

    @Groaner said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    fireball knows what level it is and can thus calculate its own raw damage, but it needs to take the victim's fire resistance into account to calculate actual damage (which isn't just a number, sometimes the Projectile is applying a buff/debuff or DoT

    receiveDamage's argument should be an object which contains:

    • source (projectile or its caster)
    • amount
    • types (like fire) if you have resistances
    • list of buffs

    or even a finer grained list of effects, like [Damage(amount=..., type=fire), StatusEffect(effect_class, duration), ...]

    Then it is up to the receiving object to apply this to itself - for example a player would take damage and buffs, but a rock would discard the buffs.



  • @MZH said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Groaner Instead of checking for the real identity of the Collidable *other, I would have the Collidable class specify all the possible externally visible behaviour of colliding objects. That way, the Collidable::onContact() function just runs through every possible interaction, most of which are null actions. This way the most generic behaviour stays in the base class and only the specialized behaviour is in the derived classes. For a made up example (not too dissimilar to what @Adynathos suggested with the polymorphism):

    Yeah, I'm beginning to think the superior solution is going to involve moving more functionality into the base class rather than leave it a nearly-empty interface.



  • @Adynathos said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    @Groaner said in Base class with knowledge of Derived? (Trigger warning: contains C++):

    fireball knows what level it is and can thus calculate its own raw damage, but it needs to take the victim's fire resistance into account to calculate actual damage (which isn't just a number, sometimes the Projectile is applying a buff/debuff or DoT

    receiveDamage's argument should be an object which contains:

    • source (projectile or its caster)
    • amount
    • types (like fire) if you have resistances
    • list of buffs

    or even a finer grained list of effects, like [Damage(amount=..., type=fire), StatusEffect(effect_class, duration), ...]

    Then it is up to the receiving object to apply this to itself - for example a player would take damage and buffs, but a rock would discard the buffs.

    Right, this is all encapsulated in ProjectileEffect and added to a list inside the victim (if the victim is a Player).



  • Readers familiar with type theory will know that the highfalutin name for the problem is that we’re in violation of the Liskov Substitution Principle.

    The problem as stated is that C# isn't expressive enough for dependent types...


Log in to reply