Continous Movement for SDL 2 Joysticks?

Started by
38 comments, last by Alberth 6 years, 10 months ago

So, I am trying to implement controller motion in SDL 2 for continuous movement when I push on a controller direction. At this point in time, my code only works when I push on the controller once. When I get to the maximum movement, the movement on the screen stops. I can mitigate this by getting to the end of the range of motion more slowly when pushing on the stick, but that does not solve the issue.

Here is my code:


//more code before
 else if (e.type == SDL_JOYAXISMOTION) //handles joystick movement with collisions
           {
                if (e.jaxis.which == 0)
                {
                    if (e.jaxis.axis == 0)
                    {
                        if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                        {
                            if(base.x == 0) //base is of type SDL_Rect
                                base.x++;
                            base.x--;
                        }

                        else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                        {
                            if (base.x == 1024 - base.w)
                                base.x--;
                            base.x++;
                        }
                    }

                    else if (e.jaxis.axis == 1)
                    {
                        if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                        {
                            if (base.y == 0)
                                base.y++;
                            base.y--;
                        }

                        else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                        {
                            if (base.y == 768 - base.h)
                                base.y--;
                            base.y++;
                        }
                    }
                }
           }
//more code after

Thoughts? How can I get the controller to move the SDL_Rect as long as the joystick is held down?

Advertisement

Your idea of SDL_JOYAXISMOTION is incorrect. Its wiki page says "An SDL_JOYAXISMOTION event occurs whenever a user moves an axis on the joystick."

