A way to make the keyboard event queue responsive and not use up all the CPU power

I am making an Sdl game, it is a 2d shooter. I use SDL to import surfaces and OpenGL to draw them to the screen (this is done because it is faster than SDL alone). I have two threads, one for material handling and rendering and the other for input. Basically, processing takes 1-2% of my cpu, whereas the input loop takes 25% (on a quad-core, so that's 1 full core). I've tried doing SDL_Delay (1) before each while (SDL_PollEvent(&keyevent))

one and it works! Reduces CPU load by 3% for the whole process. However, there is an unpleasant side effect. All program input is difficult: it does not detect all the keys pressed and, for example, to make a character move, sometimes it takes up to 3 seconds of a keyboard deck for it to react.

I also tried to solve it using SDL_PeepEvent()

and SDL_WaitEvent()

however it causes the same (very long!) Delay.

Event loop code:

void GameWorld::Movement()
{
    SDL_Event keyevent;
    bool x1, x2, y1, y2, z1, z2, z3, m;         // Booleans to determine the
    x1 = x2 = y1 = y2 = z1 = z2 = z3 = m = 0;   // movement direction
    SDL_EnableKeyRepeat(0, 0);
    while (1)
    {
        while (SDL_PollEvent(&keyevent))
        {
            switch(keyevent.type)
            {
            case SDL_KEYDOWN:
                switch(keyevent.key.keysym.sym)
                {
                case SDLK_LEFT:
                    x1 = 1;
                    x2 = 0;
                    break;
                case SDLK_RIGHT:
                    x1 = 0;
                    x2 = 1;
                    break;
                case SDLK_UP:
                    y1 = 1;
                    y2 = 0;
                    break;
                case SDLK_DOWN:
                    y1 = 0;
                    y2 = 1;
                    break;
                default:
                    break;
                }
                break;
            case SDL_KEYUP:
                switch(keyevent.key.keysym.sym)
                {
                case SDLK_LEFT:
                    x1 = x2 = 0;
                    break;
                case SDLK_RIGHT:
                    x1 = x2 = 0;
                    break;
                case SDLK_UP:
                    y1 = y2 = 0;
                    break;
                case SDLK_DOWN:
                    y1 = y2 = 0;
                    break;
                default:
                    break;
                }
                break;
            case SDL_QUIT:
                PrintToFile("The game was closed manually.\n");
                CleanUp();
                return;
                break;
            default:
                break;
            }
        }
        m = x1 || x2 || y1 || y2;
        if (m)   // if any button is pushed down, calculate the movement
        {        // direction and assign it to the player
            z1 = (x1 || x2) && (y1 || y2);
            z2 = !x1 && (x2 || y2);
            z3 = (!y1 && x1) || (!y2 && x2);
            MainSurvivor->SetMovementDirection(4 * z1 + 2 * z2 + z3);
        }
        else    // if no button is pushed down, reset the direction
            MainSurvivor->SetMovementDirection(-1);
    }
}

      

Code for calculation / rendering loop:

void GameWorld::GenerateCycles()
{
    int Iterator = 0;
    time_t start;   
    SDL_Event event;

    Render();
    _beginthread(MovementThread, 0, this);
    while (1)
    {
            // I know I check this in input loop, but if I comment
        SDL_PollEvent(&event);   // out it from here, that loop cannot
        if (event.type == SDL_QUIT)  // see any of the events (???)!
        {
            PrintToFile("The game was closed manually.\n");
            CleanUp();
        }                            // It never closes through here though

        start = clock();
        Iterator++;
        if (Iterator >= 232792560)
            Iterator %= 232792560;
        MainSurvivor->MyTurn(Iterator);
        for (unsigned int i = 0; i < Survivors.size(); i++)
        {
            Survivors[i]->MyTurn(Iterator);
            if (Survivors[i]->GetDiedAt() != 0 && Survivors[i]->GetDiedAt() + 25 < clock())
            {
                delete Survivors[i];
                Survivors.erase(Survivors.begin() + 5);
            }
        }
        if (Survivors.size() == 0)
            SpawnSurvivors();

        for (int i = 0; i < int(Zombies.size()); i++)
        {
            Zombies[i]->MyTurn(Iterator);
            if (Zombies[i]->GetType() == 3 && Zombies[i]->GetDiedAt() + 25 < Iterator)
            {
                delete Zombies[i];
                Zombies.erase(Zombies.begin() + i);
                i--;
            }
        }
        if (Zombies.size() < 3)
            SpawnZombies();

            // No need to render every cycle, gameplay is slow
        if (Iterator % 2 == 0)
            Render();

        if (Interval - clock() + start > 0)
            SDL_Delay(Interval - clock() + int(start));
    }
}

      

Does anyone have any ideas?

+3


source to share


4 answers


I don't really know much about SDL or game programming, but here are some random ideas:

Reaction to state changes

Your code:

while (1)
{
    while (SDL_PollEvent(&keyevent))
    {
        switch(keyevent.type)
        {
            // code to set keyboard state
        }
    }

    // code to calculate movement according to keyboard state
    // then act on that movement
}

      

This means that no matter what happens on the keyboard, you are calculating and setting the data.

If installing data is expensive (hint: synced data) then it will cost you even more.

SDL_WaitEvent: state measurement

You need to wait for the event to happen, instead of writing that 100% uses one processor.

Here's a variation of the event loop I wrote for a test at home:

while(true)
{
    // message processing loop
    ::SDL_Event event ;

    ::SDL_WaitEvent(&event) ; // THIS IS WHAT IS MISSING IN YOUR CODE

    do
    {
        switch (event.type)
        {
            // etc.
        }
    }
    while(::SDL_PollEvent(&event)) ;

    // re-draw the internal buffer
    if(this->m_isRedrawingRequired || this->m_isRedrawingForcedRequired)
    {
        // redrawing code
    }

    this->m_isRedrawingRequired = false ;
    this->m_isRedrawingForcedRequired = false ;
}

      

Note. It was single threaded. I'll talk about streams later.

Note 2: the dot around the two "m_isRedrawing ..." boolean is a forced redrawing when one of these booleans is true and when the timer asks a question. Usually no redrawing.

The difference between my code and yours is that by no means are you letting the thread "wait".

Keyboard events

There is a problem, I think, with your keyboard event handling.

Your code:

        case SDL_KEYDOWN:
            switch(keyevent.key.keysym.sym)
            {
            case SDLK_LEFT:
                x1 = 1;
                x2 = 0;
                break;
            case SDLK_RIGHT:
                x1 = 0;
                x2 = 1;
                break;
            // etc.
            }
        case SDL_KEYUP:
            switch(keyevent.key.keysym.sym)
            {
            case SDLK_LEFT:
                x1 = x2 = 0;
                break;
            case SDLK_RIGHT:
                x1 = x2 = 0;
                break;
            // etc.
            }

      

Suppose you pressed LEFT and then RIGHT, then press LEFT. I would expect:

  • press LEFT: the character stays on the left
  • press RIGHT: the symbol stops (by pressing the LEFT and RIGHT buttons)
  • unpress LEFT: character goes right because RIGHT is still pressed

In your case, you have:

  • press LEFT: the character stays on the left
  • press RIGHT: character goes to the right (since now LEFT is ignored, with x1 = 0)
  • unpress LEFT: the character stops (because you unplugged both x1 and x2.) despite the RIGHT button being pressed

You are doing it wrong because:

  • You react immediately to the event, instead of using a timer to react to the situation every nth millisecond
  • you mix events together.

I'll find the link later, but you need to make an array of boolean states for the keys pressed. Something like:



// C++ specialized vector<bool> is silly, but...
std::vector<bool> m_aKeyIsPressed ;

      

You initialize it with the available keys:

m_aKeyIsPressed(SDLK_LAST, false)

      

Then, on the key up event:

void MyContext::onKeyUp(const SDL_KeyboardEvent & p_oEvent)
{
    this->m_aKeyIsPressed[p_oEvent.keysym.sym] = false ;
}

      

and by pressing a key:

void MyContext::onKeyDown(const SDL_KeyboardEvent & p_oEvent)
{
    this->m_aKeyIsPressed[p_oEvent.keysym.sym] = true ;
}

      

This way, when you check at regular intervals (and the important part when checking ), you know the exact momentary state of the keyboard and can react to it.

Topics

The topics are cool, but then you have to know exactly what you are dealing with.

For example, the event loop thread calls the following method:

MainSurvivor->SetMovementDirection

      

The resolution (render) thread calls the following method:

MainSurvivor->MyTurn(Iterator);

      

Seriously, are you sharing data between two different threads?

If you are (and I know you are), then you have either:

  • If you have not synchronized access, there is a data consistency issue due to processor caching. Simply put, there is no guarantee that one dataset by one stream will be considered "modified" by another in a reasonable amount of time.
  • if you have synchronized accesses (with mutex, atomic variable, etc.) then you have performance because you (for example) lock / unlock the mutex at least once in each thread per loop iteration.

Instead, I would like to push the change from one thread to another (e.g. via a message to a synchronized queue).

Streaming is a hard problem anyway, so you should familiarize yourself with the concept before mixing it with SDL and OpenGL. Herb Sutter Blog is a wonderful collection of articles on topics.

What you need to do:

  • Try to write the thing in one thread using events, dispatched messages and timers.
  • If you find performance issues, move the event or drawing flow to a different location, but continue to work with events, dispatched messages and timers to communicate

PS: What's wrong with your boolean data?

Obviously you are using C ++ (for example void GameWorld::Movement()

), so using 1 or 0 instead of true

or false

won't make your code clearer or faster.

+5


source


If you have initialized SDL on a GameWorld::GenerateCycles()

stream and MovementThread

is called GameWorld::Movement()

then you have a problem :



  • Don't call SDL video / event functions from separate streams
+1


source


Have you tried using something like usleep(50000)

instead delay(1)

?

This will cause your thread to sleep 50ms between polling the queue, or equivalently, you polling the queue 20 times per second.

Also, what is the platform: Linux, Windows?

On Windows, you may not have usleep()

, but you can try select()

this way:

struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 50000;
select(0, NULL, NULL, NULL, &tv);

      

Another suggestion is to try out polling in a tight loop until it stops returning events. Once no events are expected, continue sleeping for 50ms between polls until it starts returning events again.

0


source


I would suggest looking into SDL_EventFilter and related functions. This is not a poll queue input method, so it doesn't need to stop, although if I remember correctly it doesn't happen on the main thread, which might be exactly what you need for performance but can complicate the code.

0


source







All Articles