How to use a Manager class to manage Characters and their animations in C++ using the SFML library

跟風遠走 提交于 2020-06-29 05:16:40

问题


I'm trying to create a 2D sidescroller mini-game. For now, I only have a character with a sprite and one animation, which i'm trying to move using the left/right arrows. At first, I only had a Character class, storing the sprite of the character and its running animation. And it worked. But now, I'm trying to add a CharacterManager class, which will create all the characters to avoid doing it in the main, and which will manage their movements and draw them.

And it doesn't work anymore. I think my problems come from the fact that I have trouble using pointers, which I'm not really familiar with.

Here are the different classes I'm using :

Animation.h :

#pragma once
#include <vector>
#include <SFML/Graphics.hpp>
#include <stdexcept>
#include <ctime>
#include "Constants.h"

class Animation {
public:
    Animation();
    ~Animation();

    void SetFrames(std::vector<sf::IntRect> frames) { m_frames = frames; }
    sf::IntRect Play();

private:
    std::vector<sf::IntRect> m_frames;
    unsigned int m_currentFrame;
    float m_updateTime;
    float m_timeSinceLastFrame;
    float m_lastCallTimestamp;
    float m_currentTimestamp;
    bool m_firstCall;
};

Animation.cpp :

#include "Animation.h"

Animation::Animation() {
    m_currentFrame = 0;

    m_updateTime = 1.0f / ANIMATION_SPEED;
    m_timeSinceLastFrame = 0.0f;
    m_firstCall = true;
}

Animation::~Animation() {

}

sf::IntRect Animation::Play() {
    if (m_frames.size() == 0) {
        throw std::length_error("The frames vector is empty");
    }

    // Advance time and add the elapsed time to timeSinceLastFrame
    m_currentTimestamp = std::clock();

    // Ignore elapsed time if first call
    if (m_firstCall) {
        m_timeSinceLastFrame = 0.0f;
        m_lastCallTimestamp = m_currentTimestamp;
        m_firstCall = false; // Not first call anymore
    }
    else {
        m_timeSinceLastFrame += (m_currentTimestamp - m_lastCallTimestamp) / CLOCKS_PER_SEC;
        m_lastCallTimestamp = m_currentTimestamp;
    }

    // Next frame
    if (m_timeSinceLastFrame >= m_updateTime) {
        m_currentFrame++;
        m_timeSinceLastFrame = 0; 

        // Check animation end
        if (m_currentFrame >= m_frames.size()) {
            m_currentFrame = 0; // Reset frame progression
            m_firstCall = true; // Next passage will be the first call of a new animation

            /* TODO : return something to alert the end of the animation
            (like a specific rectint or set a variable to true and get it on the other side) */
        }
    }

    return m_frames[m_currentFrame];
}

Character.h :

#pragma once

#include<string>
#include<iostream>
#include <SFML/Graphics.hpp>
#include <vector>
#include <map>
#include "Constants.h"
#include "Animation.h"

class Character : public sf::Drawable {
public:
    Character();
    Character(std::string name);
    ~Character();

    void Move(float value);

    // Setters
    void SetTexture(std::string filename);
    void SetPosition(sf::Vector2f pos) { m_position = pos; };
    void SetAnimations(std::map<std::string, Animation*> animations) { m_animations = animations; };

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

    std::string m_name;
    unsigned int m_orientation; // 0 (default) = right | 1 = left
    std::map<std::string, Animation*> m_animations; 
    Animation runningAnimation; 
    sf::Vector2f m_position;
    sf::Texture m_texture;
    sf::Sprite m_sprite;
};

Character.cpp :

#include "Character.h"

Character::Character() {}

Character::Character(std::string name) {
    m_name = name;
    m_orientation = 0;

    runningAnimation = Animation();
}

Character::~Character() {
}

void Character::draw(sf::RenderTarget& target, sf::RenderStates states) const {
    target.draw(m_sprite, states);
}

void Character::Move(float value) {
    m_sprite.setTextureRect(runningAnimation.Play());
    m_position.x += value;
    m_sprite.setPosition(m_position);
}

