c++sfml

The enemy sprites are not rendered when i fill the vector with a for loop


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

#include "Player.hpp"
#include "Global.hpp"
#include "Enemy.hpp"


float randint(float min, float max) {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(min, max);
    return dis(gen);
}

int main() {
    Player player;

    player.setPos({ gbl::winWidth / 2.f, gbl::winHeight / 2.f });

    std::vector<Enemy> enemies;

    sf::RenderWindow window(sf::VideoMode(gbl::winWidth, gbl::winHeight), gbl::title);

    for (int i = 0; i < 5; i++) {
        Enemy newEnemy;
        newEnemy.setPos({ randint(10 + newEnemy.getSize().x, gbl::winWidth - newEnemy.getSize().x), 100 });
        enemies.push_back(newEnemy);
        
    }

    sf::Event ev;
    while (window.isOpen()) {
        while (window.pollEvent(ev)) {
            switch (ev.type) {
            case sf::Event::KeyPressed:
                if (ev.key.code == sf::Keyboard::Escape) window.close();
                break;
            case sf::Event::Closed:
                window.close();
                break;
            case sf::Event::KeyReleased:
                if (ev.key.code == sf::Keyboard::R) {
                    player.reset();
                    for (auto& enemy : enemies) {
                        enemy.setPos({ randint(0, gbl::winWidth), 100 });
                    }
                }
            }
        }


        window.clear(gbl::bgColor);

        player.draw(window);
        for (Enemy& enemy : enemies) {
            enemy.update();
            if (enemy.getPos().y + enemy.getSize().y > gbl::winHeight + enemy.getSize().y) {
                enemy.setOffscreen();
            }
            enemy.draw(window);
        }

        player.move();

        window.display();
    }

    return 0;
}

I tried manually creating object of type Enemy and push them in the vector. When i run the game doing this everything works as intended. But when i use a for loop to fill the vector and i run the game only the player's sprite is rendered. I tried putting the for loop inside and outside the main loop but it didn't work. What should i do?

Note: the random header file is in one of the header files

#include "Enemy.hpp"
#include <random>

static inline float randint(float min, float max) {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(min, max);
    return dis(gen);
}

Enemy::Enemy() {
    texture.loadFromFile("assets/images/Enemy.png");
    sprite.setTexture(texture);
    speed = 0.1f;
    offscreen = false;
}


sf::Sprite Enemy::getSprite() const {
    return sprite;
}

sf::Vector2f Enemy::getPos() const {
    return sprite.getPosition();
}

sf::Texture Enemy::getTexture() const {
    return texture;
}

float Enemy::getSpeed() const {
    return speed;
}

bool Enemy::getOffscreen() const {
    return offscreen;
}

sf::Vector2u Enemy::getSize() const {
    return texture.getSize();
}


void Enemy::setPos(sf::Vector2f e_pos) {
    sprite.setPosition(e_pos);
}


void Enemy::setOffscreen() {
    offscreen = true;
}



void Enemy::draw(sf::RenderWindow& d_window){
    
    d_window.draw(sprite);
}


void Enemy::update() {
    if (offscreen == false)
        sprite.move({ 0, speed });
    else {
        setPos({ randint(getSize().x, 800.f - getSize().x), 0 });
        offscreen = false;
    }
    
}

This is the code for the Enemy class

Solution

  • in your Enemy constructor you bind the sprite to the texture

    Enemy::Enemy() {
        texture.loadFromFile("assets/images/Enemy.png");
        sprite.setTexture(texture);
        speed = 0.1f;
        offscreen = false;
    }
    

    but when you insert it into the vector, a copy of the object is inserted into the vector, and the original texture is destroyed, so the sprite now has no texture. (and you don't/shouldn't implement a copy/move constructor)

    you need the sf::Texture to not change its location in memory, so you need to store a pointer to it inside Enemy object instead, the best option is to use std::shared_ptr<sf::Texture> shared_ptr link instead of storing the texture by value, this way even if Enemy got copied/moved the texture will remain in the same location in memory.

    PS: don't forget to initialize the shared_ptr with std::make_shared, also you could initialize only 1 enemy texture outside of Enemy and pass it into the constructor of Enemy as an argument to save memory.

    // in main
    auto enemyTexture = std::make_shared<sf::Texture>();
    enemyTexture->loadFromFile("assets/images/Enemy.png");
    for (int i = 0; i < 5; i++) {
            Enemy newEnemy{enemyTexture};
            ...
        }
    
    // in Enemy
    Enemy::Enemy(std::shared_ptr<sf::Texture> texture) {
        texture_shared_ptr = std::move(texture);
        sprite.setTexture(*texture_shared_ptr);
        speed = 0.1f;
        offscreen = false;
    }
    

    A raw pointer will work here too, but shared_ptr will save you from thinking about object lifetimes.


    When considering what type of pointer to store in your object.

    1. Raw pointers are cheap to copy but require you to manage the object lifetime externally. (Usually through a resource manager)
    2. unique_ptr disables the copy operations, and prevents you from sharing the textures, and you really want to share those, as each texture can be a few KBs of size, but it has the same performance as the raw pointer.
    3. shared_ptr has a small overhead on copy and destruction (they only happen once per object), but you will never run into lifetime issues, and it doesn't prevent your object from being copyable.

    You can safely use shared_ptr for now as the performance hit is negligable, but for a large project with thousands of objects you want to use raw pointers and manage the texture's lifetime externally, as those small savings add up.