\$\begingroup\$
\$\endgroup\$
Based on the previous question, I have implemented all suggestions. Also, I worked a bit in this project to improve it.
Summary of improvements:
- split the code into multiple files as .hpp and .cpp
- removed STL functions
- used only one
sf::RectangleShape
for border instead ofstd::vector
.
I would like to know how I can improve it further.
Utility.hpp
#pragma once
#include <random>
namespace utility
{
using DistType = std::uniform_real_distribution<float>;
auto randomEngine();
float random(DistType& dist);
float radian(std::size_t direction);
float degree(std::size_t direction);
}
Utility.cpp
#include "Utility.hpp"
#include <array>
#include <utility>
#include <algorithm>
#include <functional>
auto utility::randomEngine()
{
std::array<std::mt19937::result_type, std::mt19937::state_size> seed_data;
thread_local std::random_device source;
std::generate(std::begin(seed_data), std::end(seed_data), std::ref(source));
std::seed_seq seeds(std::begin(seed_data), std::end(seed_data));
thread_local std::mt19937 seeded_engine(seeds);
return seeded_engine;
}
float utility::random(DistType& dist)
{
thread_local auto RandomEngine = randomEngine();
return dist(RandomEngine);
}
float utility::radian(std::size_t direction)
{
constexpr static auto Ratio = 0.0174532925f; //pi/180
constexpr static std::array<float, 4> Radians
{
180 * Ratio,
0,
270 * Ratio,
90 * Ratio
};
return Radians[direction];
}
float utility::degree(std::size_t direction)
{
constexpr static std::array<float, 4> Degrees
{
180,
0,
270,
90
};
return Degrees[direction];
}
Snake.hpp
#pragma once
#include <SFML/System/Time.hpp>
#include <SFML/Graphics/Text.hpp>
#include <SFML/System/NonCopyable.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
#include <vector>
namespace sf
{
class CircleShape;
}
class Snake : public sf::Drawable, sf::NonCopyable
{
public:
enum Direction
{
Up,
Down,
Left,
Right
};
private:
using SnakeContainer = std::vector<sf::RectangleShape>;
public:
explicit Snake(const sf::Font& font, float windowWidth);
void reset();
void setDirection(Direction direction);
float getBlockSize() const;
bool hasLost() const;
void lose();
void extend();
void update(sf::Time dt);
sf::FloatRect getGlobalBounds() const;
bool hasCollideWithFruit(const sf::CircleShape& shape) const;
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
void checkCollision();
void move(sf::Time dt);
void shrink(std::size_t index);
private:
SnakeContainer mSnakeBody;
Direction mDirection;
std::size_t mLives;
sf::Text mLivesText;
float mBlockSize;
bool mIsFirstRun;
};
Snake.cpp
#include "Snake.hpp"
#include "Utility.hpp"
#include <SFML/Graphics/CircleShape.hpp>
#include <SFML/Graphics/RenderWindow.hpp>
using namespace utility;
Snake::Snake(const sf::Font& font, float windowWidth) : mBlockSize(16)
{
reset();
constexpr auto Offset = 140.f;
mLivesText.setFont(font);
mLivesText.setStyle(sf::Text::Bold);
mLivesText.setCharacterSize(30);
mLivesText.setColor(sf::Color::White);
mLivesText.setPosition(windowWidth - Offset, 0.f);
}
void Snake::reset()
{
mSnakeBody.clear();
static sf::RectangleShape shape{ { mBlockSize - 1, mBlockSize - 1 } };
shape.setOrigin(shape.getSize() / 2.f);
constexpr static auto SnakeLength = 3u;
constexpr static auto SnakeHead = 0u;
constexpr static auto SnakeBodyPart = 1u;
const static auto Position = sf::Vector2f{ 70, 70 };
for (auto part = 0u; part < SnakeLength; ++part)
{
if (part == SnakeHead)
shape.setFillColor(sf::Color::Yellow);
else if (part == SnakeBodyPart)
shape.setFillColor(sf::Color::Green);
shape.setPosition(Position.x, Position.y + (SnakeLength - part) * mBlockSize);
mSnakeBody.emplace_back(shape);
}
mDirection = Direction::Down;
mLives = 3;
mIsFirstRun = true;
mLivesText.setString("Lives: " + std::to_string(mLives));
}
void Snake::setDirection(Direction direction)
{
if (std::abs(degree(mDirection) - degree(direction)) == 180) return;
if (mIsFirstRun) mIsFirstRun = false;
mDirection = direction;
}
float Snake::getBlockSize() const
{
return mBlockSize;
}
bool Snake::hasLost() const
{
return mLives == 0;
}
void Snake::lose()
{
mLives = 0;
}
void Snake::extend()
{
static sf::RectangleShape shape{ { mBlockSize - 1, mBlockSize - 1 } };
shape.setOrigin(shape.getSize() / 2.f);
const auto& tail = mSnakeBody.back();
auto x = tail.getPosition().x - std::sin(radian(mDirection)) * mBlockSize;
auto y = tail.getPosition().y - std::cos(radian(mDirection)) * mBlockSize;
shape.setPosition(x, y);
shape.setFillColor(sf::Color::Green);
mSnakeBody.emplace_back(shape);
}
void Snake::update(sf::Time dt)
{
if (mIsFirstRun) return;
checkCollision();
move(dt);
}
sf::FloatRect Snake::getGlobalBounds() const
{
return mSnakeBody.front().getGlobalBounds();
}
bool Snake::hasCollideWithFruit(const sf::CircleShape& shape) const
{
return std::find_if(mSnakeBody.begin(), mSnakeBody.end(),
[&shape](const auto& part)
{
return part.getGlobalBounds().intersects(shape.getGlobalBounds());
}) != mSnakeBody.end();
}
void Snake::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
for (const auto& part : mSnakeBody)
target.draw(part, states);
target.draw(mLivesText, states);
}
void Snake::checkCollision()
{
auto begin = mSnakeBody.cbegin();
auto end = mSnakeBody.cend();
for (auto it = std::next(begin); it != end; ++it)
{
if (!it->getGlobalBounds().contains(begin->getPosition())) continue;
auto index = std::distance(begin, it);
shrink(index);
break;
};
}
void Snake::move(sf::Time dt)
{
constexpr static auto SnakeSpeed = 80.f;
auto vx = std::sin(radian(mDirection)) * SnakeSpeed * dt.asSeconds();
auto vy = std::cos(radian(mDirection)) * SnakeSpeed * dt.asSeconds();
mSnakeBody.front().move(vx, vy);
auto begin = mSnakeBody.begin();
auto end = mSnakeBody.end();
for (auto current = std::next(begin); current != end; ++current)
{
auto previous = std::prev(current);
auto position = previous->getPosition() - current->getPosition();
auto angle = std::atan2(position.x, position.y);
auto x = previous->getPosition().x - std::sin(angle) * mBlockSize;
auto y = previous->getPosition().y - std::cos(angle) * mBlockSize;
current->setPosition(x, y);
};
}
void Snake::shrink(std::size_t index)
{
--mLives;
auto it = mSnakeBody.begin();
std::advance(it, index);
mSnakeBody.erase(it, mSnakeBody.end());
mLivesText.setString("Lives: " + std::to_string(mLives));
}
World.hpp
#pragma once
#include "Snake.hpp"
#include <SFML/Window/Keyboard.hpp>
#include <SFML/Graphics/CircleShape.hpp>
#include <map>
class World : sf::NonCopyable
{
using KeysMap = std::map<sf::Keyboard::Key, Snake::Direction>;
public:
explicit World(sf::RenderTarget& window, const sf::Font& font);
void update(sf::Time dt);
void draw();
private:
void respawnFruit();
void handleRealTimeInput();
void checkCollision();
void checkGameEnd();
private:
sf::RenderTarget& mWindow;
Snake mSnake;
std::size_t mScore;
sf::RectangleShape mBorder;
sf::CircleShape mFruit;
KeysMap mKeyBinding;
sf::Text mScoreText;
};
World.cpp
#include "World.hpp"
#include "Utility.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
using namespace utility;
World::World(sf::RenderTarget& window, const sf::Font& font)
: mWindow(window)
, mSnake(font, static_cast<float>(window.getSize().x))
, mScore()
{
const auto blockSize = mSnake.getBlockSize();
const auto x = window.getSize().x / 2.f;
const auto y = window.getSize().y / 2.f + blockSize;
const auto width = window.getSize().x - 2 * blockSize;
const auto height = window.getSize().y - 4 * blockSize;
mBorder.setPosition(x, y);
mBorder.setSize({ width, height });
mBorder.setOrigin(mBorder.getSize() / 2.f);
mBorder.setFillColor(sf::Color::Transparent);
mBorder.setOutlineThickness(-blockSize);
mBorder.setOutlineColor(sf::Color(211, 211, 211));
mFruit.setRadius(blockSize / 2.f);
const auto originX = mFruit.getGlobalBounds().width / 2u;
const auto originY = mFruit.getGlobalBounds().height / 2u;
mFruit.setOrigin(originX, originY);
mFruit.setFillColor(sf::Color::Red);
respawnFruit();
// initial keys binding
mKeyBinding.emplace(sf::Keyboard::Up, Snake::Up);
mKeyBinding.emplace(sf::Keyboard::Down, Snake::Down);
mKeyBinding.emplace(sf::Keyboard::Left, Snake::Left);
mKeyBinding.emplace(sf::Keyboard::Right, Snake::Right);
mScoreText.setString("Score: " + std::to_string(mScore));
mScoreText.setFont(font);
mScoreText.setStyle(sf::Text::Bold);
mScoreText.setCharacterSize(30);
mScoreText.setColor(sf::Color::White);
mScoreText.setPosition(30.f, 0.f);
}
void World::update(sf::Time dt)
{
handleRealTimeInput();
checkGameEnd();
checkCollision();
mSnake.update(dt);
}
void World::draw()
{
mWindow.draw(mBorder);
mWindow.draw(mFruit);
mWindow.draw(mSnake);
mWindow.draw(mScoreText);
}
void World::respawnFruit()
{
const static auto size = mSnake.getBlockSize();
const static auto bound = mBorder.getGlobalBounds();
const static auto width = mFruit.getGlobalBounds().width / 2u;
const static auto height = mFruit.getGlobalBounds().height / 2u;
static DistType distX(bound.left + size + width, bound.width - width);
static DistType distY(bound.top + size + height, bound.height + size + height);
do
mFruit.setPosition(random(distX), random(distY));
while (mSnake.hasCollideWithFruit(mFruit));
}
void World::handleRealTimeInput()
{
for (const auto& pair : mKeyBinding)
{
if (sf::Keyboard::isKeyPressed(pair.first))
mSnake.setDirection(pair.second);
}
}
void World::checkCollision()
{
const static auto bound = sf::FloatRect
{
mBorder.getGlobalBounds().left + 2 * mSnake.getBlockSize(),
mBorder.getGlobalBounds().top + 2 * mSnake.getBlockSize(),
mBorder.getGlobalBounds().width - 4 * mSnake.getBlockSize(),
mBorder.getGlobalBounds().height - 4 * mSnake.getBlockSize(),
};
if (!bound.intersects(mSnake.getGlobalBounds()))
{
mScore = 0;
mScoreText.setString("Score: " + std::to_string(mScore));
mSnake.lose();
}
if (mFruit.getGlobalBounds().intersects(mSnake.getGlobalBounds()))
{
mSnake.extend();
mScore += 10;
mScoreText.setString("Score: " + std::to_string(mScore));
respawnFruit();
}
}
void World::checkGameEnd()
{
if (mSnake.hasLost())
{
mScore = 0;
mSnake.reset();
mScoreText.setString("Score: " + std::to_string(mScore));
}
}
Game.hpp
#pragma once
#include "World.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
class Game : sf::NonCopyable
{
public:
Game();
void run();
private:
void processEvents();
void update(sf::Time dt);
void render();
private:
sf::RenderWindow mWindow;
World mWorld;
sf::Font mFont;
};
Game.cpp
#include "Game.hpp"
#include <SFML/Window/Event.hpp>
Game::Game()
: mWindow(sf::VideoMode(640, 480), "Snake")
, mWorld(mWindow, mFont)
{
if (!mFont.loadFromFile("Media/arial.ttf"))
throw std::runtime_error("Can't load font file");
}
void Game::run()
{
sf::Clock clock;
auto timeSinceLastUpdate = sf::Time::Zero;
const auto TimePerFrame = sf::seconds(1 / 60.f);
while (mWindow.isOpen())
{
auto dt = clock.restart();
timeSinceLastUpdate += dt;
while (timeSinceLastUpdate > TimePerFrame)
{
timeSinceLastUpdate -= TimePerFrame;
processEvents();
update(TimePerFrame);
}
render();
}
}
void Game::processEvents()
{
sf::Event event;
while (mWindow.pollEvent(event))
{
if (event.type == sf::Event::Closed)
mWindow.close();
}
}
void Game::update(sf::Time dt)
{
mWorld.update(dt);
}
void Game::render()
{
mWindow.clear();
mWorld.draw();
mWindow.display();
}
Main.cpp
#include "Game.hpp"
#include <stdexcept>
#include <iostream>
int main()
{
try
{
Game game;
game.run();
}
catch (std::runtime_error& e)
{
std::cout << "Exception: " << e.what() << std::endl;
return 1;
}
}
asked Jan 10, 2016 at 16:29
lang-cpp