Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Problem with smooth movement in 2d game (scrolling view, render interpolation)  (Read 16703 times)

0 Members and 1 Guest are viewing this topic.

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
Hello.

I use SFML 2.2 from the github repo (the current version).
I program a 2d prototype game (2d mario style where the camera follows the player) , and am experiencing the buggy imagery during rendering, for example a chunk of vertex array blocks are misplaced which gives an awful visual/glitches. I've read several game-related articles regarding game loop, time step, etc and I *do* understand what's going on, but cannot spot the bug.

What I do: perform scene updates with fixed UPDATE_STEP value (it then gets used when calculating, for example, the Player's new position, being applied to the velocity, etc).
Then I use the remained slice of time since the last update divided by UPDATE_STEP as a ratio (0..1) to perform the interpolation during rendering.

Basically I update the position of my Player and calibrate the Camera accordingly, all the rest of the game scene (tile map represented as a vertex array) remains untouched.

Could you help me to figure out where's my mistake?
Here we go:

void gameLoop() {
    const sf::Time UPDATE_STEP = sf::seconds(1.0f / 120.0f);
    sf::Time timeSinceLastUpdate = sf::Time::Zero;
    _renderWindow.setFramerateLimit(60);
    sf::Clock clock;

    while (_renderWindow.isOpen()) {

        sf::Event event;
        while (_renderWindow.pollEvent(event)) {
         
            _curGameScene->processEvent(event);
           
            switch (event.type) {
            case sf::Event::KeyPressed:
                switch (event.key.code) {
                case sf::Keyboard::Escape:                
                    _renderWindow.close();
                    break;                
                default:
                    break;
                }
                break;
            case sf::Event::Closed:
                _renderWindow.close();
                break;
            default:
                break;
            }
        }

                _curGameScene->processKeyboard();

                sf::Time dt = clock.restart();
                timeSinceLastUpdate += dt;
               
                if (timeSinceLastUpdate > sf::seconds(0.25)) {
                        timeSinceLastUpdate = sf::seconds(0.25);
                }

                while (timeSinceLastUpdate >= UPDATE_STEP) {

                        _curGameScene->update(UPDATE_STEP.asSeconds());
                        timeSinceLastUpdate -= UPDATE_STEP;
                }

        //RATIO FOR INTERPOLATION DURING RENDER
        const float alpha = timeSinceLastUpdate.asSeconds() / UPDATE_STEP.asSeconds();
       
        //normal draw
        _renderWindow.setView(_renderWindow.getDefaultView());
        _curGameScene->render(_renderWindow, sf::RenderStates::Default, alpha);
       
        //ortho draw
        _renderWindow.setView(_renderWindow.getDefaultView());
        _renderWindow.draw(infoText);
        _curGameScene->renderOrtho(_renderWindow);

        _renderWindow.display();
    }
}
 

my _curGameScene has the player sprite entity, the camera, which follows him, and the tile map (implemented as a vertex array).
so, in the update() method I do:
void LevelScene::update(float dtExpandedInSeconds) {

    _player->update(dtExpandedInSeconds);      
}
 

player's update method is as follows:
virtual void update(float dtExpandedInSeconds) {
       
    //store the previous position of the sprite
    _prevPos = _pos;

    //calculate the new position of the sprite
    _pos.x = _pos.x + _vxy.x*dtExpandedInSeconds;
}
 

then when we invoke the LevelScene's render method:
void LevelScene::render(sf::RenderTarget &target, sf::RenderStates renderStates, float ratio) {

    //CALCULATED POSITION OF THE CAMERA TAKING INTO ACCOUNT THE RENDER RATIO
    const sf::Vector2f viewCenter = _camera->calcPos(ratio);

    sf::View view = target.getView();
    if (_useZoom) {
        view.zoom(0.5);
    }
    view.setCenter(viewCenter);
    target.setView(view);
    target.clear(_bgColor);

    _tileMap->render(target, renderStates, ratio);
    _player->render(target, renderStates, ratio);
}
 

which involves the Camera's calcPos method:
const sf::Vector2f &calcPos(float ratio) {

    sf::Vector2f followPos = _sprite2Follow->getPos();
    sf::Vector2f followPrevPos = _sprite2Follow->getPrevPos();

    //HERE COMES THE INTERPOLATION
    _calcPos.x = followPrevPos.x*ratio + followPos.x*(1.0-ratio);
    _calcPos.y = followPrevPos.y*ratio + followPos.y*(1.0-ratio);

    if (_calcPos.x <= _halfViewWH.x) {
        _calcPos.x = 0;
    } else if (_calcPos.x <= _mapWH.x - _halfViewWH.x) {
        _calcPos.x = _calcPos.x - _halfViewWH.x;
    } else {
        _calcPos.x = _mapWH.x - _viewWH.x;
    }

    if (_calcPos.y <= _halfViewWH.y) {
        _calcPos.y = 0;
    } else if (_calcPos.y <= _mapWH.y - _halfViewWH.y) {
        _calcPos.y = _calcPos.y - _halfViewWH.y;
    } else {
        _calcPos.y = _mapWH.y - _viewWH.y;
    }

    _calcPos.x += _halfViewWH.x;
    _calcPos.y += _halfViewWH.y;

    return _calcPos;
}
 

The Player's render method:
virtual void render(sf::RenderTarget &target, sf::RenderStates renderStates, float ratio) {

    _sprite.setPosition(_prevPos.x*ratio + _pos.x*(1.0-ratio), _prevPos.y*ratio + _pos.y*(1.0-ratio));
    target.draw(_sprite, renderStates);
}
 

I have been debugging half a day but the bug is still here. Maybe you have some ideas about what is wrong with my approach? Thank you.
« Last Edit: January 20, 2015, 01:06:16 pm by grok »

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
UPDATED: I have slightly better results when I do not take the remained part of time slice into consideration and perform an additional update with that part, i.e.:

void gameLoop() {

    //... as above

    _curGameScene->processKeyboard();

    sf::Time dt = clock.restart();
    timeSinceLastUpdate += dt;

    if (timeSinceLastUpdate > sf::seconds(0.25)) {
        timeSinceLastUpdate = sf::seconds(0.25);
    }

    while (timeSinceLastUpdate >= UPDATE_STEP) {

        _curGameScene->update(UPDATE_STEP.asSeconds());
        timeSinceLastUpdate -= UPDATE_STEP;
        ++updatesCnt;
    }

    //ADDED
    _curGameScene->update(timeSinceLastUpdate.asSeconds());
    timeSinceLastUpdate = sf::Time::Zero;

    _renderWindow.setView(_renderWindow.getDefaultView());
    _curGameScene->render(_renderWindow, sf::RenderStates::Default); //NO ALPHA IS PASSED
    //ortho draw
    _renderWindow.setView(_renderWindow.getDefaultView());
    _renderWindow.draw(infoText);
    _curGameScene->renderOrtho(_renderWindow);
    //
}
 
« Last Edit: January 20, 2015, 02:17:36 pm by grok »

Hapax

  • Hero Member
  • *****
  • Posts: 3367
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Just to warn you, you're likely to hear "complete and minimal example" soon. It's very unlikely that someone is going to look through all of this and follow how its all linked etc.. Maybe someone will get bored or fancy a challenge. You may be in luck; who knows?

However, going by your description, it's possible that the "glitches" are because your tiles are not being positioned at integer locations. This reduces smoothness in motion, though, so it's a trade-off.
A screenshot/video of the glitch would help us to know what problem is looking to be solved, especially if it's not what I just described.
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
Thanks for the reply, but no, it didn't help. I rechecked that the coodinates are integral (they're floats, but rounded, like: 32.0, 64.0, etc).

