5
\$\begingroup\$

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:

enter image description here

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.