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()
.