Welcome to Code Forum!

Join a community that supports you and your coding journey from day one. We strive to be a friendly, supportive community that empowers everyone to be better developers. By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!
  • Tutorials listed in this section have not been reviewed nor are affliated by Code Forum. It is the sole responsbility of the authour to properly maintain and ensure information is accurate.

Unmaintained Introduction to coding - 14. Adding shadows

Adding shadows​

Lighting and shadows​

Simulating lighting and shadows can greatly improve the visuals of a video game. It may however get very complex. For the sake of simplicity, here, we will only handle the display of shadows (And in a simple way).

Representation of colors​

Colors are often represented as the amount of red, green and blue they contains (RGB). Each amount is usually in the range [0, 255].

When we want to also handle the transparency, we add the 'alpha' channel (RGBA). If the alpha channel equals 0, then the color is totally transparent and if it equals 255, it is totally opaque.

With SFML, the colors are represented using the channels RGBA.

The following represents the purest red displayable (Red: 255, Green: 0, Blue: 0, Alpha: 255):
C++:
sf::Color(255, 0, 0, 255);
The following represents the purest blue displayable:
C++:
sf::Color(0, 0, 255, 255);
The following represents the purest black displayable:
C++:
sf::Color(0, 0, 0, 255);
The following represents the purest white displayable:
C++:
sf::Color(255, 255, 255, 255);
Note that the color above is equivalent to sf::Color::White.

Filtering the texture with a color​

When we set the color of a sprite, using the method 'setColor', the color given acts as a filter for the texture of the sprite.

A sprite with sf::Color::White as color (This is the default):
C++:
stone.setColor(sf::Color::White);
View attachment 627
The same sprite with a shade of red as color:
C++:
stone.setColor(sf::Color(255, 120, 120, 255));
View attachment 628

The technique​

In this page, the technique used to display the shadow of the objects will consist of displaying the objects twice. First, we display a version of them that is shifted a little toward the direction of the rays of the sun and that is filtered to make it black and lightly translucent. Finally, we draw the normal sprite over it.

Example:

We load the texture of the tree:
C++:
sf::Texture treeTex;
treeTex.loadFromFile("resources/tree.png");
We create a sprite representing the shadow of the tree (Its texture is filtered such that its color becomes a translucent black).
C++:
sf::Sprite treeShadow;
treeShadow.setPosition(100, 100);
treeShadow.setScale(3, 3);
treeShadow.setColor(sf::Color(0, 0, 0, 130));
treeShadow.setTexture(treeTex);
We draw the shadow:
C++:
win.draw(treeShadow);
View attachment 629
We create the sprite of the tree, that we shift a little toward the sun:
C++:
sf::Sprite tree;
tree.setPosition(106, 94);
tree.setScale(3, 3);
tree.setTexture(treeTex);
We draw the sprite of the tree over the shadow:
C++:
win.draw(tree);
View attachment 630

Adding the shadows to the game​

The sprites of the shadows of the trees:
C++:
sf::Sprite treeShadows[8];

for(int c = 0; c < 8; c++)
{
    treeShadows[c].setTexture(treeTex);
    treeShadows[c].setScale(3, 3);
    treeShadows[c].setColor(sf::Color(0, 0, 0, 130));
    treeShadows[c].setPosition(trees[c].getPosition() - sf::Vector2f(8, -8));
}
View attachment 631
The sprites of the shadows of the stones:
C++:
sf::Sprite stoneShadows[3];

for(int c = 0; c < 3; c++)
{
    stoneShadows[c].setTexture(stoneTex);
    stoneShadows[c].setScale(3, 3);
    stoneShadows[c].setColor(sf::Color(0, 0, 0, 130));
    stoneShadows[c].setPosition(stones[c].getPosition() - sf::Vector2f(8, -8));
}
View attachment 632
The sprites of the shadows of the monsters:
C++:
sf::Sprite monsterShadows[4];

