JohnDoe
JohnDoe

Reputation: 189

SFML and moving object to position

I got problem with my SFML board game. I got token which is moving around the board, board has 40 fields, every field has defined positionX and positionY. Yet sometimes, which happens completely random my token misses his target position, does additional round around the board and then stops.

class Field {
protected:  
    int m_positionX;
    int m_positionY;
public:
    //getters and setters
};

Every player has PositionID target position coordinates and CircleShape that is a token that causes troubles

class Player {
    sf::CircleShape m_token;
    int m_positionID = 0;       
    int m_targetPositionX;
    int m_targetPositionY;
public:
    //getters and setters
}

Then in my GameEngine class I got setInMotion() function which sets few variables for player, code of functions called here is irrelevant I guess, function names say it all

void GameEngine::setInMotion(int number) {  
        m_players[m_activePlayer].startMoving();
        m_players[m_activePlayer].incrementPositionID(number);                          
        m_players[m_activePlayer].setTargetPositionX(m_gameBoard.getField(m_players[m_activePlayer].getPositionID()).getPositionX());
        m_players[m_activePlayer].setTargetPositionY(m_gameBoard.getField(m_players[m_activePlayer].getPositionID()).getPositionY());
}

And then most important function that actually moves token around the board. #define TOKEN_SPEED 1000

void Player::moveForward(sf::Time dt) {
    if ((int)m_token.getPosition().x >= 240 && (int)m_token.getPosition().y >= 600) {
        this->m_token.move(-TOKEN_SPEED * dt.asSeconds(), 0);       
    }

    if ((int)m_token.getPosition().x <= 240 && (int)m_token.getPosition().y >= 40) {
        this->m_token.move(0, -TOKEN_SPEED * dt.asSeconds());    
    }

    if ((int)m_token.getPosition().x <= 800) && (int)m_token.getPosition().y <= 40) {
        this->m_token.move(TOKEN_SPEED * dt.asSeconds(), 0);    
    }

    if ((int)m_token.getPosition().x >= 800) && (int)m_token.getPosition().y <= 600) {
        this->m_token.move(0, TOKEN_SPEED * dt.asSeconds());        
    }

    if ((int)m_token.getPosition().x >= getTargetPositionX() - 1 && (int)m_token.getPosition().x <= getTargetPositionX() + 1 &&
        (int)m_token.getPosition().y >= getTargetPositionY() - 1 && (int)m_token.getPosition().y <= getTargetPositionY() + 1) {
        this->stopMoving();
    }
}

Token moves in 4 directions, direction depends on current position (reminder, its board game, it will be easier to understand what I did here) At the end, once current position equals target position, token stops moving. Now here is main problem, my token sometimes misses target position, does additonal run around the board and then stops at target position he missed. It happens completely random. It like sometimes application has not enough time to check last if statement that should stop the token. Slowing down TOKEN_SPEED helps, but then it moves just too slow. Thats another reason why my last if-statement looks like this, If I simply check for (int)m_token.getPosition().x == getTargetPositionX() it will almost always miss. Not converting float position value to int will make it even worse. At the end here is my update() function from gameLoop()

void GameState::update(sf::Time dt) {   

    if (m_gameEngine.getActivePlayer().isMoving()) { 
        m_gameEngine.getActivePlayer().moveForward(dt);         
    }
}

Upvotes: 0

Views: 2113

Answers (1)

Mario
Mario

Reputation: 36487

I think you're making it far more complicated than required. Your specific problem is most likely a timing issue: the time passed (dt) might be big enough for you to simply step over your target position (and the added +1 isn't enough to compensate). You can't avoid this unless you're implementing a fixed timestep movement (just look up that term, if you want to know more).

However, I'd implement the movement in a completely different way, which will also make the whole implementation easier and more dynamic.

First of all, I define my game board and its positions using a vector. If you want more complex paths like branches or shortcuts, you might want to define your own linked list or something similar.

Back to the playing field, in my example, I define the board by a sequence of position (each position representing one possible field where the player might stand):

std::vector<sf::Vector2f> board;
board.push_back({75, 75});
board.push_back({150, 50});
board.push_back({250, 50});
board.push_back({325, 75});
board.push_back({350, 150});
board.push_back({350, 250});
board.push_back({325, 325});
board.push_back({250, 350});
board.push_back({150, 350});
board.push_back({75, 325});
board.push_back({50, 250});
board.push_back({50, 150});

