4
\$\begingroup\$

I'm trying to make the transition from the 'understand how standalone features work' phase to writing.. more 'complex' code that could actually see use in real life, so I've been trying find some neat, simple project that uses both inheritance and pointer related features (the things I've been struggling with the most). I came up with what I imagine an extremely basic 2D platformer game would use as its entity class-architecture, and well, it works, but I have no idea if the way I went about it is really good.

So basically I have an Entity type that has 2 derived classes, Player and Enemy. Each Entity also has a collision box of type Shape, which can either be a Rectangle or a Circle (both derived classes of Shape). The main goal of the project was to be able to call a function (collides()) from Entity that can detect collision with any other Entity of any shaped collision box.

Since pasting each file would definitely make the post too long, I've decided to not include Circle/Enemy, since they're practically the same as Rectangle/Player, here's the rest though:

Entity.h

#pragma once
#include "Shape.h"
#include <memory>

class Entity
{
    public:
        Entity();
        Entity(const double& weight, const double& x, const double& y,
               const double& width, const double& height);
        Entity(const double& weight, const double& x, const double& y, const double& radius);

        virtual void printLine() = 0;
        bool collides(Entity* other);

        inline Shape* getCollisionBox() 
        {
            return m_collisionBox.get();
        }

        inline double getWeight() const
        {
            return m_weight;
        }
    protected:
        double m_weight;
        std::unique_ptr<Shape> m_collisionBox;
};

Entity.cpp

#include "Entity.h"
#include "Rectangle.h"
#include "Circle.h"

Entity::Entity():
    m_weight(0)
{ }

Entity::Entity(const double& weight, const double& x, const double& y,
    const double& width, const double& height) : //Rectangle-shaped entity
    m_weight(weight),
    m_collisionBox(std::make_unique<Rectangle>(Rectangle(x, y, width, height)))
{ }

Entity::Entity(const double& weight, const double& x, const double& y, const double& radius) : 
    m_weight(weight), //Circle-shaped entity
    m_collisionBox(std::make_unique<Circle>(Circle(x, y, radius)))
{ }

bool Entity::collides(Entity* other)
{
    return m_collisionBox->collides(*other->getCollisionBox());
}

Player.h

#pragma once
#include "Entity.h"
#include <string>

class Player: public Entity
{
    public:
        Player();
        Player(const double& weight, std::string name, const double& x, const double& y,
               const double& width, const double& height);

        void printLine() override;

        inline std::string getName() const
        {
            return m_name;
        }
    private:
        std::string m_name;
};

Player.cpp

#include "Player.h"
#include <iostream>

Player::Player():
    Entity(0, 0, 0, 0, 0)
{ }

Player::Player(const double& weight, std::string name, const double& x, const double& y,
    const double& width, const double& height):
    Entity(weight, x, y, width, height),
    m_name(name)
{ }

void Player::printLine() 
{
    std::cout << "Well met." << '\n';
}

Shape.h

#pragma once

enum class ShapeType
{
    RECTANGLE,
    CIRCLE,
    NONE
};

class Shape
{
    public:
        Shape();
        Shape(const ShapeType& type);

        virtual bool collides(const Shape& other) = 0;

        inline ShapeType getType() const
        {
            return m_type;
        }
    protected:
        ShapeType m_type;
};

Shape.cpp

#include "Shape.h"

Shape::Shape():
    m_type(ShapeType::NONE)
{ }


Shape::Shape(const ShapeType& type):
    m_type(type)
{ }

Rectangle.h

#pragma once
#include "Shape.h"

class Rectangle: public Shape
{
    public:
        Rectangle();
        Rectangle(const double& x, const double& y, const double& width, const double& height);
        bool Rectangle::collides(const Shape& other) override;

        inline double getLeft() const
        {
            return m_x;
        }

        inline double getRight() const
        {
            return m_x + m_width;
        }

        inline double getTop() const
        {
            return m_y;
        }

        inline double getBottom() const
        {
            return m_y + m_height;
        }
    private:
        double m_x, m_y, m_width, m_height;
};

Rectangle.cpp

#include "Rectangle.h"
#include "Circle.h"
#include <algorithm>

Rectangle::Rectangle():
    Shape(ShapeType::RECTANGLE),
    m_x(0), m_y(0), m_width(0), m_height(0)
{ }


Rectangle::Rectangle(const double& x, const double& y, const double& width, const double& height):
    Shape(ShapeType::RECTANGLE),
    m_x(x), m_y(y), m_width(width), m_height(height)
{ }

bool Rectangle::collides(const Shape& other)
{
    switch (other.getType())
    {
        case ShapeType::RECTANGLE:
        {
            auto rec = static_cast<const Rectangle&>(other);
            return (getLeft() <= rec.getRight() && getRight() >= rec.getLeft() &&
                getTop() <= rec.getBottom() && getBottom() >= rec.getTop());
        }
        case ShapeType::CIRCLE:
        {
            auto circle = static_cast<const Circle&>(other);
            auto clamp = [](const double& val, const double& min, const double& max)
            {
                return val < min ? min : (max < val ? max : val);
            };
            double nearestX = clamp(circle.getX(), getLeft(), getRight());
            double nearestY = clamp(circle.getY(), getTop(), getBottom());
            double dX = circle.getX() - nearestX;
            double dY = circle.getY() - nearestY;
            double distanceSquared = (dX * dX) + (dY * dY);
            return distanceSquared <= (circle.getRadius() * circle.getRadius());
        }
        default:
        {
            return false;
        }
    }
}

So I guess the things that I'm most curious about are how I handled the whole passing m_collisionBox around, downcasting it in collides(), the use of the enum class and inlining some functions (is it really worth doing if it's just a compiler suggestion?), but any criticism/observations are welcome. I thought most functionality is kinda trivial, so I didn't bother with comments (definitely would've otherwise). One thing that I probably should comment on is the use of the enum class, its basically just a way for Rectangle and Circle to identify what type the Shape parameter is in collides().

