Here's my attempt at reimplementing part of https://diep.io/, a 2D game where tanks battle with each other. The tanks are circular and they have cannons which fire bullets. The bullets can hit other tanks and they disappear after three seconds. Here's a random YouTube video if you want to see how the original game works: https://www.youtube.com/watch?v=9R6zsD5rdd8.
I'm using Box2D for the physics and SFML for graphics and input. Currently, I have only implemented basic tanks and bullets, but I want to make sure the overall structure is good before continuing so that I don't spend a bunch of time refactoring later. I plan on implementing health and damage in the future but I'm not asking for help on those.
arena.fwd.h
:
#ifndef CPPDIEP_ARENA_FWD_H
#define CPPDIEP_ARENA_FWD_H
/// @file
/// Forward declaration for Arena used to avoid circular dependencies.
namespace cppdiep {
class Arena;
} // namespace cppdiep
#endif // CPPDIEP_ARENA_FWD_H
arena.h
:
#ifndef CPPDIEP_ARENA_H
#define CPPDIEP_ARENA_H
#include "arena.fwd.h"
#include <concepts>
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/Color.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include "bullet.h"
#include "tank.h"
#include "time.h"
namespace cppdiep {
/// The Arena class manages all of the objects in the game.
class Arena {
public:
/// Construct an arena.
/// @param size the side length of the arena.
/// @param time_step the number of seconds that each time step represents.
Arena(float size, float time_step);
/// Draw the arena to an SFML render target.
/// @param target the SFML render target to draw to.
void draw(sf::RenderTarget &target) const;
/// Advance the state of the arena by one time step.
void step();
/// Spawn a non-tank object in the arena.
/// @tparam ObjectType the type of the object to spawn
/// @param args the arguments to be forwarded to the object's constructor
/// @return A reference to the new object.
// clang-format and doxygen doen't handle the requires expression properly.
// clang-format off
template <std::derived_from<Object> ObjectType, typename... Args>
/// @cond
requires(!std::derived_from<ObjectType, Tank>)
/// @endcond
ObjectType &spawnObject(Args &&...args) {
ObjectType *object = new ObjectType(*this, std::forward<Args>(args)...);
objects.emplace_back(object);
return *object;
}
// clang-format on
/// Spawn a tank in the arena.
/// @tparam TankType the type of the tank to spawn
/// @param args the arguments to be forwarded to the tank's constructor
/// @return A reference to the new tank.
template <std::derived_from<Tank> TankType, typename... Args>
TankType &spawnObject(Args &&...args) {
TankType *tank = new TankType(*this, std::forward<Args>(args)...);
tanks.emplace_back(tank);
return *tank;
}
/// Get the time step size.
/// @return The time step size.
float getTimeStep() const { return time_step; }
/// Get the current time.
/// @return The current time in steps.
Time getTime() const { return time; }
private:
friend Object;
/// Alias for the type of the smart pointers used to store the polymorphic
/// objects.
/// @tparam ObjectType the type of the object that the pointer points to.
template <std::derived_from<Object> ObjectType>
using ObjectPtr = std::unique_ptr<ObjectType, typename ObjectType::Deleter>;
/// Alias for the type of the container used to store objects.
/// @tparam ObjectType the type of the objects stored in the container.
template <std::derived_from<Object> ObjectType>
using ObjectContainer = std::vector<ObjectPtr<ObjectType>>;
/// Get the arena's Box2D world.
/// @return A reference to the arena's Box2D world.
b2World &getB2World() { return b2_world; }
/// @copydoc getB2World()
const b2World &getB2World() const { return b2_world; }
/// The Box2D world of the arena. The gravity vector is zero since the world
/// is horizontal. This has to be destructed after all of the objects have
/// been destructed since the destructors of the objects will access the world
/// to destroy their bodies.
b2World b2_world{b2Vec2(0.f, 0.f)};
/// Container of all of the objects in the arena except for tanks.
ObjectContainer<Object> objects;
/// Container of all of the tanks in the arena. Tank barrels can overlap with
/// other objects, so they have to be kept separately and drawn in a
/// consistent order after other objects.
ObjectContainer<Tank> tanks;
/// The number of seconds in each time step.
const float time_step;
/// The current time as the number of time steps since the arena was created.
Time time = 0;
};
} // namespace cppdiep
#endif // CPPDIEP_ARENA_H
arena.cpp
:
#include "arena.h"
#include <array>
#include <concepts>
#include <memory>
#include <utility>
#include <vector>
#include <Box2D/Collision/Shapes/b2ChainShape.h>
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2Fixture.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/RenderTarget.hpp>
#include "box2d_categories.h"
#include "bullet.h"
#include "render_utils.h"
#include "tank.h"
#include "time.h"
namespace cppdiep {
Arena::Arena(float size, float time_step) : time_step(time_step) {
b2BodyDef border_body_def;
b2Body &border_body = *b2_world.CreateBody(&border_body_def);
std::array border_vertices = {
b2Vec2(size / 2.f, size / 2.f), b2Vec2(-size / 2.f, size / 2.f),
b2Vec2(-size / 2.f, -size / 2.f), b2Vec2(size / 2.f, -size / 2.f)};
b2ChainShape border_chain;
border_chain.CreateLoop(border_vertices.data(), border_vertices.size());
b2FixtureDef border_fixture_def;
border_fixture_def.shape = &border_chain;
border_fixture_def.friction = 0.25f;
border_fixture_def.restitution = 0.25f;
border_fixture_def.filter.categoryBits = box2d_categories::BORDER;
border_fixture_def.filter.maskBits = box2d_categories::TANK;
border_body.CreateFixture(&border_fixture_def);
}
void Arena::draw(sf::RenderTarget &target) const {
target.clear(colors::BACKGROUND);
// Bullets are drawn first so that they are underneath the tank barrels.
for (const ObjectPtr<Object> &object : objects) {
object->draw(target);
}
for (const ObjectPtr<Tank> &tank : tanks) {
tank->draw(target);
}
}
void Arena::step() {
// Replace objects that need to be destroyed with objects moved from the end
// of the vector. Iterating in reverse simplifies things since we don't have
// to worry about skipping over objects when removing an object.
auto new_end = objects.end();
for (auto it = objects.rbegin(); it != objects.rend(); ++it) {
if ((*it)->step()) {
*it = std::move(*--new_end);
}
}
objects.erase(new_end, objects.end());
// The tanks have to be rendered in a consistent order because their barrels
// may overlap with other tanks.
std::erase_if(tanks,
[](const ObjectPtr<Tank> &tank) { return tank->step(); });
b2_world.Step(time_step, 8, 3);
++time;
}
} // namespace cppdiep
object.h
:
#ifndef CPPDIEP_OBJECT_H
#define CPPDIEP_OBJECT_H
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.fwd.h"
namespace cppdiep {
/// A game object, such as a tank, a bullet, or a polygon.
class Object {
public:
Object(const Object &) = delete;
/// Get the current position of the object.
/// @return The current position of the object.
b2Vec2 getPosition() const { return getB2Body().GetPosition(); }
/// Get the current velocity of the object.
/// @return The current velocity of the object.
b2Vec2 getVelocity() const { return getB2Body().GetLinearVelocity(); }
/// Draw the object to an SFML render target.
/// @param target the SFML render target to draw to.
virtual void draw(sf::RenderTarget &target) const = 0;
protected:
/// Construct an object.
/// @param arena the arena that contains the object. The object will keep a
/// reference to the arena so the
/// @param b2_body_def the Box2D body definition that will be used to create
/// the object's body.
Object(Arena &arena, const b2BodyDef &b2_body_def);
/// Destruct an object.
virtual ~Object();
/// Advance the state of the object by one time step and return whether the
/// object should be destroyed now.
/// @return Whether the object should be destroyed now.
virtual bool step() {
// Health and damage haven't been implemented yet so this just returns
// false.
return false;
}
/// Get a reference to the arena that contains the object.
/// @return A reference to the arena that contains the object.
Arena &getArena() const { return arena; }
/// Get a reference to the Box2D body of the object.
/// @return A reference to the Box2D body of the object.
b2Body &getB2Body() { return b2_body; }
/// @copydoc getB2Body()
const b2Body &getB2Body() const { return b2_body; }
private:
friend Arena;
/// A deleter that the arena passes to the smart pointer. This is necessary
/// since the destructor is not public.
struct Deleter {
void operator()(Object *object) const { delete object; }
};
/// The arena that contains the object.
Arena &arena;
/// The Box2D body of the object.
b2Body &b2_body;
};
} // namespace cppdiep
#endif // CPPDIEP_OBJECT_H
object.cpp
:
#include "object.h"
#include "arena.h"
namespace cppdiep {
Object::Object(Arena &arena, const b2BodyDef &b2_body_def)
: arena(arena), b2_body(*arena.getB2World().CreateBody(&b2_body_def)) {}
Object::~Object() { arena.getB2World().DestroyBody(&b2_body); }
} // namespace cppdiep
tank.h
:
#ifndef CPPDIEP_TANK_H
#define CPPDIEP_TANK_H
#include <Box2D/Collision/Shapes/b2Shape.h>
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2Fixture.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/Color.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.fwd.h"
#include "object.h"
#include "render_utils.h"
namespace cppdiep {
/// A generic tank.
class Tank : public Object {
public:
/// Get the radius of the tank body.
/// @return the radius of the tank body.
float getRadius() const {
return getB2Body().GetFixtureList()->GetShape()->m_radius;
}
/// Get the current target position of the tank.
/// @return the target position of the tank relative to the tank's position.
virtual b2Vec2 getTarget() const = 0;
/// Get the direction of the tank's target relative to the tank.
/// @return The direction of the tank's target as an angle in radians.
float getTargetAngle() const {
b2Vec2 target = getTarget();
return std::atan2(target.y, target.x);
}
/// Get the color of the tank.
/// @return The color of the tank.
sf::Color getColor() const { return color; }
void draw(sf::RenderTarget &target) const override;
protected:
/// Construct a Tank.
/// @param arena the arena that contains the tank.
/// @param position the initial position of the tank.
/// @param radius the radius of the tank's body.
/// @param color the color of the tank.
Tank(Arena &arena, const b2Vec2 &position, float radius,
const sf::Color &color);
/// Helper function for drawing cannons.
/// @param target the SFML render target to draw to.
/// @param length the length of the barrel. The barrel starts from the center
/// of the tank.
/// @param width the width of the barrel.
/// @param angle the angle that the cannon is pointing towards in radians.
void drawCannon(sf::RenderTarget &target, float length, float width,
float angle) const;
/// Apply a force to move the tank.
/// @param vec the direction and speed to move in. A magnitude of 1 represents
/// full speed.
void move(const b2Vec2 &vec) {
getB2Body().ApplyForceToCenter(getMoveForce() * vec, true);
}
/// Fire the tank's cannon(s).
virtual void fire() = 0;
private:
friend Arena;
/// Draw the tank's cannon(s).
/// @param target the SFML render target to draw to.
virtual void drawCannons(sf::RenderTarget &target) const = 0;
/// Get the magnitude of the force used to move the tank.
/// @param the magnitude of the force used to move the tank.
virtual float getMoveForce() const = 0;
/// The color of the tank.
sf::Color color;
};
} // namespace cppdiep
#endif // CPPDIEP_TANK_H
tank.cpp
:
#include "tank.h"
#include <Box2D/Collision/Shapes/b2CircleShape.h>
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2Fixture.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/CircleShape.hpp>
#include <SFML/Graphics/Color.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.h"
#include "box2d_categories.h"
#include "render_utils.h"
namespace cppdiep {
namespace {
b2BodyDef makeB2BodyDef(const b2Vec2 &position) {
b2BodyDef body_def;
body_def.type = b2_dynamicBody;
body_def.position = position;
body_def.fixedRotation = true;
body_def.linearDamping = 1.f;
return body_def;
}
} // namespace
Tank::Tank(Arena &arena, const b2Vec2 &position, float radius,
const sf::Color &color)
: Object(arena, makeB2BodyDef(position)), color(color) {
b2CircleShape body_shape;
body_shape.m_radius = radius;
b2FixtureDef fixture_def;
fixture_def.shape = &body_shape;
fixture_def.density = 1.f;
fixture_def.friction = 0.3f;
fixture_def.restitution = 0.25f;
fixture_def.filter.categoryBits = box2d_categories::TANK;
fixture_def.filter.maskBits = box2d_categories::TANK |
box2d_categories::BORDER |
box2d_categories::BULLET;
getB2Body().CreateFixture(&fixture_def);
}
void Tank::draw(sf::RenderTarget &target) const {
drawCannons(target);
drawCircle(target, getPosition(), getRadius(), getColor());
}
void Tank::drawCannon(sf::RenderTarget &target, float length, float width,
float angle) const {
sf::RectangleShape cannon_shape(sf::Vector2f(length, width));
cannon_shape.setOrigin(0.f, width / 2.f);
cannon_shape.setPosition(convertVector(getPosition()));
cannon_shape.setFillColor(colors::CANNON);
cannon_shape.setOutlineThickness(OUTLINE_THICKNESS);
cannon_shape.setOutlineColor(darken(colors::CANNON));
cannon_shape.setRotation(radiansToDegrees(angle));
target.draw(cannon_shape);
}
} // namespace cppdiep
basic_tank.h
:
#ifndef CPPDIEP_BASIC_TANK_H
#define CPPDIEP_BASIC_TANK_H
#include <SFML/Graphics/RenderTarget.hpp>
#include "tank.h"
namespace cppdiep {
/// A tank with a single cannon.
class BasicTank : public Tank {
protected:
using Tank::Tank;
void fire() override;
private:
void drawCannons(sf::RenderTarget &target) const override;
float getMoveForce() const override { return 15.f * getRadius(); }
};
} // namespace cppdiep
#endif // CPPDIEP_BASIC_TANK_H
basic_tank.cpp
:
#include "basic_tank.h"
#include <cmath>
#include <Box2D/Common/b2Math.h>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.h"
#include "bullet.h"
namespace cppdiep {
void BasicTank::drawCannons(sf::RenderTarget &target) const {
float radius = getRadius();
drawCannon(target, 2 * radius, radius, getTargetAngle());
}
void BasicTank::fire() {
b2Vec2 target_vec = getTarget();
target_vec.Normalize();
float bullet_radius = getRadius() / 2.f;
float impulse_magnitude = 10.f * getRadius();
// The bullet is spawned in the barrel just outside of the tank body to avoid
// teleportation due to the bullet intersecting the tank body. This causes
// some teleportation if the spawned bullet intersects another object. In the
// future, collisions between a bullet and the tank that fired it will be
// disabled and the bullet will be spawned inside the tank body.
getArena().spawnObject<Bullet>(
getPosition() + (getRadius() + bullet_radius) * target_vec, getVelocity(),
impulse_magnitude * target_vec, bullet_radius, getColor());
// Simulate recoil by applying the same impulse in the opposite direction to
// the tank.
getB2Body().ApplyLinearImpulse(-impulse_magnitude * target_vec,
getB2Body().GetWorldCenter(), true);
}
} // namespace cppdiep
external_control_tank.h
:
#ifndef CPPDIEP_EXTERNAL_CONTROL_TANK_H
#define CPPDIEP_EXTERNAL_CONTROL_TANK_H
#include <concepts>
#include <Box2D/Common/b2Math.h>
#include "tank.h"
namespace cppdiep {
/// A tank controlled externally using the move() and fire() methods.
/// @tparam BaseTank the type of the tank to be controlled externally.
template <std::derived_from<Tank> BaseTank>
class ExternalControlTank final : public BaseTank {
public:
using BaseTank::BaseTank;
/// Apply a force to move the tank in the direction of the given vector.
/// @param vec a vector indicating the direction and speed to move in. A
/// magnitude of 1 indicates full speed.
void move(const b2Vec2 &vec) { BaseTank::move(vec); }
/// @copydoc Tank::getTarget()
b2Vec2 getTarget() const override { return target; }
/// Set the target point of the tank. This is the point that the tank will aim
/// towards.
/// @param target the target point relative to the position of the tank.
void setTarget(const b2Vec2 &target) { this->target = target; }
/// Fire the tank's cannon(s).
void fire() { BaseTank::fire(); }
private:
/// The current target point of the tank.
b2Vec2 target{0.f, 0.f};
};
} // namespace cppdiep
#endif // CPPDIEP_EXTERNAL_CONTROL_TANK_H
bullet.h
:
#ifndef CPPDIEP_BULLET_H
#define CPPDIEP_BULLET_H
#include <Box2D/Common/b2Math.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2Fixture.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.fwd.h"
#include "object.h"
#include "render_utils.h"
#include "time.h"
namespace cppdiep {
/// A bullet fired from a cannon. Bullets disappear after three seconds.
class Bullet : public Object {
public:
/// Get the radius of the bullet.
/// @return The radius of the bullet.
float getRadius() const {
return getB2Body().GetFixtureList()->GetShape()->m_radius;
}
void draw(sf::RenderTarget &target) const override;
protected:
/// Construct a bullet.
/// @param arena the arena that contains the bullet.
/// @param position the initial position of the bullet.
/// @param velocity the initial velocity of the bullet.
/// @param impulse the impulse applied to the bullet on top of the initial
/// velocity.
/// @param radius the radius of the bullet.
/// @param color the color of the bullet.
Bullet(Arena &arena, const b2Vec2 &position, const b2Vec2 &velocity,
const b2Vec2 &impulse, float radius, const sf::Color &color);
bool step() override;
private:
friend Arena;
/// The color of the bullet.
sf::Color color;
/// The time when the bullet should be destroyed.
Time destroy_time;
};
} // namespace cppdiep
#endif // CPPDIEP_BULLET_H
bullet.cpp
:
#include "bullet.h"
#include <Box2D/Collision/Shapes/b2CircleShape.h>
#include <Box2D/Dynamics/b2Body.h>
#include <Box2D/Dynamics/b2World.h>
#include <SFML/Graphics/RenderTarget.hpp>
#include "arena.h"
#include "box2d_categories.h"
#include "render_utils.h"
namespace cppdiep {
namespace {
b2BodyDef makeB2BodyDef(const b2Vec2 &position, const b2Vec2 &velocity) {
b2BodyDef body_def;
body_def.type = b2_dynamicBody;
body_def.position = position;
body_def.linearVelocity = velocity;
body_def.linearDamping = 0.5f;
body_def.angularDamping = 0.5f;
body_def.bullet = true;
return body_def;
}
} // namespace
Bullet::Bullet(Arena &arena, const b2Vec2 &position, const b2Vec2 &velocity,
const b2Vec2 &impulse, float radius, const sf::Color &color)
: Object(arena, makeB2BodyDef(position, velocity)), color(color),
destroy_time(arena.getTime() + 3.f / arena.getTimeStep()) {
b2CircleShape body_shape;
body_shape.m_radius = radius;
b2FixtureDef fixture_def;
fixture_def.shape = &body_shape;
fixture_def.density = 1.f;
fixture_def.friction = 0.3f;
fixture_def.restitution = 0.25f;
fixture_def.filter.categoryBits = box2d_categories::BULLET;
fixture_def.filter.maskBits =
box2d_categories::BULLET | box2d_categories::TANK;
getB2Body().CreateFixture(&fixture_def);
getB2Body().ApplyLinearImpulse(impulse, getB2Body().GetWorldCenter(), true);
}
void Bullet::draw(sf::RenderTarget &target) const {
drawCircle(target, getPosition(), getRadius(), color);
}
bool Bullet::step() {
if (Object::step()) {
return true;
}
return getArena().getTime() >= destroy_time;
}
} // namespace cppdiep
box2d_categories.h
:
#ifndef CPPDIEP_B2_CATEGORIES_H
#define CPPDIEP_B2_CATEGORIES_H
#include <Box2D/Common/b2Settings.h>
namespace cppdiep {
/// Box2D collision category bitmasks.
namespace box2d_categories {
/// Box2D collision category bitmask for the arena border.
inline constexpr uint16 BORDER = 1u << 0;
/// Box2D collision category bitmask for tanks.
inline constexpr uint16 TANK = 1u << 1;
/// Box2D collision category bitmask for bullets.
inline constexpr uint16 BULLET = 1u << 2;
} // namespace box2d_categories
} // namespace cppdiep
#endif // CPPDIEP_B2_CATEGORIES_H
render_utils.h
:
#ifndef CPPDIEP_RENDER_UTILS_H
#define CPPDIEP_RENDER_UTILS_H
/// @file
/// Constants and helper functions used for rendering.
#include <numbers>
#include <Box2D/Common/b2Math.h>
#include <SFML/Graphics/CircleShape.hpp>
#include <SFML/Graphics/Color.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include <SFML/System/Vector2.hpp>
namespace cppdiep {
/// Colors used for rendering.
namespace colors {
/// Color for blue tanks and bullets.
inline const sf::Color BLUE(0, 178, 225);
/// Color for red tanks and bullets.
inline const sf::Color RED(241, 78, 84);
/// Color for tank cannons.
inline const sf::Color CANNON(153, 153, 153);
/// Background color of the arena.
inline const sf::Color BACKGROUND(205, 205, 205);
} // namespace colors
/// The thickness of the outlines around the objects. This is negative to make
/// the outline inside the edge of the object.
inline constexpr float OUTLINE_THICKNESS = -0.125f;
/// Darken a color to get the color of the outline.
/// @param color the color to darken.
/// @return The darkened color.
inline sf::Color darken(const sf::Color &color) {
return sf::Color(color.r * 0.75, color.g * 0.75, color.b * 0.75, color.a);
}
/// Draw a circle.
/// @param target the SFML render target to draw to.
/// @param position the position of the center of the circle.
/// @param radius the radius of the circle.
/// @param color the color of the circle.
inline void drawCircle(sf::RenderTarget &target, const b2Vec2 &position,
float radius, const sf::Color &color) {
sf::CircleShape shape(radius);
shape.setOrigin(radius, radius);
shape.setPosition(position.x, position.y);
shape.setFillColor(color);
shape.setOutlineThickness(OUTLINE_THICKNESS);
shape.setOutlineColor(darken(color));
target.draw(shape);
}
/// Convert radians to degrees.
/// @param radians an angle in radians.
/// @return The angle in degrees.
inline float radiansToDegrees(float radians) {
return radians * 180.f / std::numbers::pi_v<float>;
}
/// Convert a Box2D vector to an SFML vector.
/// @param b2_vec a Box2D vector.
/// @return The SFML vector.
inline sf::Vector2f convertVector(const b2Vec2 &b2_vec) {
return sf::Vector2f(b2_vec.x, b2_vec.y);
}
/// Convert an SFML vector to a Box2D vector.
/// @param sf_vec an SFML vector.
/// @return The Box2D vector.
inline b2Vec2 convertVector(const sf::Vector2f &sf_vec) {
return b2Vec2(sf_vec.x, sf_vec.y);
}
} // namespace cppdiep
#endif // CPPDIEP_RENDER_UTILS_H
time.h
:
#ifndef CPPDIEP_TIME_H
#define CPPDIEP_TIME_H
#include <cstdint>
namespace cppdiep {
/// The signed integer type that will be used to represent time in the arena as
/// a number of steps.
using Time = std::int64_t;
} // namespace cppdiep
#endif // CPPDIEP_TIME_H
main.cpp
:
#include <Box2D/Common/b2Math.h>
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/Window/ContextSettings.hpp>
#include <SFML/Window/Event.hpp>
#include <SFML/Window/Keyboard.hpp>
#include <SFML/Window/Mouse.hpp>
#include "arena.h"
#include "basic_tank.h"
#include "external_control_tank.h"
#include "render_utils.h"
int main() {
// Set up the window.
sf::ContextSettings settings;
settings.antialiasingLevel = 4;
sf::RenderWindow window(sf::VideoMode(800, 800), "CppDiep",
sf::Style::Titlebar | sf::Style::Close, settings);
constexpr int frame_rate = 60;
window.setFramerateLimit(frame_rate);
constexpr float arena_size = 20.f;
// The Y size of the view is negative to flip things vertically since SFML
// uses a downwards vertical axis while the arena uses an upwards vertical
// axis.
sf::View view(sf::Vector2f(0.f, 0.f), sf::Vector2f(arena_size, -arena_size));
window.setView(view);
// Create the arena and spawn two tanks for testing.
cppdiep::Arena arena(arena_size, 1.f / frame_rate);
auto &tank =
arena.spawnObject<cppdiep::ExternalControlTank<cppdiep::BasicTank>>(
b2Vec2(0.f, 0.f), 1.f, cppdiep::colors::BLUE);
arena.spawnObject<cppdiep::ExternalControlTank<cppdiep::BasicTank>>(
b2Vec2(0.f, 5.f), 1.f, cppdiep::colors::RED);
while (window.isOpen()) {
// Make the tank cannon point towards the mouse.
b2Vec2 mouse_position = cppdiep::convertVector(
window.mapPixelToCoords(sf::Mouse::getPosition(window)));
tank.setTarget(mouse_position - tank.getPosition());
// Process events.
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed) {
window.close();
} else if (event.type == sf::Event::MouseButtonPressed &&
event.mouseButton.button == sf::Mouse::Left) {
tank.fire();
}
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) {
tank.move(b2Vec2(0.f, 1.f));
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) {
tank.move(b2Vec2(-1.f, 0.f));
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) {
tank.move(b2Vec2(0.f, -1.f));
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) {
tank.move(b2Vec2(1.f, 0.f));
}
arena.step();
arena.draw(window);
window.display();
}
}
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.18)
project(
CppDiep
DESCRIPTION "Diep.io reimplemented in C++ with SFML and Box2D"
LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(MSVC)
add_compile_options(/W4 /WX)
else()
add_compile_options(-Wall -Wextra -pedantic -Werror)
endif()
find_package(
SFML 2.5
COMPONENTS graphics
REQUIRED)
find_package(Box2D REQUIRED)
add_executable(cppdiep arena.cpp basic_tank.cpp bullet.cpp main.cpp object.cpp
tank.cpp)
target_link_libraries(cppdiep PRIVATE sfml-graphics Box2D)
Github link: https://github.com/bkrl/cppdiep/tree/72222654a22320b2372a306894047e59ecace9a8
When you run the code, it should create a window with a blue tank and a red tank. You can move the blue tank with the WASD keys and aim with the mouse. Left-clicking will fire a bullet. Bullets collide with tanks but should pass through the border.