The player position itself – for simplicity I've got only one token – is just a floating point number representing the "index" of the current token position in the playing field:

float playerPos = 0.f;

Sounds odd? Yes, more or less. The integer part of the number represents the index, while the part behind the decimal point represents the actual position between fields. For example, a value of 3.75 means the token is at 75% between the fourth (index 3) and fifth (index 4) field.

Drawing the game board is rather trivial and I've simplified it to just drawing some circle shape for each field:

for (auto &pos : board) {
    field.setPosition(pos);
    window.draw(field);
}

Now for drawing the token, we'll have to determine the point before and after the token:

const sf::Vector2f playerPosLast = board[static_cast<std::size_t>(playerPos)];
const sf::Vector2f playerPosNext = board[static_cast<std::size_t>(playerPos + 1) % board.size()];

The casting will round down the values and in the second case I'm just making sure that we'll overflow back to the beginning of the board.

To determine the position between points, we'll just have to subtract the rounded position:

const float step = playerPos - static_cast<std::size_t>(playerPos);

While we now have everything to calculate the position of the token's visual representation, I'd like to add a small "jumping offset" to animate the path between the fields:

const sf::Vector2f jumpOffset(0, 25.f * (2.f * (step - .5)) * (2.f * (step - .5)) - 25.f);

The actual math behind this isn't important here, but since step ranges from 0 to 1, it's rather easy to make this a nice parabola like "path" (offset changes from 0 to -25 and back to 0 between points).

Now we just set the token's position using some vector math (determining a point between the previous and next field based on the calculated step and adding our jump offset):

player.setPosition(playerPosLast + (playerPosNext - playerPosLast) * step + jumpOffset);
window.draw(player);

But how to move the player? Pretty simple. If you want the player to move 4 fields forward, add 4 to playerPos. If you want to move 2 fields back, subtract 2. Just make sure to wrap back into the valid range of the board once you overflow behind the end.

If you compile and run the following demo, you'll end up with a simple window where a token jumps around from field to field indefinitely:

Demo Screenshot

Here's the full source code – uncommented, but most things should either be obvious or explained above:

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

int main()
{
    sf::RenderWindow window({480, 480}, "Board Game");

    std::vector<sf::Vector2f> board;
    board.push_back({75, 75});
    board.push_back({150, 50});
    board.push_back({250, 50});
    board.push_back({325, 75});
    board.push_back({350, 150});
    board.push_back({350, 250});
    board.push_back({325, 325});
    board.push_back({250, 350});
    board.push_back({150, 350});
    board.push_back({75, 325});
    board.push_back({50, 250});
    board.push_back({50, 150});

    sf::CircleShape field(25, 24);
    field.setFillColor(sf::Color::White);
    field.setOutlineColor(sf::Color::Black);
    field.setOutlineThickness(2);
    field.setOrigin({25, 25});

    sf::CircleShape player(20, 3);
    player.setFillColor(sf::Color::Red);
    player.setOutlineColor(sf::Color::Black);
    player.setOutlineThickness(2);
    player.setOrigin({20, 20});

    float playerPos = 0.f;

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            switch (event.type) {
                case sf::Event::Closed:
                    window.close();
                    break;
                default:
                    break;
            }
        }
        window.clear({0, 127, 127});
        for (auto &pos : board) {
            field.setPosition(pos);
            window.draw(field);
        }

        const sf::Vector2f playerPosLast = board[static_cast<std::size_t>(playerPos)];
        const sf::Vector2f playerPosNext = board[static_cast<std::size_t>(playerPos + 1) % board.size()];
        const float step = playerPos - static_cast<std::size_t>(playerPos);

        const sf::Vector2f jumpOffset(0, 25.f * (2.f * (step - .5)) * (2.f * (step - .5)) - 25.f);
        player.setPosition(playerPosLast + (playerPosNext - playerPosLast) * step + jumpOffset);
        window.draw(player);

        window.display();

        if ((playerPos += .001f) > board.size())
            playerPos -= board.size();
    }
}

Upvotes: 2

Related Questions