void Character::SetTexture(std::string filename) {
    filename = TEXTURE_FILES_PREFIX + filename;

    // Load the entire texture file
    if (!m_texture.loadFromFile(filename))
        std::cout << "Error loading texture file : " << filename << std::endl;

    // Set the texture (by default, initialize to idle state) and the position
    std::vector<sf::IntRect> runningFrames{
        sf::IntRect(67, 45, 19, 28),
        sf::IntRect(116, 46, 20, 27),
        sf::IntRect(166, 48, 20, 25),
        sf::IntRect(217, 45, 22, 28),
        sf::IntRect(266, 46, 19, 27),
        sf::IntRect(316, 48, 20, 25)
    };

    runningAnimation.SetFrames(runningFrames);
    m_sprite.setTexture(m_texture);
    m_sprite.setTextureRect(runningAnimation.Play());
    m_sprite.setPosition(m_position);
}

CharacterManager.h :

#pragma once

#include <vector>
#include <map>
#include <iostream>
#include <SFML\Graphics.hpp>
#include "AliveCharacter.h"
#include "Npc.h"
#include "Animation.h"
#include "CharacterStats.h"

enum CharacterType
{
    NPC,
    ALIVE,
    GENERAL
};

// Class containing a vector of character entities and creates the animations of these entities from a data file (later)
class CharacterManager : public sf::Drawable {
public :
    CharacterManager();
    ~CharacterManager();

    // Loads the file and stores the content inside data string (not used for now)
    void LoadDataFile(std::string filename); 
    // Create a character and add it to the list
    void CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos);
    void CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos, std::map<std::string, Animation*> animations);
    void CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos, std::map<std::string, Animation*> animations, CharacterStats stats);

    void Move(float value);

    Character* GetCharacter(std::string name) { return m_characters[name]; }

private :
    // Calls the draw() function of each stored Character
    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;

    std::string m_data;
    std::map<std::string, Character*> m_characters;
};

CharacterManager.cpp :

#include "CharacterManager.h"

CharacterManager::CharacterManager() {
    m_characters = std::map<std::string, Character*>();
}

CharacterManager::~CharacterManager() {
    //delete m_characters;
}

void CharacterManager::LoadDataFile(std::string filename) {
    // TODO : load file content
}

void CharacterManager::CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos) {
    Character new_character(name); // Create a generic character...

    // ... and specialise it depending on the character type param
    switch (characterType)
    {
    case NPC:
        new_character = Npc(name);
        break;
    case ALIVE:
        new_character = AliveCharacter(name);
        break;
    default:
        new_character = Character(name);
        break;
    }

    // Set texture, position and add to the characters list
    new_character.SetTexture(textureFilename);
    new_character.SetPosition(pos);
    m_characters.insert({ name, &new_character });
}

void CharacterManager::CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos, std::map<std::string, Animation*> animations) {
    CreateCharacter(textureFilename, name, characterType, pos);
    m_characters[name]->SetAnimations(animations);
}

void CharacterManager::CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos, std::map<std::string, Animation*> animations, CharacterStats stats) {
    CreateCharacter(textureFilename, name, characterType, pos);
    m_characters[name]->SetAnimations(animations);
    //m_characters[name]->SetStats(stats);
}

void CharacterManager::Move(float value) {
    for each (std::pair<std::string, Character*> pair in m_characters) {
        Character* character = pair.second;
        character->Move(value);
    }
}

void CharacterManager::draw(sf::RenderTarget& target, sf::RenderStates states) const {
    for each (std::pair<std::string, Character*> pair in m_characters) {
        Character* character = pair.second;
        target.draw(*character);
    }
}

And finally the Main.cpp, where you can see in comments the things I tried without success :

#include "Map.h"
#include "CharacterManager.h"