for(int c = 0; c < 4; c++)
{
    monsterShadows[c].setTexture(monsterTexs[0]);
    monsterShadows[c].setScale(3, 3);
    monsterShadows[c].setColor(sf::Color(0, 0, 0, 100));
    monsterShadows[c].setPosition(monsters[c].getPosition() - sf::Vector2f(5, -5));
}
We must update the texture of the shadows of the monsters at the same rate they are animated:
C++:
for(int c=0; c < 4; c++)
{
    monsters[c].setTexture(monsterTexs[index]);
    monsterShadows[c].setTexture(monsterTexs[index]);
}
View attachment 633
The sprite of the shadow of the player:
C++:
sf::Sprite playerShadow;
playerShadow.setTexture(playerTex);
playerShadow.setScale(3, 3);
playerShadow.setPosition(player.getPosition() - sf::Vector2f(6, -6));
playerShadow.setColor(sf::Color(0, 0, 0, 130));
We must update the texture of the shadow when the player attacks:
C++:
player.setTexture(playerAttackingTex);
playerShadow.setTexture(playerAttackingTex);
And set it back to normal when the attack is over:
C++:
if(playerAttackClock.getElapsedTime().asMilliseconds() > 1000)
{
    player.setTexture(playerTex);
    playerShadow.setTexture(playerTex);
}
We also have to update the position of the shadow of the character as the player moves:
C++:
playerShadow.setPosition(player.getPosition() - sf::Vector2f(6, -6));
View attachment 634
Finally, the shadow of the merchant and castle:
C++:
sf::Sprite merchantShadow;
merchantShadow.setTexture(merchantTex);
merchantShadow.setScale(3, 3);
merchantShadow.setPosition(merchant.getPosition() - sf::Vector2f(6, -6));
merchantShadow.setColor(sf::Color(0, 0, 0, 130));

sf::Sprite castleShadow;
castleShadow.setTexture(castleTex);
castleShadow.setScale(4, 4);
castleShadow.setPosition(castle.getPosition() - sf::Vector2f(15, -15));
castleShadow.setColor(sf::Color(0, 0, 0, 130));
View attachment 635

The final code of the game​

C++:
#include <SFML/Graphics.hpp>

