Based on my previous question, I have implemented all the recommendations received. In addition, i have implemented new features and completed road geometries.
Here is a summary of the improvements:
- Rewritten the
Colors
class again instead of old class which is used hierarchy design. - Added curve for the road
- Added hills for the road
- Added fake fog effect to the scene (no Opengl used)
I would like to know how can I improve it further.
#define _USE_MATH_DEFINES
#include <SFML/Graphics.hpp>
#include <vector>
#include <array>
#include <memory>
#include <cmath>
#include <random>
#include <stdexcept>
#include <iostream>
#ifndef M_PI
#define M_PI 3.141592653589793238462643383
#endif
#ifndef M_E
#define M_E 2.71828182845904523536
#endif
namespace
{
float increase(float start, float increment, float max)
{
auto result = start + increment;
while (result >= max)
result -= max;
while (result < 0)
result += max;
return result;
}
float limit(float value, float min, float max)
{
return std::max(min, std::min(value, max));
}
float rumbleWidth(float projectedRoadWidth, std::size_t lanes)
{
return projectedRoadWidth / std::max(6u, 2 * lanes);
}
float laneMarkerWidth(float projectedRoadWidth, std::size_t lanes)
{
return projectedRoadWidth / std::max(32u, 8 * lanes);
}
float exponentialFog(float distance, float density)
{
return static_cast<float>(1 / std::pow(M_E, (distance * distance * density)));
}
float easeIn(float a, float b, float percent)
{
return a + (b - a) * std::pow(percent, 2.f);
}
float easeOut(float a, float b, float percent)
{
return a + (b - a) * (1 - std::pow(1.f - percent, 2.f));
}
float easeInOut(float a, float b, float percent)
{
return a + (b - a) * (static_cast<float>(-std::cos(percent * M_PI) / 2.f) + 0.5f);
}
float interpolate(float a, float b, float percent)
{
return a + (b - a)*percent;
}
float percentRemaining(float n, float total)
{
return (static_cast<int>(n) % static_cast<int>(total)) / total;
}
}
class Polygon final : public sf::Drawable, public sf::Transformable, sf::NonCopyable
{
public:
Polygon() : mVertices(sf::Quads, 4u) {}
void setVertices(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, sf::Color color)
{
mVertices[0].position = sf::Vector2f(x1, y1);
mVertices[1].position = sf::Vector2f(x2, y2);
mVertices[2].position = sf::Vector2f(x3, y3);
mVertices[3].position = sf::Vector2f(x4, y4);
mVertices[0].color = mVertices[1].color = mVertices[2].color = mVertices[3].color = color;
}
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override
{
states.transform *= getTransform();
target.draw(mVertices, states);
}
private:
sf::VertexArray mVertices;
};
struct Point
{
struct Screen
{
float x{};
float y{};
float w{};
}screen{};
sf::Vector3f world{};
sf::Vector3f camera{};
void project(float cameraX, float cameraY, float cameraZ, float cameraDepth, float width, float height, float roadWidth)
{
camera.x = world.x - cameraX;
camera.y = world.y - cameraY;
camera.z = world.z - cameraZ;
auto scale = cameraDepth / camera.z;
screen.x = width / 2.f + scale * camera.x * width / 2.f;
screen.y = height / 2.f - scale * camera.y * height / 2.f;
screen.w = scale * roadWidth * width / 2.f;
}
};
class Colors
{
struct ColorsData
{
sf::Color road{};
sf::Color grass{};
sf::Color rumble{};
sf::Color lanes{};
};
using ColorsContainer = std::vector<ColorsData>;
public:
enum Type
{
Light,
Dark,
Start,
Finish,
TypeCount
};
public:
Colors(Type type = Colors::Light)
: mType(type)
, mData(Colors::TypeCount)
{
mData[Colors::Light].road = { 100, 100, 100 };
mData[Colors::Light].grass = { 16, 170, 16 };
mData[Colors::Light].rumble = { 85, 85 , 85 };
mData[Colors::Light].lanes = sf::Color::White;
mData[Colors::Dark].road = { 100, 100, 100 };
mData[Colors::Dark].grass = { 0, 154, 0 };
mData[Colors::Dark].rumble = { 187,187, 187 };
mData[Colors::Dark].lanes = { 100, 100, 100 };
mData[Colors::Start].road = sf::Color::White;
mData[Colors::Start].grass = { 16, 170, 16 };
mData[Colors::Start].rumble = sf::Color::White;
mData[Colors::Start].lanes = sf::Color::White;
mData[Colors::Finish].road = {};
mData[Colors::Finish].grass = { 16, 170, 16 };
mData[Colors::Finish].rumble = {};
mData[Colors::Finish].lanes = {};
}
void setType(Type type) { mType = type;}
sf::Color getRoad() const { return mData[mType].road;}
sf::Color getGrass() const { return mData[mType].grass;}
sf::Color getRumble() const { return mData[mType].rumble;}
sf::Color getLane() const { return mData[mType].lanes;}
private:
Type mType;
ColorsContainer mData;
};
class Segment final : public sf::Drawable, public sf::Transformable, sf::NonCopyable
{
public:
using Ptr = std::unique_ptr<Segment>;
public:
void setCurve(float i) { mCurve = i; }
float getCurve() const { return mCurve; }
Point& getPoint1() { return mPoint1; }
Point& getPoint2() { return mPoint2; }
const Point& getPoint1() const { return mPoint1; }
const Point& getPoint2() const { return mPoint2; }
void setSegmentColors(Colors::Type c) { mColors.setType(c); }
void setIndex(std::size_t i) { mIndex = i; }
std::size_t getIndex() const { return mIndex; }
void setGrounds(float width, float fog)
{
auto lanes = 3u;
// Landscape
mLandscape.setSize({ width, mPoint1.screen.y - mPoint2.screen.y });
mLandscape.setPosition(0, mPoint2.screen.y);
mLandscape.setFillColor(mColors.getGrass());
// Rumble sides
auto rumbleWidth1 = rumbleWidth(mPoint1.screen.w, lanes);
auto rumbleWidth2 = rumbleWidth(mPoint2.screen.w, lanes);
mRumbleSide1.setVertices(mPoint1.screen.x - mPoint1.screen.w - rumbleWidth1, mPoint1.screen.y,
mPoint1.screen.x - mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x - mPoint2.screen.w, mPoint2.screen.y,
mPoint2.screen.x - mPoint2.screen.w - rumbleWidth2, mPoint2.screen.y, mColors.getRumble());
mRumbleSide2.setVertices(mPoint1.screen.x + mPoint1.screen.w + rumbleWidth1, mPoint1.screen.y,
mPoint1.screen.x + mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x + mPoint2.screen.w, mPoint2.screen.y,
mPoint2.screen.x + mPoint2.screen.w + rumbleWidth2, mPoint2.screen.y, mColors.getRumble());
// Main Road
mMainRoad.setVertices(mPoint1.screen.x - mPoint1.screen.w, mPoint1.screen.y,
mPoint1.screen.x + mPoint1.screen.w, mPoint1.screen.y,
mPoint2.screen.x + mPoint2.screen.w, mPoint2.screen.y,
mPoint2.screen.x - mPoint2.screen.w, mPoint2.screen.y, mColors.getRoad());
// Lanes
auto laneMarkerWidth1 = laneMarkerWidth(mPoint1.screen.w, lanes);
auto laneMarkerWidth2 = laneMarkerWidth(mPoint2.screen.w, lanes);
auto lanew1 = mPoint1.screen.w * 2 / lanes;
auto lanew2 = mPoint2.screen.w * 2 / lanes;
auto lanex1 = mPoint1.screen.x - mPoint1.screen.w + lanew1;
auto lanex2 = mPoint2.screen.x - mPoint2.screen.w + lanew2;
for (auto lane = 1u; lane < lanes; lanex1 += lanew1 + 1, lanex2 += lanew2 + 1, lane++)
{
if (lane == 1)
mLanes1.setVertices(lanex1 - laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex1 + laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex2 + laneMarkerWidth2 / 2, mPoint2.screen.y,
lanex2 - laneMarkerWidth2 / 2, mPoint2.screen.y, mColors.getLane());
else
mLanes2.setVertices(lanex1 - laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex1 + laneMarkerWidth1 / 2, mPoint1.screen.y,
lanex2 + laneMarkerWidth2 / 2, mPoint2.screen.y,
lanex2 - laneMarkerWidth2 / 2, mPoint2.screen.y, mColors.getLane());
}
// Fog effect
mFog.setSize({ width, mPoint1.screen.y - mPoint2.screen.y });
mFog.setPosition(0, mPoint1.screen.y);
mFog.setFillColor(sf::Color(255, 255, 255, 255 - static_cast<unsigned char>(fog * 255)));
}
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override
{
states.transform *= getTransform();
target.draw(mLandscape, states);
target.draw(mRumbleSide1, states);
target.draw(mRumbleSide2, states);
target.draw(mMainRoad, states);
target.draw(mLanes1, states);
target.draw(mLanes2, states);
target.draw(mFog, states);
}
private:
Point mPoint1{};
Point mPoint2{};
Polygon mRumbleSide1{};
Polygon mRumbleSide2{};
Polygon mLanes1{};
Polygon mLanes2{};
Polygon mMainRoad{};
sf::RectangleShape mLandscape{};
sf::RectangleShape mFog{};
Colors mColors{};
std::size_t mIndex{};
float mCurve{};
};
struct Road
{
struct Length
{
const float shorty = 25.f;
const float medium = 50.f;
const float longy = 100.f;
} length;
struct Curve
{
const float easy = 2.f;
const float medium = 4.f;
const float hard = 6.f;
} curve;
struct Hill
{
const float none = 0.f;
const float low = 20.f;
const float medium = 40.f;
const float high = 60.f;
}hill;
}road;
class Game
{
using SegmentContainer = std::vector<Segment::Ptr>;
public:
Game()
: mWindow(sf::VideoMode(640, 480), "test")
, mSegments()
, mSegmentLength(200.f)
, mPlayerX(0.f)
, mCameraDepth(1 / std::atan((100.f / 2.f)))
, mCameraHeight(1000.f)
, mPlayerZ((mCameraHeight * mCameraDepth))
, mPosition(0.f)
, mRumbleLength(3u)
, mTrackLength(0.f)
, mSpeed(0.f)
{
// build road
addStraight(road.length.shorty / 2);
addHill(road.length.shorty, road.hill.low);
addLowRollingHills();
addCurve(road.length.medium, road.curve.medium, road.hill.low);
addLowRollingHills();
addCurve(road.length.longy, road.curve.medium, road.hill.medium);
addStraight();
addCurve(road.length.longy, -road.curve.medium, road.hill.medium);
addHill(road.length.longy, road.hill.high);
addCurve(road.length.longy, road.curve.medium, -road.hill.low);
addHill(road.length.longy, -road.curve.medium);
addStraight();
addDownhillToEnd();
// setup start and finish of road
mSegments[mSegments[static_cast<std::size_t>(std::floor(mPlayerZ / mSegmentLength)) % mSegments.size()]->getIndex() + 2]->setSegmentColors(Colors::Start);
mSegments[mSegments[static_cast<std::size_t>(std::floor(mPlayerZ / mSegmentLength)) % mSegments.size()]->getIndex() + 3]->setSegmentColors(Colors::Start);
for (auto n = 0u; n < mRumbleLength; n++)
{
mSegments[mSegments.size() - 1 - n]->setSegmentColors(Colors::Finish);
}
mTrackLength = mSegments.size() * mSegmentLength;
}
void run()
{
sf::Clock clock;
auto timeSinceLastUpdate = sf::Time::Zero;
while (mWindow.isOpen())
{
auto elapsedTime = clock.restart();
timeSinceLastUpdate += elapsedTime;
while (timeSinceLastUpdate > TimePerFrame)
{
timeSinceLastUpdate -= TimePerFrame;
processEvents();
update(TimePerFrame);
}
render();
}
}
private:
void processEvents()
{
sf::Event event;
while (mWindow.pollEvent(event))
{
if (event.type == sf::Event::Closed)
mWindow.close();
}
}
void update(sf::Time TimePerFrame)
{
auto dt = TimePerFrame.asSeconds();
auto step = 1 / 60.f;
auto maxSpeed = mSegmentLength / step;
auto accel = maxSpeed / 5.f;
auto breaking = -maxSpeed;
auto decel = -maxSpeed / 5.f;
auto offRoadDecel = -maxSpeed / 2.f;
auto offRoadLimit = maxSpeed / 4.f;
auto centrifugal = 0.3f;
const auto& playerSegment = *mSegments[static_cast<std::size_t>(std::floor((mPosition + mPlayerZ) / mSegmentLength)) % mSegments.size()];
auto speedPercent = mSpeed / maxSpeed;
auto dx = dt * speedPercent;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
mPlayerX -= dx;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
mPlayerX += dx;
mPlayerX -= (dx * speedPercent * playerSegment.getCurve() * centrifugal);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
mSpeed += accel * dt;
else
mSpeed += decel * dt;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
mSpeed += breaking * dt;
if (((mPlayerX < -1.f) || (mPlayerX > 1.f)) && (mSpeed > offRoadLimit))
mSpeed += offRoadDecel * dt;
mPlayerX = limit(mPlayerX, -2.f, 2.f);
mSpeed = limit(mSpeed, 0, maxSpeed);
mPosition = increase(mPosition, dt * mSpeed, mTrackLength);
}
void render()
{
auto width = 640.f;
auto height = 480.f;
auto roadWidth = 2000.f;
auto drawDistance = 500u;
auto fogDensity = 5.f;
const auto& baseSegment = *mSegments[static_cast<std::size_t>(std::floor(mPosition / mSegmentLength)) % mSegments.size()];
auto basePercent = percentRemaining(mPosition, mSegmentLength);
const auto& playerSegment = *mSegments[static_cast<std::size_t>(std::floor((mPosition + mPlayerZ) / mSegmentLength)) % mSegments.size()];
auto playerPercent = percentRemaining(mPosition + mPlayerZ, mSegmentLength);
auto playerY = interpolate(playerSegment.getPoint1().world.y, playerSegment.getPoint2().world.y, playerPercent);
auto x = 0.f;
auto dx = -(baseSegment.getCurve() * basePercent);
auto maxy = height;
mWindow.clear();
for (auto n = 0u; n < drawDistance; ++n)
{
auto& segment = *mSegments[(baseSegment.getIndex() + n) % mSegments.size()];
bool looped = segment.getIndex() < baseSegment.getIndex();
auto fog = exponentialFog(n / (drawDistance * 1.f), fogDensity);
auto camX = mPlayerX * roadWidth;
auto camY = playerY + mCameraHeight;
auto camZ = mPosition - (looped ? mTrackLength : 0.f);
auto& point1 = segment.getPoint1();
auto& point2 = segment.getPoint2();
point1.project(camX - x, camY, camZ, mCameraDepth, width, height, roadWidth);
point2.project(camX - x - dx, camY, camZ, mCameraDepth, width, height, roadWidth);
x += dx;
dx += segment.getCurve();
if ((point1.camera.z <= mCameraDepth) || (point2.screen.y >= maxy || point2.screen.y >= point1.screen.y))
continue;
segment.setGrounds(width, fog);
mWindow.draw(segment);
maxy = point2.screen.y;
}
mWindow.display();
}
void addSegment(float curve, float y)
{
auto n = mSegments.size();
auto segment = std::make_unique<Segment>();
segment->setIndex(n);
segment->setCurve(curve);
segment->getPoint1().world.y = lastY();
segment->getPoint2().world.y = y;
segment->getPoint1().world.z = n * mSegmentLength;
segment->getPoint2().world.z = (n + 1) * mSegmentLength;
if (static_cast<std::size_t>(std::floor(n / mRumbleLength)) % 2)
segment->setSegmentColors(Colors::Light);
else
segment->setSegmentColors(Colors::Dark);
mSegments.push_back(std::move(segment));
}
void addRoad(float enter, float hold, float leave, float curve, float y)
{
auto startY = lastY();
auto endY = startY + (y * mSegmentLength);
auto total = enter + hold + leave;
for (auto n = 0.f; n < enter; ++n)
addSegment(easeIn(0, curve, n / enter), easeInOut(startY, endY, n / total));
for (auto n = 0.f; n < hold; ++n)
addSegment(curve, easeInOut(startY, endY, (enter + n) / total));
for (auto n = 0.f; n < leave; ++n)
addSegment(easeInOut(curve, 0, n / leave), easeInOut(startY, endY, (enter + hold + n) / total));
}
void addStraight(float n = 0)
{
auto num = (n == 0) ? road.length.medium : n;
addRoad(num, num, num, 0, 0);
}
void addHill(float n = 0, float h = 0)
{
auto num = (n == 0) ? road.length.medium : n;
auto height = (h == 0) ? road.hill.medium : h;
addRoad(num, num, num, 0, height);
}
void addCurve(float n = 0, float c = 0, float h = 0)
{
auto num = (n == 0) ? road.length.medium : n;
auto curve = (c == 0) ? road.curve.medium : c;
auto height = (h == 0) ? road.hill.none : h;
addRoad(num, num, num, curve, height);
}
void addDownhillToEnd(float n = 0)
{
auto num = (n == 0) ? 200 : n;
addRoad(num, num, num, -road.curve.easy, -lastY() / mSegmentLength);
}
void addSCurves()
{
addRoad(road.length.medium, road.length.medium, road.length.medium, -road.curve.easy, road.hill.none);
addRoad(road.length.medium, road.length.medium, road.length.medium, road.curve.medium, road.hill.medium);
addRoad(road.length.medium, road.length.medium, road.length.medium, road.curve.easy, -road.hill.low);
addRoad(road.length.medium, road.length.medium, road.length.medium, -road.curve.easy, road.hill.medium);
addRoad(road.length.medium, road.length.medium, road.length.medium, -road.curve.medium, -road.hill.medium);
}
void addLowRollingHills(float n = 0, float h = 0)
{
auto num = (n == 0) ? road.length.shorty : n;
auto height = (h == 0) ? road.hill.low : h;
addRoad(num, num, num, 0, height / 2);
addRoad(num, num, num, 0, -height);
addRoad(num, num, num, 0, height);
addRoad(num, num, num, 0, 0);
addRoad(num, num, num, 0, height / 2);
addRoad(num, num, num, 0, 0);
}
float lastY()
{
return (mSegments.size() == 0) ? 0 : mSegments[mSegments.size() - 1]->getPoint2().world.y;
}
private:
sf::RenderWindow mWindow;
const static sf::Time TimePerFrame;
SegmentContainer mSegments;
std::size_t mRumbleLength;
float mSegmentLength;
float mPlayerX;
float mCameraDepth;
float mCameraHeight;
float mPlayerZ;
float mPosition;
float mTrackLength;
float mSpeed;
};
const sf::Time Game::TimePerFrame = sf::seconds(1 / 60.f);
int main()
{
try
{
Game game;
game.run();
}
catch (std::exception& e)
{
std::cout << "\nEXCEPTION: " << e.what() << std::endl;
}
}
Output: