Push Button, Receive Code

Programming tutorials, games, music...free beer.

The Walking Dead Screensaver Part 1

Posted by Mark on Saturday August 22nd, 2015 at 5:52am

 

Thank you for checking out "The Walking Dead" screensaver tutorial. You may have already read my first screensaver tutorial, if so you'll be glad that I'm leaving out the Windows API code. The reason is that because this screensaver does not utilize some features of screensavers such as setting parameters and user customization, there is really no need to do it. The previous tutorial did not use any of those features either, but it was more along the lines of walking through the making of a "proper" Windows screensaver - in other words it can be used as a springboard to move forward with implementing platform-specific screensaver features. Here is what the final result looks like:

That said this screensaver will be platform-independent - in so far as SFML itself is - and will have pretty simple functionality. We'll facilitate the management of images using data tables and use c++11/c++14 features as much as possible to cut down on code. We'll even use the PIMPL c++ idiom to clean up the interfaces a bit. Here is the absolute minimum app for an SFML screensaver:

#include <iostream>
#include <SFML/Graphics.hpp>

int main()
{
    sf::VideoMode mode = sf::VideoMode::getDesktopMode();
    sf::RenderWindow window(mode, "The Walking Dead", sf::Style::Fullscreen);
    sf::Event e;

    //create variables for tracking mouse movements
    sf::Mouse::setPosition(sf::Vector2i(400,300), window);
    sf::Vector2i initpos = sf::Mouse::getPosition(window);
    sf::Vector2i movement;

    //fudge factor is to account for mouse sensitivity
    //this eliminates problems with the screensaver
    //exiting at the slightest nudge of the mouse
    sf::Vector2i fudgefactor = sf::Vector2i(4,4);

    while(window.isOpen())
    {
        while(window.pollEvent(e))
        {
            if(e.type == sf::Event::MouseMoved)
            {
                movement = sf::Mouse::getPosition(window);
                //make sure to use the absolute value of the difference
                //between the initial mouse position and the movement
                //and compare it to the fudge factor
                //if this value is greater exit the screensaver
                if (abs(movement.x - initpos.x) > fudgefactor.x
                    || abs(movement.y - initpos.y) > fudgefactor.y)
                {
                    return 0;
                }
            }
        }

        window.clear();

        window.display();
    }

    return 0;
}

In order to move closer to the end result, we'll need to implement some classes to handle cycling through images. The first of which is the "Carousel" class - it is exactly what you are thinking, an image carousel. Here is the class definition for "Carousel.hpp":

#ifndef CAROUSEL_HPP
#define CAROUSEL_HPP

#include <SFML/Graphics/Drawable.hpp>
#include <SFML/Graphics/Transformable.hpp>
#include <SFML/Graphics/Texture.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/System/Vector2.hpp>
#include <SFML/System/Time.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>

#include <memory>

//forward declarations
namespace sf
{
class RenderWindow;
}

namespace CarouselMovement
{
enum
{
    FORWARD = 0,
    REVERSE = 1
};
}

namespace CarouselType
{
enum
{
    NORMAL = 0,
    FADE   = 1,
};
}

class Carousel : public sf::Drawable, sf::Transformable
{
public:
    explicit Carousel(sf::RenderWindow& window, sf::Vector2f scalingFactor, sf::Uint32 type, float effectDuration=0.f);
    ~Carousel();

    void addItem(sf::Sprite& s);

    void requestMove(sf::Uint32 direction);

    sf::Uint32 const getNumberOfItems() const;
    sf::Uint32 const getCurrentItemNumber() const;

    void handleInput(const sf::Event& event);

    void autoScroll(const sf::Uint32 direction);

    void update(sf::Time dt);

    void draw(sf::RenderTarget& target, sf::RenderStates states) const;

private:
    class impl;
    std::unique_ptr<impl> m_impl;
};

#endif // CAROUSEL_HPP

At the top, we've got the necessary includes for fleshing out the image carousel. Note that we will be using the standard memory library for PIMPL'ing the implementation of this class - this idiom requires heap allocation. The anonymous enumerations will be used for the direction in which the carousel moves and the type of carousel specified by the user of the class. For this tutorial, the direction will only be used to iterate through the images left or right. If you had a grid (...or 2D array, matrix whatever) of images you would probably want to display and iterate through them differently, so this may be extended for that purpose. The type of carousel will be normal for this tutorial, but the possibilities are endless. By normal, I mean it will not have any fancy effects or transitions - except fade in/fade out, which isn't very fancy, really. There are currently two main types of Carousel - NORMAL and FADE. This is just an emun to handle the differences between the two, which you will see in the source file. Here is the source file (Carousel.cpp):

#include <Carousel.hpp>
#include <interpolate.hpp>

#include <SFML/Graphics/RenderWindow.hpp>

#include <iostream>

class Carousel::impl
{
    public:
        explicit impl(sf::RenderWindow& window, sf::Vector2f scalingFactor, sf::Uint32 type, float effectDuration);

        bool moveForward();
        bool moveReverse();
    public:
        float m_duration;
        std::vector<sf::Sprite> m_sprites;
        std::size_t m_selector;
        sf::Vector2f m_screenSize;
        sf::Vector2f m_scaleFactor;
        bool m_canMove;
        bool m_requestForward;
        bool m_requestReverse;
        sf::Uint32 m_type;
        sf::Time m_effectDuration;
        sf::Time m_movementDuration;
        unsigned int m_alpha;
};

Carousel::impl::impl(sf::RenderWindow& window, sf::Vector2f scalingFactor, sf::Uint32 type, float effectDuration)
: m_duration(effectDuration)
, m_selector(0)
, m_canMove(true)
, m_screenSize(sf::Vector2f(window.getSize().x, window.getSize().y))
, m_scaleFactor(scalingFactor)
, m_requestForward(true)
, m_requestReverse(false)
, m_type(type)
, m_effectDuration(sf::Time::Zero)
, m_movementDuration(sf::Time::Zero)
, m_alpha(0)
{

}

bool Carousel::impl::moveForward()
{
    if(this->m_requestForward)
    {
        switch(this->m_type)
        {
        case CarouselType::NORMAL:
        {
            if(this->m_selector >= this->m_sprites.size()-1)
                this->m_selector = 0;
            else
                ++this->m_selector;

            //std::cout<<"image number: "<<this->m_selector<<std::endl;

            return true;
        }
        break;

        case CarouselType::FADE:
        {
            if(this->m_selector >= this->m_sprites.size()-1)
                this->m_selector = 0;
            else
                ++this->m_selector;

            //std::cout<<"image number: "<<this->m_selector<<std::endl;

            return true;
        }
        break;

        default:
            break;
        }
    }
    else
        return false;
}

bool Carousel::impl::moveReverse()
{
    if(this->m_requestReverse)
    {
        switch(this->m_type)
        {
        case CarouselType::NORMAL:
        {
            if(this->m_selector <= 0)
                this->m_selector = 0;
            else
                --this->m_selector;

            //std::cout<<"image number: "<<this->m_selector<<std::endl;

            return true;
        }
        break;

        case CarouselType::FADE:
        {
            if(this->m_selector <= 0)
                this->m_selector = 0;
            else
                --this->m_selector;

            //std::cout<<"image number: "<<this->m_selector<<std::endl;

            return true;
        }
        break;

        default:
            break;
        }

    }
    else
        return false;
}

Carousel::Carousel(sf::RenderWindow& window, sf::Vector2f scalingFactor, sf::Uint32 type, float effectDuration)
: m_impl(new impl(window, scalingFactor, type, effectDuration))
{

}

Carousel::~Carousel()
{

}

sf::Uint32 const Carousel::getNumberOfItems() const
{
    return m_impl->m_sprites.size();
};

sf::Uint32 const Carousel::getCurrentItemNumber() const
{
    return m_impl->m_selector;
}

void Carousel::addItem(sf::Sprite& s)
{
    //scale each sprite
    s.scale(m_impl->m_scaleFactor);

    //set each sprite opacity according to type
    switch(m_impl->m_type)
    {
    case CarouselType::NORMAL:
    {
        s.setColor(sf::Color(255,255,255,255));
    }
    break;

    case CarouselType::FADE:
    {
        s.setColor(sf::Color(255,255,255,m_impl->m_alpha));
    }
    break;

    default:
        break;
    }

    //add them to the container
    m_impl->m_sprites.push_back(s);
    //std::cout<<"Number of items: "<<m_impl->m_sprites.size()<<std::endl;
}

void Carousel::requestMove(sf::Uint32 direction)
{
    switch(direction)
    {
        case CarouselMovement::FORWARD:
        {
            m_impl->m_requestForward = true;
            m_impl->m_requestReverse = false;
        }
        break;

        case CarouselMovement::REVERSE:
        {
            m_impl->m_requestForward = false;
            m_impl->m_requestReverse = true;
        }
        break;

        default:
        {
            m_impl->m_requestForward = false;
            m_impl->m_requestReverse = false;
        }
        break;
    }
}

void Carousel::autoScroll(const sf::Uint32 direction)
{
    m_impl->m_canMove = true;
    requestMove(direction);
}

void Carousel::handleInput(const sf::Event& event)
{
    if(event.type == sf::Event::KeyPressed)
    {
        switch(event.key.code)
        {

        case sf::Keyboard::Right:
        {
            switch(m_impl->m_type)
            {
                case CarouselType::NORMAL:
                {
                    m_impl->m_canMove = !m_impl->m_canMove;
                    if(m_impl->m_canMove)
                        requestMove(CarouselMovement::FORWARD);
                }
                break;

                case CarouselType::FADE:
                {
                    m_impl->m_canMove = !m_impl->m_canMove;
                    if(m_impl->m_canMove)
                        requestMove(CarouselMovement::FORWARD);
                }
                break;

            default:
                break;
            }

        }
        break;

        case sf::Keyboard::Left:
        {
            switch(m_impl->m_type)
            {
                case CarouselType::NORMAL:
                {
                    m_impl->m_canMove = !m_impl->m_canMove;
                    if(m_impl->m_canMove)
                        requestMove(CarouselMovement::REVERSE);
                }
                break;

                case CarouselType::FADE:
                {
                    m_impl->m_canMove = !m_impl->m_canMove;
                    if(m_impl->m_canMove)
                        requestMove(CarouselMovement::REVERSE);
                }
                break;

                default:
                    break;
            }
        }
        break;

        default:
            break;
        }
    }
}

void Carousel::update(sf::Time dt)
{
    switch(m_impl->m_type)
    {
        case CarouselType::NORMAL:
        {
            if(m_impl->m_effectDuration.asSeconds() >= m_impl->m_duration/4.f)
                m_impl->m_effectDuration = sf::Time::Zero;
            else
                m_impl->m_effectDuration += dt;

            if(m_impl->m_movementDuration.asSeconds() >= m_impl->m_duration)
                m_impl->m_movementDuration = sf::Time::Zero;
            else
                m_impl->m_movementDuration += dt;

            if(m_impl->m_movementDuration.asSeconds() >= m_impl->m_duration)
            {
                m_impl->m_effectDuration = sf::Time::Zero;
                m_impl->m_movementDuration = sf::Time::Zero;

                if(m_impl->moveForward())
                {
                    m_impl->m_requestForward = false;
                    m_impl->m_canMove = false;
                    //std::cout<<"move forward successful"<<std::endl;
                }

                else if(m_impl->moveReverse())
                {
                    m_impl->m_requestReverse = false;
                    m_impl->m_canMove = false;
                    //std::cout<<"move reverse successful"<<std::endl;
                }
                //else
                //std::cout<<"no move"<<std::endl;
            }
        }
        break;

        case CarouselType::FADE:
        {
            if(m_impl->m_effectDuration.asSeconds() >= m_impl->m_duration/4.f)
                m_impl->m_effectDuration = sf::Time::Zero;
            else
                m_impl->m_effectDuration += dt;

            if(m_impl->m_movementDuration.asSeconds() >= 4.f)
                m_impl->m_movementDuration = sf::Time::Zero;
            else
                m_impl->m_movementDuration += dt;

            if(m_impl->m_movementDuration.asSeconds() > 0.f && m_impl->m_movementDuration.asSeconds() <= m_impl->m_duration/4.f)
            {
                if(m_impl->m_alpha >= 255)
                {
                    m_impl->m_alpha = 255;
                }
                else
                    m_impl->m_alpha = (unsigned int)math::interpolate::sineEaseIn(m_impl->m_effectDuration.asSeconds(), 0.f, 255.f, m_impl->m_duration/4.f);
            }
            else if(m_impl->m_movementDuration.asSeconds() > (m_impl->m_duration/4.f) && m_impl->m_movementDuration.asSeconds() <= (m_impl->m_duration*.75f))
            {
                m_impl->m_alpha = 255;
                m_impl->m_effectDuration = sf::Time::Zero;
            }
            else if(m_impl->m_movementDuration.asSeconds() > (m_impl->m_duration*.75f) && m_impl->m_movementDuration.asSeconds() < m_impl->m_duration)
            {
                if(m_impl->m_alpha <= 0)
                {
                    m_impl->m_alpha = 0;
                }
                else
                    m_impl->m_alpha = 255-(unsigned int)math::interpolate::sineEaseIn(m_impl->m_effectDuration.asSeconds(), 0.f, 255.f, m_impl->m_duration/4.f);
            }
            else if(m_impl->m_movementDuration.asSeconds() >= m_impl->m_duration)
            {
                m_impl->m_effectDuration = sf::Time::Zero;
                m_impl->m_movementDuration = sf::Time::Zero;

                if(m_impl->moveForward())
                {
                    m_impl->m_requestForward = false;
                    m_impl->m_canMove = false;
                    //std::cout<<"move forward successful"<<std::endl;
                }

                else if(m_impl->moveReverse())
                {
                    m_impl->m_requestReverse = false;
                    m_impl->m_canMove = false;
                    //std::cout<<"move reverse successful"<<std::endl;
                }
                //else
                //std::cout<<"no move"<<std::endl;
            }

            m_impl->m_sprites[m_impl->m_selector].setColor(sf::Color(255,255,255,m_impl->m_alpha));
        }
        break;

        default:
            break;
    }
}

void Carousel::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    states.transform *= getTransform();

    target.draw(m_impl->m_sprites[m_impl->m_selector], states);
}

The Carousel::impl class simply holds all of the data that might be used by the person implementing a screensaver with the image carousel. The Carousel class itself handles all of the messy details by operating on the Carousel::impl data in the source file (invisible to the user of the library - if your library is open source it just makes things less convenient, obviously, and prevents any accidental operations on data that is internal to the Carousel class).

The Carousel::impl::moveForward() class handles forward iteration through the list of images available to the image carousel. The Carousel::impl::moveReverse() method does the same thing in reverse. Using the CarouselType enum, it is now easier to organize behavior according to the type of carousel the user elects to use - which neither types currently available differ right now with respect to iteration. That's about it for the Carousel::impl class - there is a lot of construction activity at the beginning, but I'm sure that's easy to follow! ;)

The Carousel class itself handles some of the more arduous tasks, such as requesting movement (forward or reverse in the list of images), adding items (not really arduous, but some scaling and pre-calculations are needed prior to adding), "auto-scrolling" through the images, handling input (not useful for a screensaver, but possibly for some other application), and performing updates. The Carousel::requestMove(sf::Uint32 direction) method is fairly simple - it just takes a direction defined in the CarouselMovement enum and prevents the possibility that the application will try to move in another direction using boolean variables (m_requestForward and m_requestReverse acquired from m_impl). Of course it accounts for type here, but again, there are no meaningful difference in terms of the iteration of the images, and thus the motion.

The Carousel::autoScroll(const sf::Uint32 direction) method is very simple - it takes the direction requested and sets the boolean for m_canMove to true, because the motion will be handled with timers. It should continuously scroll in the direction requested and reset automagically. ;)

The Carousel::update(sf::Time dt) method is where most of the magic happens for cycling through images and applying effects to the images. The simplest case, CarouselType::NORMAL is almost self-explanatory. Since there are no updates on the opacity or color of the image, the clocks () just run normally and the updates cycle. When m_impl->m_movementDuration.asSeconds() >= m_impl->m_duration, the iterator is moved forward to point to the next image in the container. The m_impl->m_duration variable is set by the user of the class on construction of the instance, so set it and forget it! If the user picks the CarouselType::FADE, it works the same way - set it and forget it! The updates run normally, except now the m_impl->m_effectDuration time is reset every time the m_impl->m_movementDuration is reset or it is between 1/4 of the duration given by the user and 3/4 of the duration the user specifies when they create an instance. So, if the user specifies 4 seconds between each image, the image will fade in on the first second (0-1), and fade out on the last second (3-4). Easy as pie. Play around with different numbers, I found 4 seconds to be pleasant.

In the next part of this tutorial, I'll wrap it up with a minimal example. I'll also show you the ScreenSaver class which moves the Carousel into a more concrete application.