int main()
{
    sf::RenderWindow win(sf::VideoMode(800, 600), "Introduction to coding", sf::Style::Close);

    // Grass

    sf::Texture grassTex;
    grassTex.loadFromFile("resources/grass.png");
    grassTex.setRepeated(true);

    sf::Sprite grass;
    grass.setTexture(grassTex);
    grass.setScale(5, 5);
    grass.setTextureRect(sf::IntRect(0,0,160,120));

    // Dirt tiles

    sf::Texture dirtTex;
    dirtTex.loadFromFile("resources/dirt.png");

    sf::Sprite dirtTiles[10];
    dirtTiles[0].setPosition(300, 60);
    dirtTiles[1].setPosition(660, 300);
    dirtTiles[2].setPosition(390, 480);
    dirtTiles[3].setPosition(660, 60);
    dirtTiles[4].setPosition(720, 420);
    dirtTiles[5].setPosition(180, 300);
    dirtTiles[6].setPosition(60, 180);
    dirtTiles[7].setPosition(60, 420);
    dirtTiles[8].setPosition(240, 420);
    dirtTiles[9].setPosition(480, 120);

    for(int c = 0; c < 10; c++)
    {
        dirtTiles[c].setTexture(dirtTex);
        dirtTiles[c].setScale(5, 5);
    }

    // Trees

    sf::Texture treeTex;
    treeTex.loadFromFile("resources/tree.png");

    sf::Sprite trees[8];
    trees[0].setPosition(50, 60);
    trees[1].setPosition(600, 150);
    trees[2].setPosition(630, 456);
    trees[3].setPosition(300, 600);
    trees[4].setPosition(720, 40);
    trees[5].setPosition(460, 500);
    trees[6].setPosition(20, 370);
    trees[7].setPosition(340, 240);

    for(int c = 0; c < 8; c++)
    {
        trees[c].setTexture(treeTex);
        trees[c].setScale(3, 3);
    }

    // Stones

    sf::Texture stoneTex;
    stoneTex.loadFromFile("resources/stone.png");

    sf::Sprite stones[3];
    stones[0].setPosition(304, 64);
    stones[1].setPosition(664, 304);
    stones[2].setPosition(180, 300);

    for(int c = 0; c < 3; c++)
    {
        stones[c].setTexture(stoneTex);
        stones[c].setScale(3, 3);
    }

    // Player

    sf::Texture playerTex;
    playerTex.loadFromFile("resources/player.png");
    sf::Texture playerAttackingTex;
    playerAttackingTex.loadFromFile("resources/playerAttacking.png");

    sf::Sprite player;
    player.setTexture(playerTex);
    player.setPosition(400, 300);
    player.setScale(3, 3);

    float playerMovementSpeed = 200.0;
    sf::Clock playerMovementClock;
    sf::Clock playerAttackClock;

    int playerGoldAmount = 0, playerPotionAmount = 0, playerHealthAmount = 3;

    // Monsters

    sf::Texture monsterTexs[3];
    monsterTexs[0].loadFromFile("resources/monster1.png");
    monsterTexs[1].loadFromFile("resources/monster2.png");
    monsterTexs[2].loadFromFile("resources/monster3.png");

    sf::Sprite monsters[4];
    monsters[0].setPosition(500, 40);
    monsters[1].setPosition(100, 170);
    monsters[2].setPosition(400, 450);
    monsters[3].setPosition(690, 160);

    bool monstersDead[4];

    for(int c = 0; c < 4; c++)
    {
        monsters[c].setTexture(monsterTexs[0]);
        monsters[c].setScale(3, 3);
        monstersDead[c] = false;
    }

    sf::Clock monstersAnimationClock;
    sf::Clock monsterRespawnClocks[4];
    sf::Clock monsterAttackClock;
    bool isPlayerNearMonster = false;

    // Gold

    sf::Texture goldTex;
    goldTex.loadFromFile("resources/gold.png");

    sf::Sprite gold;
    gold.setTexture(goldTex);
    gold.setScale(2, 2);
    gold.setPosition(15, 10);

    sf::Font font;
    font.loadFromFile("resources/LinuxBiolinum.ttf");

    sf::Text goldAmountText;
    goldAmountText.setFont(font);
    goldAmountText.setString("0");
    goldAmountText.setFillColor(sf::Color::White);
    goldAmountText.setCharacterSize(24);
    goldAmountText.setPosition(60, 10);

    // Potions

    sf::Texture healthPotionTex;
    healthPotionTex.loadFromFile("resources/healthPotion.png");
    sf::Texture emptyVialTex;
    emptyVialTex.loadFromFile("resources/emptyVial.png");

    sf::Sprite potion;
    potion.setTexture(emptyVialTex);
    potion.setScale(2, 2);
    potion.setPosition(350, 10);

    sf::Text potionAmountText;
    potionAmountText.setFont(font);
    potionAmountText.setString("0");
    potionAmountText.setFillColor(sf::Color::White);
    potionAmountText.setCharacterSize(24);
    potionAmountText.setPosition(395, 10);

    // Health of the player

    sf::Texture heartTex;
    heartTex.loadFromFile("resources/heart.png");
    sf::Texture emptyHeartTex;
    emptyHeartTex.loadFromFile("resources/emptyHeart.png");

    sf::Sprite hearts[3];
    hearts[0].setPosition(800 - 130, 10);
    hearts[1].setPosition(800 - 90, 10);
    hearts[2].setPosition(800 - 50, 10);

    for(int c = 0; c < 3; c++)
    {
        hearts[c].setTexture(heartTex);
        hearts[c].setScale(2, 2);
    }

    // Castle

    sf::Texture castleTex;
    castleTex.loadFromFile("resources/castle.png");

    sf::Sprite castle;
    castle.setTexture(castleTex);
    castle.setScale(4, 4);
    castle.setPosition(40, 460);

    // Merchant

    sf::Texture merchantTex;
    merchantTex.loadFromFile("resources/merchant.png");

    sf::Sprite merchant;
    merchant.setTexture(merchantTex);
    merchant.setScale(3, 3);
    merchant.setPosition(77, 415);

    sf::Text buyPotionText;
    buyPotionText.setFont(font);
    buyPotionText.setString("Press B to buy a potion.");
    buyPotionText.setFillColor(sf::Color::White);
    buyPotionText.setCharacterSize(30);
    buyPotionText.setPosition((800 - (buyPotionText.getGlobalBounds().width)) / 2, (600 - (buyPotionText.getGlobalBounds().height)) / 2);

    bool isPlayerNearMerchant = false;

    // Message at the death of the player

    sf::Text deadText;
    deadText.setFont(font);
    deadText.setString("You are dead.");
    deadText.setFillColor(sf::Color::White);
    deadText.setCharacterSize(30);
    deadText.setPosition((800 - (deadText.getGlobalBounds().width)) / 2, (600 - (deadText.getGlobalBounds().height)) / 2);

    // [B]* Shadows *[/B]

    // Shadow of the trees

    sf::Sprite treeShadows[8];

    for(int c = 0; c < 8; c++)
    {
        treeShadows[c].setTexture(treeTex);
        treeShadows[c].setScale(3, 3);
        treeShadows[c].setColor(sf::Color(0, 0, 0, 130));
        treeShadows[c].setPosition(trees[c].getPosition() - sf::Vector2f(8, -8));
    }

    // Shadow of the stones

    sf::Sprite stoneShadows[3];

    for(int c = 0; c < 3; c++)
    {
        stoneShadows[c].setTexture(stoneTex);
        stoneShadows[c].setScale(3, 3);
        stoneShadows[c].setColor(sf::Color(0, 0, 0, 130));
        stoneShadows[c].setPosition(stones[c].getPosition() - sf::Vector2f(8, -8));
    }

    // Shadow of the player

    sf::Sprite playerShadow;
    playerShadow.setTexture(playerTex);
    playerShadow.setScale(3, 3);
    playerShadow.setPosition(player.getPosition() - sf::Vector2f(6, -6));
    playerShadow.setColor(sf::Color(0, 0, 0, 130));

    // Shadow of the monsters

    sf::Sprite monsterShadows[4];

    for(int c = 0; c < 4; c++)
    {
        monsterShadows[c].setTexture(monsterTexs[0]);
        monsterShadows[c].setScale(3, 3);
        monsterShadows[c].setColor(sf::Color(0, 0, 0, 100));
        monsterShadows[c].setPosition(monsters[c].getPosition() - sf::Vector2f(5, -5));
    }

    //  Shadow of the merchant

    sf::Sprite merchantShadow;
    merchantShadow.setTexture(merchantTex);
    merchantShadow.setScale(3, 3);
    merchantShadow.setPosition(merchant.getPosition() - sf::Vector2f(6, -6));
    merchantShadow.setColor(sf::Color(0, 0, 0, 130));

    // Shadow of the castle

    sf::Sprite castleShadow;
    castleShadow.setTexture(castleTex);
    castleShadow.setScale(4, 4);
    castleShadow.setPosition(castle.getPosition() - sf::Vector2f(15, -15));
    castleShadow.setColor(sf::Color(0, 0, 0, 130));

    // Game loop

    sf::Event event;

    while(win.isOpen())
    {
        while(win.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
                win.close();
            else if(event.type == sf::Event::KeyPressed && playerHealthAmount > 0)
            {
                if(event.key.code == sf:⌨️:B && isPlayerNearMerchant)
                {
                    if(playerGoldAmount > 1)
                    {
                        playerGoldAmount -= 2;
                        playerPotionAmount += 1;

                        goldAmountText.setString(std::to_string(playerGoldAmount));
                        potionAmountText.setString(std::to_string(playerPotionAmount));
                        potion.setTexture(healthPotionTex);
                    }
                }
                else if(event.key.code == sf:⌨️:P)
                {
                    if(playerPotionAmount > 0 && playerHealthAmount < 3)
                    {
                        playerHealthAmount++;
                        playerPotionAmount--;
                        potionAmountText.setString(std::to_string(playerPotionAmount));

                        if(playerPotionAmount == 0)
                            potion.setTexture(emptyVialTex);

                        if(playerHealthAmount == 2)
                            hearts[1].setTexture(heartTex);
                        else if(playerHealthAmount == 3)
                            hearts[2].setTexture(heartTex);
                    }
                }
            }
        }

        if(playerHealthAmount > 0)
        {
            // Player movements

            sf::Vector2f trans;
            float mouv = playerMovementClock.getElapsedTime().asSeconds() * playerMovementSpeed;

            if(sf:⌨️:isKeyPressed(sf:⌨️:W))
                trans.y -= mouv;
            if(sf:⌨️:isKeyPressed(sf:⌨️:S))
                trans.y += mouv;
            if(sf:⌨️:isKeyPressed(sf:⌨️:A))
                trans.x -= mouv;
            if(sf:⌨️:isKeyPressed(sf:⌨️:D))
                trans.x += mouv;

            player.setPosition(player.getPosition().x+trans.x, player.getPosition().y+trans.y);
            playerMovementClock.restart();

            // Collision testing

            for(int c=0; c < 8; c++)
            {
                if(player.getGlobalBounds().intersects(trees[c].getGlobalBounds()))
                    player.setPosition(player.getPosition().x-trans.x, player.getPosition().y-trans.y);
            }
            for(int c=0; c < 3; c++)
            {
                if(player.getGlobalBounds().intersects(stones[c].getGlobalBounds()))
                    player.setPosition(player.getPosition().x-trans.x, player.getPosition().y-trans.y);
            }
            if(player.getGlobalBounds().intersects(castle.getGlobalBounds()))
                player.setPosition(player.getPosition().x-trans.x, player.getPosition().y-trans.y);
            if(player.getGlobalBounds().intersects(merchant.getGlobalBounds()))
                player.setPosition(player.getPosition().x-trans.x, player.getPosition().y-trans.y);

            playerShadow.setPosition(player.getPosition() - sf::Vector2f(6, -6));

            // Animation of the monsters

            int index = monstersAnimationClock.getElapsedTime().asMilliseconds() / 333;

            if(index > 2)
            {
                monstersAnimationClock.restart();
                index = 0;
            }

            for(int c=0; c < 4; c++)
            {
                monsters[c].setTexture(monsterTexs[index]);
                monsterShadows[c].setTexture(monsterTexs[index]);
            }

            // Player attacking

            if((sf:⌨️:isKeyPressed(sf:⌨️:Space) || sf:🐭:isButtonPressed(sf:🐭:Left)) && playerAttackClock.getElapsedTime().asMilliseconds() > 1000)
            {
                playerAttackClock.restart();
                player.setTexture(playerAttackingTex);
                playerShadow.setTexture(playerAttackingTex);


                // Testing if the attack reaches a monster

                sf::FloatRect attackRect(player.getPosition()+sf::Vector2f((player.getGlobalBounds().width/4), -player.getGlobalBounds().height/3), sf::Vector2f(player.getGlobalBounds().width/4, player.getGlobalBounds().height/3));

                for(int c=0; c < 4; c++)
                {
                    if(monstersDead[c] == false && attackRect.intersects(monsters[c].getGlobalBounds()))
                    {
                        monstersDead[c] = true;
                        monsterRespawnClocks[c].restart();

                        playerGoldAmount++;
                        goldAmountText.setString(std::to_string(playerGoldAmount));
                    }
                }
            }

            if(playerAttackClock.getElapsedTime().asMilliseconds() > 1000)
            {
                player.setTexture(playerTex);
                playerShadow.setTexture(playerTex);
            }

            // Monster attacking

            bool isPlayerCurrentlyNearMonster = false;

            for(int c=0; c < 4; c++)
            {
                if(monstersDead[c] == false)
                {
                    sf::FloatRect nearMonsterRect(monsters[c].getGlobalBounds());
                    nearMonsterRect.left -= nearMonsterRect.width;
                    nearMonsterRect.top -= nearMonsterRect.height;
                    nearMonsterRect.width *= 3;
                    nearMonsterRect.height *= 3;

                    if(player.getGlobalBounds().intersects(nearMonsterRect))
                    {
                        isPlayerCurrentlyNearMonster = true;

                        if(isPlayerNearMonster)
                        {
                            if(monsterAttackClock.getElapsedTime().asSeconds() >= 1 && playerHealthAmount > 0)
                            {
                                playerHealthAmount--;
                                monsterAttackClock.restart();

                                if(playerHealthAmount == 0)
                                    hearts[0].setTexture(emptyHeartTex);
                                else if(playerHealthAmount == 1)
                                    hearts[1].setTexture(emptyHeartTex);
                                else
                                    hearts[2].setTexture(emptyHeartTex);
                            }
                        }
                        else
                        {
                            isPlayerNearMonster = true;
                            monsterAttackClock.restart();
                        }
                    }
                }
            }

            if(isPlayerCurrentlyNearMonster == false)
                isPlayerNearMonster = false;

            // Respawn of the monsters

            for(int c=0; c < 4; c++)
            {
                if(monsterRespawnClocks[c].getElapsedTime().asSeconds() >= 8)
                    monstersDead[c] = false;
            }

            // Checks if the player is near the merchant

            sf::FloatRect nearMerchantRect(merchant.getGlobalBounds());
            nearMerchantRect.left -= nearMerchantRect.width;
            nearMerchantRect.top -= nearMerchantRect.height;
            nearMerchantRect.width *= 3;
            nearMerchantRect.height *= 3;

            if(player.getGlobalBounds().intersects(nearMerchantRect))
                isPlayerNearMerchant = true;
            else
                isPlayerNearMerchant = false;
        }

        // Rendering

        win.clear();

        // Floor

        win.draw(grass);

        for(int c = 0; c < 10; c++)
            win.draw(dirtTiles[c]);

        // Shadows

        for(int c = 0; c < 8; c++)
            win.draw(treeShadows[c]);

        for(int c = 0; c < 3; c++)
            win.draw(stoneShadows[c]);

        for(int c = 0; c < 4; c++)
        {
            if(monstersDead[c] == false)
             win.draw(monsterShadows[c]);
        }

        win.draw(playerShadow);

        win.draw(castleShadow);
        win.draw(merchantShadow);

        // Game objects

        for(int c = 0; c < 8; c++)
            win.draw(trees[c]);

        for(int c = 0; c < 3; c++)
            win.draw(stones[c]);

        win.draw(player);

        for(int c = 0; c < 4; c++)
        {
            if(monstersDead[c] == false)
                win.draw(monsters[c]);
        }

        win.draw(castle);
        win.draw(merchant);


        // Player information

        win.draw(gold);
        win.draw(goldAmountText);

        win.draw(potion);
        win.draw(potionAmountText);

        for(int c = 0; c < 3; c++)
            win.draw(hearts[c]);

        if(playerHealthAmount <= 0)
            win.draw(deadText);
        else if(isPlayerNearMerchant)
            win.draw(buyPotionText);

        win.display();
    }

    return 0;
}
 
Last edited by a moderator:
Back
Top Bottom