(see https://wiki.libsdl.org/SDL_JoyAxisEvent )

This means, you get this event when the position/orientation of the joystick changes. Thus from "centered", if you push it forward, you get 1 event (theoretically, usually, the computer is faster, and you get several events with smaller movements instead). If you then keep the joystick in the same position, you don't get any event anymore, until you move it again.

(You can verify this by printing something from within your event-handling code, you should see that you only get output when you move the joystick, not when you keep it steady.)

To preserve movement, your event-handling code should modify velocity rather than position of the rectangle. Make a 'speed' variable, and change that with the events. In the main game-loop, update position from the speed.

Alright, cool. Thanks for the info. After coming back to this, here is what I have come up with:


//more code above
else if (e.type == SDL_JOYAXISMOTION)
           {
                if (e.jaxis.which == 0)//x axis
                {
                    if (e.jaxis.axis == 0)
                    {
                        if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                        {
                            pVelX = -1;
                            if (e.jaxis.value == 0)
                                pVelX = 0;
                        }

                        else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                        {
                            pVelX = 1;
                            if (e.jaxis.value == 0)
                                pVelX = 0;
                        }
                    }

                    else if (e.jaxis.axis == 1)//y axis
                    {
                        if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                        {
                            pVelY = -1;
                            if (e.jaxis.value == 0)
                                pVelY = 0;
                        }

                        else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                        {
                            pVelY = 1;
                            if (e.jaxis.value == 0)
                                pVelY = 0;
                        }
                    }
                }
           }
//more code below


//more code above
void World1::movePlayer()
{
    playerBase.x+= pVelX;
    if(playerBase.x < 0 || (playerBase.x + playerBase.w > 1024))
        playerBase.x-= pVelX;

    playerBase.y+= pVelY;
    if(playerBase.y < 0 || (playerBase.y +playerBase.h > 768))
        playerBase.y-= pVelY;
}//more code below

//more code above
//main loop within a class declaration
SDL_Event e;

    quit = false;

    this->loadPlayer(renderer);

    while (!quit)
    {
        this->handlePlayerEvents(e, quit);
        this->movePlayer();
        this->renderPlayer(renderer);
    }
//more code below

Currently, this code (with more) moves the player all the way to the end of the screen when pushing down the joystick, even if the joystick held down for a short time. Additionally, if I quickly slide my finger off of the controller in the direction that I have the stick pushed, the character will go in the opposite direction that I pushed, and the character will go all the way to the end of the screen. How can I improve this code to work as it should so that the character will only move a little bit when I barely push it down and so that it does not go in the opposite direction if I take my thumb off of the joystick too quickly (hope that makes sense)?

This looks fishy inside the inner if, at least.


if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
{
   // [deleted lines]
}
else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
{
    pVelX = 1;
    if (e.jaxis.value == 0)
        pVelX = 0;
}

Assuming CONTROLLER_DEAD_ZONE is a non-zero, positive value, please explain why "pVelX = 0" is ever reached. That is, I don't know any number that is bigger than non-zero, and 0 at the same time.

A second point is, what should happen between -CONTROLLER_DEAD_ZONE and +CONTROLLER_DEAD_ZONE? Currently you're not doing anything, pVelX stays what it was.

I don't know if the latter is intended (that is, it can be correct).

Your posts speak about desired changes relative to the code (ie "My code isn't doing it correct, I want X and Y solved"), rather than a statement of what you want to achieve (ie "I want the player to move when the joystick is pushed, and stop when it is centered"). The latter gives a clear goal, independent of what the code is doing or not doing (or even if you don't have any code yet!).

Likely you have such a goal in your mind (or you could not speak about desired changes), but for us it would help if you also make clear what you want as final result. We may be able to point out alternative directions to try, or point out other flaws in your code you haven't seen yet.


if (e.jaxis.axis == 0)// x axis
                    {
                        if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                        {
                            pVelX = -1;
                        }

                        else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                        {
                            pVelX = 1;
                        }
                        else
                            pVelX = 0; //new code that works
//same idea for y axis

Many edits later: I have figured it out. I just needed to have an else statement, and I accidentally put the wrong variable name for the y axis. I increased the dead zone value a bit so that it is not so sensitive. I just wonder if I can slow down the movement a bit. It feels like my character is holding down the run button or something.

Great that it works.

I just wonder if I can slow down the movement a bit.
One solution is to make the velocity and position a real number, so you can have a 0.8 velocity. The disadvantage is that you can't display an image halfway a pixel (unless you use the GPU), so you have to round or truncate the position to an integer before blitting the image of the player.

I would recommend you make a constant for the velocities (in X and Y direction). While you are at it, make a few constants for the screen size too. Having constants reduces effort if you ever want to change such a value. Instead of manually searching and replacing all values 1024 that are screen-width, just modify the constant, and you're done!

Great idea about the constants.

If I understand you correctly, the units of the velocity are in pixels, then. That is why you can't have a decimal speed. How can I use the GPU? Does SDL 2 have functions for that? If so, what are they? Can you give me some links to tutorials (or perhaps books) that explain using SDL 2 with the GPU? Perhaps other libraries may be required?

If I understand you correctly, the units of the velocity are in pixels, then.

Well, euhm, yeah, you coded it like that :)

A quick tour through your own code:


if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
{
    pVelX = -1;

Here you set speed to 1 unit (there are other places too, I just picked one at random).


playerBase.x+= pVelX;
if(playerBase.x < 0 || (playerBase.x + playerBase.w > 1024)) {
    playerBase.x-= pVelX;
}

Here you update position with 1 unit velocity, and then compare against a well-known horizontal screen-width. From this I concluded you work in pixel units.

[I added the curly brackets here. I used to program like you, leaving them out if you could, but I found it was too confusing and error-prone, so I switched to always including them.]

There is nothing wrong with using pixels as unit of position and velocity. The biggest problem is that you can't display a pixel at (for example) position 34.6, it has to be either 34 or 35, ie units, with SDL2 (without GPU, but I'll come to that later).

In your case, you feel the player character is running too fast. That can be solved in 3 ways

- Reduce speed to some fraction (what I proposed), by far the simplest.

- Enlarge the size of the playing area. Instead of 1024 we could use 2048 as width. It takes longer for the player character to reach the other side with the current speed. Unfortunately, that does not come with a new monitor that is, say, twice as wide and high. The answer to that is that you scale the bigger playing area back to your current screensize. However, 2048 wide at speed 1 is equivalent to 1024 at speed 0.5, so this is just another form of "reduce speed".

- Update the position of the player character less often. If you skip updating the position 50% of the time (ie call "movePlayer" once every 2 iterations), the player character moves 1 pixel every 2 iterations, instead of 1 pixel every iteration. It thus seems to move twice as slow. This solution is however not so flexible. It works well enough for simple cases like 50% or 75%, but imagine you want 7/12th speed, so update position every 7 out of 12 iterations. This gets horribly complicated, while changing speed to a fraction can manage the same thing easily.

That is why you can't have a decimal speed.

As you will find out, "cannot" hardly exists in software. If you want it badly enough, almost anything can be done. The normal breaking point is "it's a bad idea to want this", because you yourself won't be able to understand what happens, or why.

For fractional speed however, you're well within safety limits, it just takes a simple conversion.


int posX = ...;
int posY = ...;
int velX = ...; // Assign units
int velY = ...;
...
posX += velX;
posY += velY;
...
SDL_Rect dst_rect;
dst_rect.x = posX;
dst_rect.y = posY;
dst_rect.w = ...
dst_rect.h = ...
SDL_BlitSurface(this->image, nullptr, this->surface, &dst_rect);

Your current code likely mostly looks like this. You have an integer posX/Y, and an integer velX/Y, you update position with the velocity, and then blit the player character image to the window surface. As you can see, SDL_Rect is the data structure to give the position at the screen. Its definition is described at https://wiki.libsdl.org/SDL_Rect

As you can see, "x" and "y" of SDL_Rect are "int", SDL_BlitSurface can only handle unit positions, not fractions.

That's not a problem, we can still have floating point speeds and positions:


double posX = ...;
double posY = ...;
double velX = ...; // Assign fractions
double velY = ...;
...
posX += velX;
posY += velY;
...
SDL_Rect dst_rect;
dst_rect.x = posX; // <-- here, double to int conversion takes place, it rounds towards zero.
dst_rect.y = posY;
dst_rect.w = ...
dst_rect.h = ...
SDL_BlitSurface(this->image, nullptr, this->surface, &dst_rect);

I only changed the type of position and velocities. (There is also "float" as type. It is also a floating point, but it's smaller and computes slightly faster. neither is relevant for your application currently.) Not shown is that you should also assign a fraction to the speed, or it has no effect.

You can compile this, and it will work. Now you may wonder, what if posX == 34.6, what happens then? The answer is that the C++ compiler converts your double posX value to an SDL_Rect integer x coordinate by rounding towards zero. 34.6 becomes 34, -1.999 becomes -1. If you want 'normal' rounding instead, for positive numbers, add 0.5, ie "dst_rect.x = posX + 0.5;". 34.6 + 0.5 = 35.1, which rounded towards zero, becomes 35. For negative numbers, this trick likely fails (if you like math puzzles, it can be fun to figure out such things), but since your positions are non-negative anyway, it doesn't matter much.

How can I use the GPU? Does SDL 2 have functions for that?

The GPU is a graphics monster, It can draw filled triangles incredibly quickly. It can draw 'pixels' at every position, including at fractional positions. Your screen however cannot display such pixels directly, so the GPU uses a trick known as anti-aliasing to do it. Basically, it splits your pixel at 34.6 in two parts, and draws 0.4 part of it at position 34, and 0.6 part of it at position 35. Our eyes and brain then merge both pixels again, and we perceive it as a single pixel again.

SDL2 does have GPU support, and I think it already uses the GPU internally, for its better performance. I started looking at how to use the GPU with SDL2 myself a few days ago, and while I found a tutorial for it ( http://lazyfoo.net/tutorials/SDL/index.php lesson 7 and further), I am not so happy with it, and decided not to use it myself.

For you, I'd advice to stick with SDL2 as you have it now. Given the kind of questions and problems you post here, switching to a GPU is a not-smart move. You'll get loads of additional problems, and no useful benefit from it, since you can easily use fractional positions and speeds with your current program, as I showed above.

Instead, try to slow the character down a bit, then extend the program so it becomes a fun game :)

Thanks for all the advice. You have been very helpful.

Alright, so it looks like I should change the constants in the class defintion like I did below:


//code excerpt
private:

        SDL_Texture* pTexture;
        SDL_Rect playerBase;

        double pVelX;
        double pVelY;
        double pPosX;
        double pPosY;

        bool quit;

        static constexpr double PLAYER_VELOCITY_X = 0.8;
        static constexpr double PLAYER_VELOCITY_Y = 0.8;
        static const int CONTROLLER_DEAD_ZONE = 20000; //dead zone for controller

Now, the code does not work yet. Perhaps I should change something in the controller function?


else if (e.type == SDL_JOYAXISMOTION)
    {
        if (e.jaxis.which == 0)//controller 0
        {
            if (e.jaxis.axis == 0)// x axis
            {
                if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                    pVelX = -PLAYER_VELOCITY_X;
                else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                    pVelX = PLAYER_VELOCITY_X;
                else
                    pVelX = 0;
            }

            else if (e.jaxis.axis == 1)//y axis
            {
                if (e.jaxis.value < -CONTROLLER_DEAD_ZONE)
                    pVelY = -PLAYER_VELOCITY_Y;
                else if (e.jaxis.value > CONTROLLER_DEAD_ZONE)
                    pVelY = PLAYER_VELOCITY_Y;
                else
                    pVelY = 0;
            }
        }
    }

Or the move function?


void World1::movePlayer()
{
    playerBase.x+= pVelX;
    if (playerBase.x < 0 || (playerBase.x + playerBase.w > 1024))
        playerBase.x-= pVelX;

    playerBase.y+= pVelY;
    if (playerBase.y < 0 || (playerBase.y + playerBase.h > 768))
        playerBase.y-= pVelY;
}

Here is the render function that I am using too, with the function to load player.


//renders player to screen
void World1::renderPlayer(SDL_Renderer*& renderer)
{
    SDL_RenderClear(renderer); //clears screen
    SDL_RenderCopy(renderer, pTexture, NULL, &playerBase);
    SDL_RenderPresent(renderer); //puts image on screen
}

//loads player
void World1::loadPlayer(SDL_Renderer*& renderer)
{
    pTexture = IMG_LoadTexture(renderer, "male_base-test-anim.gif");

    if (pTexture == 0)
    {
        cout << "Unable to load texture.";
    }

    playerBase.x = 0;
    playerBase.y = 0;
    playerBase.w = 948;
    playerBase.h = 335;

}

Given how I have my main loop, hopefully I do not need to dramatically alter anything:


//main loop of program
void World1::mainLoop(SDL_Renderer*& renderer)
{
    SDL_Event e;

    quit = false;

    this->loadPlayer(renderer);

    while (!quit)
    {
        this->handlePlayerEvents(e, quit);
        this->movePlayer();
        this->renderPlayer(renderer);
    }
}

You think I should go ahead and use a position variable in the move function, maybe?

Now, the code does not work yet.

| have coffee.

The probem with statements like that is they give absolutely no information about context. The only conclusion of "does not work" that I can draw is "it does not work", period. The same as with my statement, you have no idea what I want with coffee, especially since I already seem to have it.

To give you any kind of direction, I need to understand how it does not work. Did you turn on the computer? And the screen? Did you save the file after compiling? Does it run at all? Did you plug in the joystick? Does it crash if you turn the joystick to the left? Does the player character move?

All these, and more, are forms of "does not work". Without you doing some pre-selection work, I can only do random guessing. I don't want to do that.

Please tell what happens, under which conditions. If it deviates from your expectations, tell what you expect to happen. Also explain what it does instead, and why that is a problem or unwanted.

Edit: What's the type of the "x" and "y" coordinate of playerBase? If it is "int" what do you think will happen if you add 0.8, and you remember from my previous post that assignment of (x+0.8) is truncated?

This topic is closed to new replies.

Advertisement