Как использовать класс Manager для управления персонажами и их анимацией в C ++ с использованием библиотеки SFML - PullRequest
0 голосов
/ 05 марта 2020

Я пытаюсь создать 2D мини-игру с боковым скроллером. Пока у меня есть только персонаж со спрайтом и одной анимацией, которую я пытаюсь переместить с помощью стрелок влево / вправо. Сначала у меня был только класс Character, в котором был спрайт персонажа и его анимация. И это сработало. Но сейчас я пытаюсь добавить класс CharacterManager, который будет создавать всех персонажей, чтобы не делать это в основном, и который будет управлять их движениями и рисовать их.

И он больше не работает , Я думаю, что мои проблемы связаны с тем, что у меня возникают проблемы с использованием указателей, с которыми я не очень хорошо знаком.

Вот различные классы, которые я использую:

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

Анимация. 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, где вы можете увидеть в комментариях то, что я пробовал без 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;
}

Ошибка, которую я получаю, находится в строке return m_frames[m_currentFrame] в анимации. cpp, в конце функции Play (). Откроется всплывающее окно со словами: «Выражение: векторный индекс вне диапазона». Эта ошибка возникает только во второй раз, когда код проходит через эту строку. Первый раз он вызывается из функции SetTexture () Character. cpp (m_sprite.setTextureRect(runningAnimation.Play())), сам вызывается из функции CreateCharacter () CharacterManager (new_character.SetTexture(textureFilename)), и в этот момент объект Animation выглядит так, как он должен.

Но во второй раз он вызывается из функции Move () персонажа (m_sprite.setTextureRect(runningAnimation.Play())), сам вызывается из функции Move () CharacterManager (character->Move(value)). И в этот момент весь объект Animation совершенно не выглядит так, как должен. В режиме отладки я вижу это:

Отладочный скриншот

Как я уже говорил ранее, я думаю, что проблема связана с использованием указателей. Когда я пытаюсь удалить их, код запускается, но у меня возникает проблема белого квадрата.

Я пытался найти какое-то руководство по использованию архитектуры такого типа, но не нашел ничего подходящего. Если вы знаете один, я буду рад взглянуть на него.

1 Ответ

0 голосов
/ 05 марта 2020

Как я уже говорил ранее, я думаю, что проблема заключается в использовании указателей. Когда я пытаюсь удалить их, код запускается, но у меня возникает проблема белого квадрата.

Да, это общая проблема для SFML при использовании Texture и Sprite при использовании мелкой копии.

Давайте рассмотрим sf :: Sprite :: setTexture ссылка:

Аргумент текстуры ссылается на текстуру это должно существовать, пока спрайт использует его. Действительно, спрайт не хранит свою собственную копию текстуры, а хранит указатель на тот, который вы передали этой функции. Если исходная текстура уничтожена и спрайт пытается ее использовать, поведение не определено.

Итак, класс, подобный приведенному ниже, с сгенерированной по умолчанию операцией копирования компилятором:

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 в новом построенном объекте будет висящий указатель.

В этом случае вам следует реализовать операцию присваивания, которая делает глубокую копию текстуры для спрайта. Если вы не знаете, как это сделать, вы должны пометить операции присваивания как удалено :

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 instance.

В случае удаленного копирования вы должны полагаться на указатели. Ваша попытка не удалась, потому что вы храните указатель на локальные переменные. Локальные переменные удаляются в конце области действия функции, и последующее обращение к ним - неопределенное поведение.

Вы должны создать Character с помощью оператора 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 });
}
...