如何使用管理器 class 使用 SFML 库在 C++ 中管理角色及其动画

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

我正在尝试创建一个 2D sidescroller 迷你游戏。现在,我只有一个带有精灵和一个动画的角色,我正在尝试使用 left/right 箭头移动它。起初,我只有一个 Character class,存储角色的 sprite 及其 运行 动画。它奏效了。但是现在,我正在尝试添加一个 CharacterManager class,它将创建所有的角色以避免在主体中这样做,并且它将管理他们的动作并绘制它们。

它不再起作用了。我认为我的问题来自于我在使用指针时遇到问题,我不太熟悉。

这是我正在使用的不同 classes :

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);
    }
}

最后是 Main.cpp,您可以在评论中看到我尝试过但没有成功的事情:

#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;
}

我遇到的错误是 Animation.cpp 中的 return m_frames[m_currentFrame] 行,在 Play() 函数的末尾。弹出 window 打开说:"Expression: vector subscript out of range"。此错误仅在代码第二次通过此行时发生。第一次是从 Character.cpp (m_sprite.setTextureRect(runningAnimation.Play())) 的 SetTexture() 函数中调用的,它本身是从 CharacterManager (new_character.SetTexture(textureFilename)) 的 CreateCharacter() 函数中调用的,此时 Animation对象看起来应该如此。

但是第二次,它是从 Character (m_sprite.setTextureRect(runningAnimation.Play())) 的 Move() 函数调用的,它本身是从 CharacterManager (character->Move(value)) 的 Move() 函数调用的。在这一点上,所有的动画对象看起来都不像它应该的那样。在调试模式下,我可以看到:

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.

是的,这是 SFML 使用 TextureSprite 使用浅拷贝时的常见问题。

我们看一下sf::Sprite::setTexture参考:

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.

因此,class 如下所示,编译器默认生成复制操作:

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

会给你带来麻烦。因为当 Foo f(otherFoo); 创建副本时,新创建的 Foo 实例中的精灵将具有指向 otherFoo 纹理的指针 - 它是指向 sf::Texture 指针的浅拷贝。删除 otherFoo 将在新构造的对象中创建一个悬挂指针。

在这种情况下,您应该实现赋值操作,为精灵制作深拷贝纹理。如果你不知道怎么做,你应该将赋值操作标记为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);

然后,每次尝试复制 Character 个实例时,编译器都会给您一个错误。

如果删除复制操作,你应该依靠指针。您的尝试失败了,因为您存储了指向局部变量的指针。局部变量在函数范围的末尾被删除,以后引用它们是未定义的行为。

您必须通过运算符 new:

创建 Character
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 });
}