SFML 2D game – Smooth spaceship movement

2019-08-02 09:16发布

问题:

I am trying to make a simple game in C++ by using xcode as compiler and SFML as library. So far I have created a GUI, a background and a sprite (for the spaceship). I have also added arrow keys detection in order to be able to move the object, but the problem is that when i move the object it doesn't move smoothly and you can see it is kind of "jumping".

main.cpp

#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#include "Spaceship.hpp"
#include <vector>

// Here is a small helper for you! Have a look.
#include "ResourcePath.hpp"

int main(int, char const**)
{

    // Create the main window
    sf::RenderWindow window(sf::VideoMode(800, 600), "SpaceShuttle");
    window.setFramerateLimit(30);
    // Call to non-static member function without an object argument
    // Set the Icon
    sf::Image icon;
    if (!icon.loadFromFile(resourcePath() + "space-shuttle.png")) {
        return EXIT_FAILURE;
    }
    window.setIcon(icon.getSize().x, icon.getSize().y, icon.getPixelsPtr());

    // Load a sprite to display
    sf::Texture texture;
    if (!texture.loadFromFile(resourcePath() + "bg.png")) {
        return EXIT_FAILURE;
    }
    sf::Sprite sprite(texture);

    // Create a graphical text to display
    sf::Font font;
    if (!font.loadFromFile(resourcePath() + "sansation.ttf")) {
        return EXIT_FAILURE;
    }
    sf::Text text("SpaceShuttle K1LLM33K", font, 50);
    text.setFillColor(sf::Color::White);
    text.setPosition(100.0, 130.0);


    // Load a music to play
   /* sf::Music music; if (!music.openFromFile(resourcePath() + "nice_music.ogg")) { return EXIT_FAILURE; } 
    // Play the music
    music.play();
    */

    Spaceship spaceship(window);
    sf::Clock sf_clock;


    // Start the game loop
    while (window.isOpen()) {

        // Process events
        sf::Event event;
        while (window.pollEvent(event)) {
            // Close window: exit
            if (event.type == sf::Event::Closed) {
                window.close();
            }

            // Escape pressed: exit
            if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape) {
                window.close();
            }
            // Move Spaceship
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { spaceship.moveship('l'); }
            else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { spaceship.moveship('r'); }
            else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { spaceship.moveship('u'); }
            else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { spaceship.moveship('d'); }

        }
        // Clear screen
        window.clear();

        // Draw the sprite(s)
        window.draw(sprite);
        spaceship.drawsprite(window);

        // Draw the string(s)
        window.draw(text);

        // Update the window
        window.display();
    }

    return EXIT_SUCCESS;
}

Spaceship.cpp

#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#include "ResourcePath.hpp"
#include "Spaceship.hpp"

Spaceship::Spaceship(sf::RenderWindow& game_window){
    auto surface = game_window.getSize();
    ss_x = surface.x/2;
    ss_y = surface.y/2;
    ss_speed = 5;
    ss_width = 128;
    ss_height = 128;
    ss_radius = ss_width/2;

}
void Spaceship::drawsprite(sf::RenderWindow& game_window){
    sf::Texture ship;
    if (!ship.loadFromFile(resourcePath() + "space-shuttle-64.png")) {
        return EXIT_FAILURE;
    }
    sf::Sprite ss_sprite(ship);
    ss_sprite.setPosition(ss_x - ss_sprite.getGlobalBounds().width/2, ss_y - ss_sprite.getGlobalBounds().height/2);
    game_window.draw(ss_sprite);
}

void Spaceship::moveship(char move){
    if(move == 'l'){ ss_x -= ss_speed;  }
    else if(move == 'r'){ ss_x += ss_speed; }
    else if(move == 'u'){ ss_y -= ss_speed; }
    else if(move == 'd'){ ss_y += ss_speed; }
}

Spaceship::~Spaceship(){}

Spaceship.hpp

#ifndef Spaceship_hpp
#define Spaceship_hpp
#include <iostream>
#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#include <stdio.h>

