Search code examples
c++sdlgame-physicsgame-development

Slow or no movement in specific directions when attempted Frame-Independent Movement with SDL/C++


I am attempting to make a simple game of snake and have begun with trying to get an object to move on screen independent of the framerate. My issue is that I am getting movement when I set the velocity to a negative value to move the object either left or up on the screen. However, when I attempt to move right or down, which involves positive values, I get little to no movement on the object. If I set the values significantly higher then yes, the object moves right and down but the fact that I am getting a difference in movement speed between a positive and negative value is odd to me.

Here is my code. I have only attempted to get an object to move so far, so there are the odd one or two functions there that aren't defined or need work. I am only concerned about my movement issues for the time being.

Snake.h

    #pragma once
    #include <vector>
    #include <SDL.h>
    #include <iostream>

    typedef SDL_Rect snakeSegment;

    enum orientation { ORIENTATION_LEFT = 0, ORIENTATION_UP = 1, ORIENTATION_RIGHT = 2, 
    ORIENTATION_DOWN = 3 };


   class Snake
   {
   private:

        snakeSegment head;


        std::vector<snakeSegment> segments;

        SDL_Surface* snakeSurface;
        SDL_Texture* snakeTexture;

        const char* filepath = "assets\/sprites\/snake_body.bmp";



    public:



        Snake();

        void init(SDL_Renderer* renderer);

        void move(float delta);
        void appendSegment();
        void render(SDL_Renderer* renderer);



        int orientation;





    };

Snake.cpp

    #include "Snake.h"


    Snake::Snake()
    {
    
    } 


    void Snake::init(SDL_Renderer *renderer)
    {
        snakeSurface = SDL_LoadBMP(filepath);
        if (snakeSurface == nullptr)
        {
            throw(SDL_GetError());
        }
        else {
           snakeTexture = SDL_CreateTextureFromSurface(renderer, snakeSurface);
            if (snakeTexture == nullptr)
            {
                SDL_FreeSurface(snakeSurface);
                throw(SDL_GetError());
            }
            SDL_FreeSurface(snakeSurface);
        }

        SDL_Window* w = SDL_RenderGetWindow(renderer);



        int width;
        int height;

        SDL_GetWindowSize(w, &width, &height);

        head.w = 16;
        head.h = 32;
        head.y = height / 2 - (head.h / 2);
        head.x = width / 2 - (head.w / 2);

        orientation = ORIENTATION_UP;
     }


    void Snake::render(SDL_Renderer* renderer)
    {
        SDL_RenderCopy(renderer, snakeTexture, nullptr, &head);
    }


    void Snake::move(float delta)
    {
        float positionYDelta = 0;
        float positionXDelta = 0;


        switch (orientation) {
        case ORIENTATION_UP:
            positionYDelta = -3 * delta;
            positionXDelta = 0;
            break;
        case ORIENTATION_DOWN:
            positionYDelta = 3 * delta; 
            positionXDelta = 0;
            break;

        case ORIENTATION_RIGHT:
            positionYDelta = 0;
            positionXDelta = 3 * delta;
            break;

       case ORIENTATION_LEFT:
           positionYDelta = 0;
           positionXDelta = -3 * delta;
           break;
    }
        head.y += positionYDelta;
        head.x += positionXDelta;


    }

    void Snake::appendSegment()
    {

    }

main.cpp:

    #include <iostream>
    #include <SDL.h>
    #include "Snake.h"