int main()
{
    sf::RenderWindow window(sf::VideoMode(WINDOW_SIZE_X, WINDOW_SIZE_Y), WINDOW_TITLE);
    window.setFramerateLimit(WINDOW_FRAMERATE);

    Map map;
    int pos = WINDOW_SIZE_X / 2 - MAP_SIZE_X / 2;
    float movement = 0;
    map.SetPosition(pos);
    map.SetGroundTexture("Foreground/Tileset.png");
    map.SetBackgroundTexture("Background/BGFront.png");

    CharacterManager charManager;
    charManager.CreateCharacter("main", "Characters/test-character.png", ALIVE, sf::Vector2f(400, WINDOW_SIZE_Y - HEIGHT_OF_GROUND - 28));

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
            if (event.type == sf::Event::KeyPressed)
            {
                if (event.key.code == sf::Keyboard::Left)
                    movement = -MOVING_SPEED;
                else if (event.key.code == sf::Keyboard::Right)
                    movement = MOVING_SPEED;
            }
            else if (event.type == sf::Event::KeyReleased)
                movement = 0;
        }

        // Move the map
        map.Scroll(movement);
        //charManager.GetCharacter("main")->Move(movement);
        charManager.Move(movement);

        window.clear();
        window.draw(map);
        /*Character* mainPerso = charManager.GetCharacter("main");
        window.draw(*mainPerso);*/
        window.draw(charManager);
        window.display();
    }

    return 0;
}

The error I'm getting is on the return m_frames[m_currentFrame] line in Animation.cpp, in the end of the Play() function. A pop-up window opens saying : "Expression: vector subscript out of range". This error only happens the second time the code goes through this line. The first time it's called from the SetTexture() function of Character.cpp (m_sprite.setTextureRect(runningAnimation.Play())), itself called from the CreateCharacter() function of the CharacterManager (new_character.SetTexture(textureFilename)), and at this point the Animation object looks as it should.

But the second time, it's called from the Move() function of Character (m_sprite.setTextureRect(runningAnimation.Play())), itself called from the Move() function of the CharacterManager (character->Move(value)). And at this point, all of the Animation object absolutely doesn't look like it should. In debug mode, I can see this :

Debug screenshot

As I said earlier, I think the problem comes from the use of pointers. When I'm trying to remove them, the code runs, but I get a white square problem.

I tried to find some sort of tutorial on how to use this kind of architecture, but didn't find anything relevant. If you know one, I'll be glad to look at it.


回答1:


As I said earlier, I think the problem comes from the use of pointers. When I'm trying to remove them, the code runs, but I get a white square problem.

yep, it is a common issue for SFML when using Texture and Sprite when shallow copy is used.

Let's look at sf::Sprite::setTexture reference:

The texture argument refers to a texture that must exist as long as the sprite uses it. Indeed, the sprite doesn't store its own copy of the texture, but rather keeps a pointer to the one that you passed to this function. If the source texture is destroyed and the sprite tries to use it, the behavior is undefined.

So, a class like below, with default generated copy operation by compiler:

class Foo { 
  public:
    void setT() {
        // generate texture {t}
        s.setTexture(t);
    }
    sf::Sprite s;
    sf::Texture t;
};

will bring troubles to you. Because when a copy is made by Foo f(otherFoo);, sprite in newly created instance of Foo will have pointer to texture of otherFoo - it is shallow copy of pointer to sf::Texture. Deleting otherFoo will make a dangle pointer inside new constructed object.

In this case, you should implement assignment operation which makes deep copy of texture for sprite. If you don't know how to do it, you should mark assignment operations as deleted:

class Character : public sf::Drawable {
public:
    Character();
    Character(std::string name);
    ~Character();

    // added    
    Character& operator=(const Character&) = delete;
    Character(const Character&) = delete;

    void Move(float value);

Then, compiler will give you an error for each attempt of copying of Character instance.

In case of deleted copy operation, you should rely on pointers. Your attempt failed, because you store pointer to local variables. Local variables are deleted at the end of a function scope, and referring to them later is undefined behaviour.

You have to create Character by operator new:

void CharacterManager::CreateCharacter(std::string name, std::string textureFilename, CharacterType characterType, sf::Vector2f pos) {
    Character* new_character = new Character(name); // Create a generic character...

    //...
    // Set texture, position and add to the characters list
    new_character->SetTexture(textureFilename);
    new_character->SetPosition(pos);
    m_characters.insert({ name, new_character });
}


来源:https://stackoverflow.com/questions/60548077/how-to-use-a-manager-class-to-manage-characters-and-their-animations-in-c-usin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!