4

I am making an Sdl game, it's 2d shooter. I am using SDL to import surfaces and OpenGL to draw them on the screen (doing so because it works way faster than just SDL). I've got two threads running, one for processing stuff and rendering, and another one for input. Basically, the processing one is taking 1-2% of my CPU, while the input loop takes 25% (on quad-core, so it's 1 full core). I tried doing SDL_Delay(1) before each while (SDL_PollEvent(&keyevent)) and it works! Reduces CPU load to 3% for whole process. However, there's a nasty side effect. The whole program input is handicapped: it doesn't detect all of the key pressed, and for example, to make the character move, it sometimes takes up to 3 seconds of bashing keyboard for it to react.

I've also tried solving it by using SDL_PeepEvent() and SDL_WaitEvent(), however, it's causing 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/render 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?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Sunius
  • 2,789
  • 18
  • 30
  • You definitely need the delay, but probably less of it. – enobayram Mar 23 '12 at 19:41
  • Are you checking that you're not receiving tons of spurious SDL events? try logging that loop and see if you're getting tons of garbage. also, if using an 0x compliant compiler, std::this_thread::yield() should do it. – std''OrgnlDave Mar 23 '12 at 19:49
  • Is there any tool to make it stop for less than 1 milisecond? Also, how do I used std::this_thread::yield() and what does it do? Is visual studio considered 0x compliant compiler? – Sunius Mar 23 '12 at 19:53
  • As an aside, you shouldn’t use `SDL_Delay()` for frame timing. It’s better (and simpler) to run at a variable rate, performing updates based on how long the last frame took to update and render, i.e., `(SDL_GetTicks() - last_update) / 1000.0`. – Jon Purdy Mar 23 '12 at 19:58
  • `if (Interval - clock() + start > 0) SDL_Delay(Interval - clock() + int(start));` does that, doesn't it? Interval is the time I want between each cycle (it's 50 ms in current implementation), and clock() is same as SDL_GetTicks() (afaik), and start is when the frame started rendering. – Sunius Mar 23 '12 at 20:03
  • I don't know any SDL but, nevertheless, I can't believe that any lib/environment/whatever forces a thread to poll for keyboard input! – Martin James Mar 23 '12 at 20:09
  • Why are you using SDL_PeepEvent() and SDL_WaitEvent()? Just using wait event will block the thread until something has happened and no CPU will be used. There does not seem anything else in your while loop that needs to be triggered regularly, so blocking the thread should be OK, right? No delay should be needed if you do this. – Joost Sannen Mar 23 '12 at 20:24
  • @JoostSannen It's an interactive game, STL_WaitEvent is STL_PollEvent() with a 10 second delay. So for 10 seconds his thread won't update anything on the screen. – std''OrgnlDave Mar 23 '12 at 22:32
  • @Sunius Visual Studio 2010 doesn't support it, I don't believe. boost::this_thread::yield() would do the same. std::this_thread::yield() does something known as 'yielding,' whereby the thread says 'I'm done doing what I had to do with my timeslice, go ahead an let another program do some work.' – std''OrgnlDave Mar 23 '12 at 22:34
  • Oh! I see you're using Windows' multi-threading. If you're going to thread your game, you should use SDL's threading support. – std''OrgnlDave Mar 23 '12 at 22:41
  • You simply need to merge both threads into one - handling input in separate thread is ridiculous, especially in 2D game. If you can use multiple threads, it doesn't mean you should do it - you needlessly complicate the engine. Use single thread and let the system use extra cores for background tasks. If you really NEED multithreading, use extra cores for something that is actually useful and computationally expensive. – SigTerm Mar 23 '12 at 23:11
  • @OrgnlDave I am by no means an expert in SDL. So I cannot dispute if wait event introduces a 10 second delay. If it is the case, I would find this very weird. I cannot find this information in the [documentation](http://www.libsdl.org/docs/html/sdlwaitevent.html) either. – Joost Sannen Mar 25 '12 at 17:32

4 Answers4

5

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.

paercebal
  • 81,378
  • 38
  • 130
  • 159
  • Thanks for all the input! There's still so much I have to learn I guess! :) I've already rearranged everything in one thread, and I'm already thinking about remaking movement scheme. Though, I am wondering, would that momentary key-down check be any good? I mean, what if I press the button when it's calculating another thing? I thought queue was supposed to take care for that? About my booleans: heh, that's how I'm used to use them. Been working on digital logic for some time, and there's no true or false there, only 1 and 0 :) – Sunius Mar 23 '12 at 21:19
  • @Sunius : `would that momentary key-down check be any good? I mean, what if I press the button when it's calculating another thing?` it's already what happens in GUI on Windows, and unless the program is badly written, you don't see the difference, do you? The important thing is that when the key will be pressed, I guess the code will be interrupted to post a "key pressed" message, then the code will resume, with your "key pressed" message in the queue, waiting to be handled. – paercebal Mar 25 '12 at 15:03
  • @Sunius : As for the booleans, do yourself a favor: use true/false. Every advanced language have that (even C99 ended to have some kind of boolean thing), and those values are strongly typed. For example, the compiler will consider `0` and `1` to be integers, so in some cases (function overloading, for example), using `0` or `1` will not have the result you expect. You're coding in C++, so make sure your code is strongly typed, and don't use integers instead of booleans for boolean logic... :-) ... – paercebal Mar 25 '12 at 15:06
1

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
genpfault
  • 51,148
  • 11
  • 85
  • 139
  • Any suggestions then how to make it responsive without multithreading? – Sunius Mar 23 '12 at 20:21
  • Just the standard `while(true) { pollInput(); handleInput(); doGraphics(); SDL_Delay(1); }` – genpfault Mar 23 '12 at 20:26
  • Okay, I guess I figured it out! I made variables x1, x2, y1, y2 as private variables of GameWorld class, so they don't get lost between calls (they're storing player's velocity information). Also, I reduced the cycle interval ten times (to 10 ms), and it's running super responsive! CPU is hovering at 3-4% as well. Thanks! – Sunius Mar 23 '12 at 20:52
0

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.

George Skoptsov
  • 3,831
  • 1
  • 26
  • 44
0

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.

Oddity0x0
  • 71
  • 3