int main(int argc, char* args[])
{

    if (SDL_Init(SDL_INIT_EVERYTHING) != 0)
    {
        std::cout << "failed to initialize" << std::endl;
    }
    SDL_Window* w = SDL_CreateWindow("Snake Clone", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN);

    SDL_Renderer* ren = SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    Snake snake;

    try {
        snake.init(ren);
    }
    catch (std::exception e)
    {
        std::cout << e.what();
    }

    SDL_Event e;
    

    bool quit = false;
    float deltaTime;
    float time_seconds = SDL_GetTicks() / 1000.f;


    while (!quit)
    {

        float now;
        now = SDL_GetTicks() / 1000.f;
        deltaTime = now - time_seconds;

        time_seconds = now;
        const Uint8* state = SDL_GetKeyboardState(NULL);


        
        while (SDL_PollEvent(&e))
        {
            if (e.type == SDL_QUIT)
            {
                quit = true;
            }

            if (e.type == SDL_KEYDOWN)
            {
                if (state[SDL_SCANCODE_W])
                {
                    if (snake.orientation == ORIENTATION_LEFT || snake.orientation == ORIENTATION_RIGHT)
                    {
                        snake.orientation = ORIENTATION_UP;
                        std::cout << "W pressed" << std::endl;
                    }
                }
                if (state[SDL_SCANCODE_S])
                {
                    if (snake.orientation == ORIENTATION_LEFT || snake.orientation == ORIENTATION_RIGHT)
                    {
                        snake.orientation = ORIENTATION_DOWN;
                        std::cout << "S pressed" << std::endl;
                    }

                }

                if (state[SDL_SCANCODE_D])
                {
                    if (snake.orientation == ORIENTATION_UP || snake.orientation == ORIENTATION_DOWN)
                    {
                        snake.orientation = ORIENTATION_RIGHT;
                        std::cout << "D pressed" << std::endl;
                    }

                }
                if (state[SDL_SCANCODE_A])
                {
                    if (snake.orientation == ORIENTATION_UP || snake.orientation == ORIENTATION_DOWN)
                    {
                        snake.orientation = ORIENTATION_LEFT;
                        std::cout << "A pressed" << std::endl;
                    }

                }

            }
        }

        snake.move(deltaTime);


        SDL_RenderClear(ren);
        snake.render(ren);
        SDL_RenderPresent(ren);
            
    }

    SDL_DestroyRenderer(ren);
    SDL_DestroyWindow(w);

    return 0;
}

All I am trying to accomplish here is have my sprite move at a consistent speed in all directions.


Solution

  • You store snake position as integer values, but your movement calculation is floating point. When floating point is assigned to integer variable, it is implicitly converted using round-towards-zero convention.

    Consider this example:

    int x;
    float dt = 1.0/60;  // simulating 60 fps
    
    x = 392;
    x += 3*dt;          // adding 0.05, and result is converted to int
    // what value x have now? Still 392, no matter how many times you add 0.05 to it
    
    x = 392;
    x += -3*dt;         // adding -0.05, and result is converted to int
    // what value x have now? 391, because implicit cast to integer rounds towards zero
    

    Not only your positive movements don't do anything, your negative movements are also have much higher velocity then specified, and definitely not frametime independent, as you move 1 pixel each frame, even if e.g. -3*(1.0/60) is only -0.05.

    How to solve this depends on what result you want to achieve. Usually snake game moves snake in grid steps; if that's what you want to get, you'll need to accumulate movement in e.g. floating point variable and only perform big move step when this accumulated movement is higher than some pre-specified grid size. E.g.

    // float accumulated_movement_x, accumulated_movement_y in your snake struct,
    // initialised to zero
    
    accumulated_movement_x += positionXDelta;
    accumulated_movement_y += positionYDelta;
    if(fabs(accumulated_movement_x) >= GRID_STEP_SIZE) {
        int int_dx = ((accumulated_movement_x>0) ? 1 : -1) * GRID_STEP_SIZE;
        head.x += int_dx;
        accumulated_movement_x -= int_dx;
    }
    if(fabs(accumulated_movement_y) >= GRID_STEP_SIZE) {
        int int_dy = ((accumulated_movement_y>0) ? 1 : -1) * GRID_STEP_SIZE;
        head.y += int_dy;
        accumulated_movement_y -= int_dy;
    }
    

    You probably should reset accumulated movement to 0 when movement direction is changed, and/or tie movement to 'movement' tick, depending on what result you want to get.

    If you want your snake to move smoothly (presuming your game logic also uses that values?), you can store snake position as floating point (e.g. SDL_FRect or your custom struct/variables). Beware of floating point error accumulation, but your screen size is not very big so it shouldn't be a problem.

    Or you can combine the two, e.g. by moving by 1-pixel grid.