using namespace std;

class Spaceship {
public:
    Spaceship();
    Spaceship(sf::RenderWindow&);
    ~Spaceship();
    void moveship(char);
    void drawsprite(sf::RenderWindow&);
private:
    signed int ss_x, ss_y;
    unsigned int ss_speed;
    int ss_width, ss_height, ss_radius;

};

#endif /* Spaceship_hpp */

回答1:

As it has been suggested in the comments, the problem here is that you are not taking into account the elapsed time between two frames in your computations. Hence what happens is that you are adding a fixed amount of velocity at any frame, ignoring the fact that two consecutive frames can need a very different time to finish.

There is another problem (which appears to be the main source of your issues): you are checking if keys are pressed inside the event loop. This is not correct, this will be true only if your key is pressed AND there have been other events during the loop. You need to check this out at every frame.

Another way to tackle the last issue is to have boolean flags that you set to true / false when you detect a press / release for the keys you want to test. If you have a look at the isKeyPressed method, you will also find out that this second method is ways more efficient than the first one.

The simplest way to change your main loop to get the elapsed time at every frame, according to the docs is something like this:

sf::Clock sf_clock;

// Start the game loop
while (window.isOpen()) {
    // Get time elapsed since last frame
    float dt = clock.restart().asSeconds();

    // Process events
    sf::Event event;
    while (window.pollEvent(event)) {
        // Close window: exit
    }

    // Move Spaceship, this must be done outside of the pollEvent loop !
         if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) spaceship.moveship(dt, 'l');
    else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) spaceship.moveship(dt, 'r'); 
         if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) spaceship.moveship(dt, 'u');
    else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) spaceship.moveship(dt, 'd');

    }
    // Draw, etc..
}

Then you have to take dt into account in moveship method (I changed your if / else if in switch case, which I find cleaner in this case and should be more efficient):

void Spaceship::moveship(float dt, char move) {
    switch (move) {
        case 'l': ss_x -= dt * ss_speed_x; break;
        case 'r': ss_x += dt * ss_speed_x; break;
        case 'u': ss_y -= dt * ss_speed_y; break;
        case 'd': ss_y += dt * ss_speed_y; break;
    }
}

As suggested by Jesper Juhl, I did some integration here (and you should really have a look at the article).

BTW, I would suggest some modifications in your code:

// First of all, you should use floating-point values that you convert 
// in screen space at render time for your coordinates
// You could also think about using vectors instead, I won't here
class Spaceship {
private:
    float ss_x, ss_y;
    float ss_speed_x, ss_speed_y;

    // You should also store your sprite instead of creating it over
    // and over again
    sf::Sprite ss_sprite;
};


Spaceship::Spaceship(sf::RenderWindow& game_window) {
    // You can then take those modifications into account
    // in your constructor:
    auto surface = game_window.getSize();
    ss_x = ss_y = 0.5f;
    ss_speed_x = 5.f / surface.x;
    ss_speed_y = 5.f / surface.y;
    ss_width = 128;
    ss_height = 128;
    ss_radius = ss_width/2;

    sf::Texture ship;
    if (!ship.loadFromFile(resourcePath() + "space-shuttle-64.png")) {
        // This is really an awful way to handle an error, but I won't
        // go into details here. A better way would be to have an Init()
        // method that returns an error code on failure for example.
        exit(EXIT_FAILURE);
    }

    ss_sprite = sf::Sprite(ship);
    // http://www.sfml-dev.org/documentation/2.4.1/classsf_1_1Transformable.php#a56c67bd80aae8418d13fb96c034d25ec
    ss_sprite.setOrigin(ss_width / 2, ss_height / 2);
}

// Finally, you reflect those modifications in your draw code
void Spaceship::drawsprite(sf::RenderWindow& game_window){
    auto size = game_window.getSize();
    ss_sprite.setPosition(ss_x * size.x, ss_y * size.y);
    game_window.draw(ss_sprite);
}