c++oopaggregationcompositiondesign-decisions

How can my class be provided some information about its environment?


Here's a code sample I wrote with encapsulation and composition in mind:

class Bullet {
 private:
  Vector2 position;
  Vector2 speed;

 public:
  void move(float time_delta) {
    position += speed * time_delta;
  }
};

Basically, there's just a projectile moving in nowhere. However, a bullet can actually e. g. ricochet off a wall, having its speed changed significantly. Is there a good way of considering such interactions? I neither want my Bullet to know about "higher-rank" classes (which are supposed to use it themselves) nor write a single-use solution like this one:

template<typename F> void move(float time_delta, F collision_checker);

UPDATE: worth reading if you want this question narrowed. Here's a simplified example of the wished logic for moving Bullets (I don't exactly mean the Bullet::move() member function!) and their interactions with other entities:

Vector2 destination = bullet.position + bullet.speed * time_delta;
if (std::optional<Creature> target = get_first_creature(bullet.position, destination)) {
  // decrease Bullet::speed depending on the target (and calculate the damage)
} else if (std::optional<Wall> obstacle = get_first_wall(bullet.position, destination)) {
  // calculate the ricochet changing Bullet::position and Bullet::speed
}

All pieces of code represented by comments are supposed to use some properties of the Creature and Wall classes.


Solution

  • From a design point of view, it is probably best if your bullet doesn't know how to detect when it's ... passing_through an obstacle (scnr). So it might be better to turn your Bullet class in to a struct, i.e. have it behave like a thing that is acted upon instead of a thing that acts.

    You can still add your convenience function but have it be non-mutating:

    struct Bullet {
      Vector2 position;
      Vector2 speed;
      Vector2 move(float time_delta) const {
        return position + speed * time_delta;
      }
    };
    

    This way you can compute the collisions from the calling scope:

    auto dest = bullet.move(dt);
    while (std::optional<Collision> const col = detectCollision(bullet.position,dest)) {
      bullet.position = col->intersectPoint;
      bullet.speed = col->reflectedSpeed;
      dest = col->reflectDest;
    }
    bullet.position = dest;
    

    Here detectCollision checks whether the line from the bullet's current position to the new position dest intersects with any obstacle and computes the parameters of the reflection. Effectively you zig-zag your way to the destination that will result from all successive ping-pongs of the bullet with potential obstacles.