A way to make keyboard event queue both responsive and not take whole CPU power

时间秒杀一切 提交于 2019-11-30 22:15:49

I'm not really experienced with SDL or game programming, but here are some random ideas:

Reacting on 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 the fact nothing is happening on your keyboard, you are calculating and setting data.

If setting the data is expensive (hint: synchronized data), then it will cost you even more.

SDL_WaitEvent : measuring state

You must wait for an event to happen, instead of the spinning you wrote which causes 100% usage of 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 : This was single threaded. I'll speak about threads later.

Note 2 : The point about the the two "m_isRedrawing..." boolean is to force redrawing when one of those booleans are true, and when the timer asks the question. Usually, there is no redrawing.

The difference between my code and yours is that at no moment you let the thread "wait".

Keyboard events

There is a problem, I guess, with your handling of keyboard events.

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.
            }

Let's say You press LEFT, and then RIGHT, then unpress LEFT. What I'd expect is:

  1. press LEFT : the character goes left
  2. press RIGHT : the character stops (as both LEFT and RIGHT are pressed)
  3. unpress LEFT : the character goes right, because RIGHT is still pressed

In your case, you have:

  1. press LEFT : the character goes left
  2. press RIGHT : the character goes right (as now LEFT is ignored, with x1 = 0)
  3. unpress LEFT : the character stops (because you unset both x1 and x2.), despite the fact RIGHT is still pressed

You're doing it wrong, because:

  1. You're reacting immdiately to an event, instead of using a timer to react to a situation every nth millisecond
  2. you are mixing events together.

I'll find the link later, but what you should do is have an array of boolean states for pressed keys. Something like:

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

You initialize it with the size of available keys:

m_aKeyIsPressed(SDLK_LAST, false)

Then, on key up event:

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

and on key down:

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 when you check part is important), you know the exact instantaneous state of the keyboard, and you can react to it.

Threads

Threads are cool but then, you must know exactly what you are dealing with.

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

MainSurvivor->SetMovementDirection

The resolution (rendering) 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:

  1. if you didn't synchronize the accesses, a data coherence problem because of processor caches. Put it simply, there's no guarantee one data set by one thread will be seen as "changed" by the other in a reasonable time.
  2. if you did synchronize the accesses (with a mutex, atomic variable, etc.), then you have a performance hit because you are (for example) locking/unlocking a mutex at least once per thread per loop iteration.

What I would do instead is communicate the change from one thread to another (via a message to a synchronized queue, for example).

Anyway, threading is an heavy issue, so you should familiarize with the concept before mixing it with SDL and OpenGL. Herb Sutter's blog is a marvelous collection of articles on threads.

What you should do is:

  1. Try to write the thing in one thread, using events, posted messages, and timers.
  2. If you find performance issues, then move the event or the drawing thread elsewhere, but continue to work with events, posted messages, and timers to communicate

P.S.: What's wrong with your booleans?

You're obviously using C++ (e.g. void GameWorld::Movement()), so using 1 or 0 instead of true or false will not make your code clearer or faster.

If you initialized SDL on GameWorld::GenerateCycles()'s thread and MovementThread is calling GameWorld::Movement() then you have a problem:

  • Don't call SDL video/event functions from separate threads

Have you tried using something like usleep(50000) instead of delay(1)?

This would make your thread sleep for 50 msecs between polling the queue, or equivalently, you would check the queue 20 times per second.

Also, what platform is this on: Linux, Windows?

On Windows you may not have usleep(), but you can try select() as follows:

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

Another suggestion is to try polling in a tight loop until it stops returning events. Once no events are pending, proceed with sleeping for 50 msecs between polls until it starts returning events again.

I'd suggest looking into SDL_EventFilter and the related functions. It's not a polling queue input method, so it doesn't require stalling, although, if I remember correctly, it doesn't happen on the main thread, which may be exactly what you need for performance, but might complicate the code.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!