As for complete and minimal example, here it is:
#include <iostream>

#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/System/Vector2.hpp>

enum E_MOVE_TYPE {
    LEFT, RIGHT, IDLE
};

struct Entity {
    sf::RectangleShape shape;
    sf::Vector2f pos;
    sf::Vector2f prevPos;
    bool moveKeyHold;
    E_MOVE_TYPE stepX;
    sf::Vector2f accxy, vxy, maxVxy;

    void update(float dt) {

        //save the previous pos
        prevPos = pos;

        if (moveKeyHold) {
            float accX = accxy.x * dt;
            if (stepX == E_MOVE_TYPE::RIGHT) {
                vxy.x += accX;
            } else if (stepX == E_MOVE_TYPE::LEFT) {
                vxy.x -= accX;
            }
        } else {
            float accX = accxy.x * dt;
            if (vxy.x > 0) {
                vxy.x -= accX;
                if (vxy.x < 0) {
                    vxy.x = 0;
                }
            } else if (vxy.x < 0) {
                vxy.x += accX;
                if (vxy.x > 0) {
                    vxy.x = 0;
                }
            }
        }

        if (vxy.x > maxVxy.x) {
            vxy.x = maxVxy.x;
        } else if (vxy.x < -maxVxy.x) {
            vxy.x = - maxVxy.x;
        }

        //update the current position
        pos.x = pos.x + vxy.x*dt;
    }

    void render(sf::RenderWindow *window, sf::RenderStates states, float ratio) {
        shape.setPosition(pos.x*ratio + prevPos.x*(1.0-ratio), pos.y*ratio + prevPos.y*(1.0-ratio));
        window->draw(shape, states);
    }
};

struct Camera {
    Entity *player;
    sf::Vector2f viewWH, halfViewWH;
    sf::Vector2u mapWH;
    sf::Vector2u screenWH;

    sf::Vector2f calcPos(float ratio) {

        sf::Vector2f followPos = player->pos;
        sf::Vector2f followPrevPos = player->prevPos;

        sf::Vector2f result;

        result.x = followPos.x*ratio + followPrevPos.x*(1.0f-ratio);
        result.y = followPos.y*ratio + followPrevPos.y*(1.0f-ratio);

        if (result.x <= halfViewWH.x) {
            result.x = 0;
        } else if (result.x <= mapWH.x - halfViewWH.x) {
            result.x = result.x - halfViewWH.x;
        } else {
            result.x = mapWH.x - viewWH.x;
        }

        if (result.y <= halfViewWH.y) {
            result.y = 0;
        } else if (result.y <= mapWH.y - halfViewWH.y) {
            result.y = result.y - halfViewWH.y;
        } else {
            result.y = mapWH.y - viewWH.y;
        }

        result.x += halfViewWH.x;
        result.y += halfViewWH.y;

        return result;
    }
};

//GLOBAL VARIABLES
Entity player;
Camera camera;
sf::VertexArray *pTileMap;

////////
void updateLogic(float dt) {
    player.update(dt);
}

void render(sf::RenderWindow *window, float ratio) {

    sf::Vector2f viewCenter = camera.calcPos(ratio);
    sf::View view = window->getView();
    view.setCenter(viewCenter);
    window->setView(view);

    window->clear(sf::Color::Black);
    window->draw(*pTileMap, sf::RenderStates::Default);
    player.render(window, sf::RenderStates::Default, ratio);
}

void initializeTileMap(const sf::Vector2u &levelWH) {

    int maxX = int(levelWH.x/32) - 1;
    int maxY = int(levelWH.y/32) - 1;

    pTileMap = new sf::VertexArray();
    pTileMap->setPrimitiveType(sf::PrimitiveType::Quads);

    bool isYellowColor = false;
    for (int y = 0; y <= maxY; ++y) {
        for (int x = 0; x <= maxX; ++x) {
            if ((y >= 1 && x <= 50) || (x >= 60 && x <= 70 && y >= 20 && y <= 120) || (x == maxX)) {
                 sf::Vertex v;

                 v.position = sf::Vector2f {static_cast<float>(x * 32), static_cast<float>(levelWH.y - (y+1)*32)};
                 v.color = isYellowColor ? sf::Color::Yellow : sf::Color::Green;
                 pTileMap->append(v);

                 v.position = sf::Vector2f {static_cast<float>(x * 32), static_cast<float>(levelWH.y - (y)*32)};
                 v.color = isYellowColor ? sf::Color::Yellow : sf::Color::Green;
                 pTileMap->append(v);

                 v.position =  sf::Vector2f {static_cast<float>((x + 1) * 32), static_cast<float>(levelWH.y - (y)*32)};
                 v.color = isYellowColor ? sf::Color::Yellow : sf::Color::Green;
                 pTileMap->append(v);

                 v.position = sf::Vector2f {static_cast<float>((x + 1) * 32), static_cast<float>(levelWH.y - (y+1)*32)};
                 v.color = isYellowColor ? sf::Color::Yellow : sf::Color::Green;
                 pTileMap->append(v);

                 isYellowColor = !isYellowColor;
            }
        }
    }
}

void deallocateTileMap() {
    delete pTileMap;
}

void gameLoop(sf::RenderWindow *window) {

    const sf::Time UPDATE_STEP = sf::seconds(1.0f / 120.0f);
    sf::Time timeSinceLastUpdate = sf::Time::Zero;

    sf::Clock clock;
    while (window->isOpen()) {
        sf::Event event;
        while (window->pollEvent(event)) {
            switch (event.type) {
            case sf::Event::KeyPressed:
                switch (event.key.code) {
                case sf::Keyboard::Escape:
                    window->close();
                    break;
                case sf::Keyboard::Left:
                    player.moveKeyHold = true;
                    player.stepX = E_MOVE_TYPE::LEFT;
                    break;
                case sf::Keyboard::Right:
                    player.moveKeyHold = true;
                    player.stepX = E_MOVE_TYPE::RIGHT;
                    break;
                default:
                    break;
                }
                break;
            case sf::Event::KeyReleased:
                player.moveKeyHold = false;
                player.stepX = E_MOVE_TYPE::IDLE;
                break;
            case sf::Event::Closed:
                window->close();
                break;
            default:
                break;
            }
        }

        //update logic
        sf::Time dt = clock.restart();
        timeSinceLastUpdate += dt;

        if (timeSinceLastUpdate > sf::seconds(0.25)) {
            timeSinceLastUpdate = sf::seconds(0.25);
        }

        while (timeSinceLastUpdate >= UPDATE_STEP) {
            updateLogic(UPDATE_STEP.asSeconds());
            timeSinceLastUpdate -= UPDATE_STEP;
        }

        const float alpha = timeSinceLastUpdate.asSeconds() / UPDATE_STEP.asSeconds();

        //draw logic
        render(window, alpha);

        //display now!
        window->display();
    }
}

int main() {

    sf::RenderWindow window;
    sf::VideoMode videoMode = sf::VideoMode::getDesktopMode();

    window.create(videoMode, "test smooth movement", sf::Style::Default, sf::ContextSettings {});
    window.setFramerateLimit(60);

    sf::Vector2u levelWH(1024*3, videoMode.height);

    player.pos.x = 1000;
    player.pos.y = 400;
    player.prevPos = player.pos;
    player.moveKeyHold = false;
    player.stepX = E_MOVE_TYPE::IDLE;
    player.shape.setSize(sf::Vector2f(40, 40));
    player.shape.setPosition(player.pos);
    player.shape.setFillColor(sf::Color::Red);
    player.vxy = sf::Vector2f(50, 0);
    player.accxy = sf::Vector2f(200, 0);
    player.maxVxy = sf::Vector2f(700, 225);

    sf::Vector2u screenWH(videoMode.width, videoMode.height);
    sf::Vector2f viewWH(static_cast<float>(screenWH.x), static_cast<float>(screenWH.y));

    camera.player = &player;
    camera.mapWH = levelWH;
    camera.viewWH = viewWH;
    camera.halfViewWH = sf::Vector2f(viewWH.x/2, viewWH.y/2);
    camera.screenWH = screenWH;

    initializeTileMap(levelWH);

    gameLoop(&window);

    deallocateTileMap();

    return 0;
}
 

please compile it:
Quote
g++ -o buggy ./main.cpp -lsfml-graphics -lsfml-window -lsfml-system -std=c++11
and run it:
Quote
./buggy

Can you spot some visual glitches when the red box (i.e. the player) moves? The movement commands are arrows: left/right. Please press left/right for a while, and write back.
An Escape button closes the application.

Here's the video (taken from the smartphone) presenting the glitches I refer to:
https://www.dropbox.com/s/w038trdfqm55gjl/buggy.mp4?dl=0
(beware, it is ~ 30 MB).

Thank you.
« Last Edit: January 20, 2015, 05:49:56 pm by grok »

Hapax

  • Hero Member
  • *****
  • Posts: 3367
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Firstly, this is not really "minimal".

Secondly, I executed it and it doesn't seem to have any problems.