\$\endgroup\$
3
\$\begingroup\$

There is a somewhat-advanced technique called double dispatch that is useful for resolving situations where you have more than one unknown (virtual) type.

In this case, you are using shared_ptr<Shape> in your Entity so you will be blind as to the true types on either side of a collision.

A visitor-pattern approach in C++ can be used, which works like this:

  1. Implement a blind collides(const Entity& other) const method. This method doesn't know anything about the other entity, but it does know it's own type:

    bool Circle::  // <-- NOTE: Circle 
    collides(const Entity & other) const 
    { 
        return other.collides_with_circle(*this); // <-- NOTE: circle 
    }
    
    bool Square::  // <-- NOTE: Square 
    collides(const Entity & other) const 
    {
        return other.collides_with_square(*this);  // <-- NOTE: square 
    }
    

    These functions don't "do anything." But they tell the other what their type is, by calling different methods. So when checking for a generic "collides", they respond by saying "You don't know what I am, and I don't know what you are, but I'm a circle. So I'll call your with_circle method and tell you that."

  2. Implement two methods on each class that handle colliding with the particular other class:

    bool Circle::
    collides_with_circle(...) {...}
    
    bool Circle::
    collides_with_square(...) {...}
    
    bool Square::
    collides_with_circle(...) {...}
    
    bool Square::
    collides_with_square(...) {...}
    

Note that if your collisions are symmetric, you might want to delegate one more time to a single class. That is, square-with-circle might just return other.collides_with_square so as to only have to maintain the cross-shape collision code in a single class.

Note also that you can use overload forms and casts to get the same result, defining collides(Circle&) and collides(Square&), then calling collides(static_cast<Circle>(...)) instead of collides_with_circle(). But it's actually easier just to type out the name than to type out the cast (which I'm fairly certain was a factor in the names chosen for casting operations)...

\$\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.