There do seem to be weird things happening to the red square when it moves without the camera. This is, however, not a glitch but an optical illusion. I you wish to test this, take a screen shot of the red square near the middle, load it into Photoshop or similar, and move the image. The same seemingly-overlapping dark areas appear there too.
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
Hapax, thank you. I really appreciate your help.
Yes, I know what you're talking about while saying about an optical illusion, yes, it is ok.
But I do see visual problems of another kind related to the edges of yellow/green squares.
It can be seen in the video I supplied.

Probably it is the Nvidia Video Tearing explained here:
http://askubuntu.com/questions/125245/how-do-i-stop-video-tearing-nvidia-prop-driver-non-compositing-window-manager

Btw, what OS are you running on? Can the video driver be the cause of the problem?
« Last Edit: January 20, 2015, 06:06:15 pm by grok »

Hapax

  • Hero Member
  • *****
  • Posts: 3367
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
It could be a driver problem; who knows? Make sure you're using the very latest version  :)

Just saw your video. It's a massive tear, isn't it?!
Have you tried using vsync?

p.s. If you use vsync (setVerticalSyncEnabled(true)), remember to stop using setFramerateLimit().
« Last Edit: January 20, 2015, 06:16:30 pm by Hapax »
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
huh, yes, it is related to the Nvidia tearing. it behaves even worse when the application runs in the fullscreen mode.
Hapax,
could you test out that my example works fine with fullscreen mode, i.e.:
int main() {
     sf::VideoMode videoMode = sf::VideoMode::getFullscreenModes().front();

    window.create(videoMode, "test smooth movement", sf::Style::Fullscreen, sf::ContextSettings {});
    ..//
 

I am very curious to compare your results with mine.
Again, thanks for your patience ;)

Hapax

  • Hero Member
  • *****
  • Posts: 3367
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Yes, this causes tears when run on my computer. As mentioned earlier, however, it simply requires:
        window.create(videoMode, "test smooth movement", sf::Style::Fullscreen, sf::ContextSettings{});
        window.setVerticalSyncEnabled(true);
        //window.setFramerateLimit(60);
to fix it.

EDIT: It's not perfect, actually; it seems there is a slight fringing where the yellow meets the black but that could be my monitor. Unfortunately, I can't go full-screen on my other monitor yet. Still waiting for SFML's ability to do that (hint hint! ;)). That said, if you're still having identical problems after enabling vertical sync, you should check your drivers to make sure that v-sync can be controlled by applications.
« Last Edit: January 20, 2015, 07:40:50 pm by Hapax »
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
Tried to enable vertical sync and disable frame limit, but the bug is still here :-/
is that 100% video drivers bug? I am asking because I do not experience that bug in the other OpenGL applications like glxgears for instance (while rotating the gears).
« Last Edit: January 21, 2015, 06:52:00 am by grok »

Jesper Juhl

  • Hero Member
  • *****
  • Posts: 1405
    • View Profile
    • Email
Your driver could be configured to not allow applications to control vsync. Make sure your driver allows your app to enable/disable vsync and does not force it on/off.
Ohh and make sure it (the driver) is up-to-date.
« Last Edit: January 21, 2015, 11:33:08 pm by Jesper Juhl »

Hapax

  • Hero Member
  • *****
  • Posts: 3367
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
@Jesper : what I said???

(joke :P)
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
In nvidia-settings I check/uncheck "VSYNC" but it makes no difference, that's my problem.
Moreover, I tried to explicitly set setVerticalSyncEnabled(true) (using driver's VSYNC on/off), but it is pointless anyway.

a bit more information about my video card, graphics, openGL renderer etc:
Quote
inxi -Gxx

Graphics:  Card: NVIDIA G71 [GeForce 7900 GS] bus-ID: 01:00.0 chip-ID: 10de:0292
           X.Org: 1.15.1 drivers: nvidia (unloaded: fbdev,vesa,nouveau) Resolution: 1680x1050@59.9hz
           GLX Renderer: GeForce 7900 GS/PCIe/SSE2 GLX Version: 2.1.2 NVIDIA 304.125 Direct Rendering: Yes


I am desperate.

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 10911
    • View Profile
    • development blog
    • Email
How many other OpenGL applications which do similar movements have you tested?
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

grok

  • Jr. Member
  • **
  • Posts: 67
    • View Profile
    • Email
no one. ;)
But I don't get what's wrong with my code. if there's a bug, I cannot trace it.