Способ сделать очередь событий клавиатуры одновременно отзывчивой и не потреблять всю мощность процессора - PullRequest
4 голосов
/ 23 марта 2012

Я делаю игру Sdl, это 2d шутер. Я использую SDL для импорта поверхностей и OpenGL для их рисования на экране (потому что он работает намного быстрее, чем просто SDL). У меня запущены два потока: один для обработки и рендеринга, а другой для ввода. По сути, один процессор занимает 1-2% моего ЦП, а цикл ввода занимает 25% (на четырехъядерном, то есть 1 полное ядро). Я пытался делать SDL_Delay (1) перед каждым while (SDL_PollEvent(&keyevent)), и это работает! Уменьшает нагрузку на процессор до 3% для всего процесса. Тем не менее, есть неприятный побочный эффект. Весь ввод программы затруднен: он не обнаруживает нажатие всех клавиш, и, например, чтобы заставить персонажа двигаться, иногда требуется до 3 секунд нажатия клавиатуры, чтобы он реагировал.

Я также пытался решить эту проблему с помощью SDL_PeepEvent() и SDL_WaitEvent(), однако это вызывает такую ​​же (очень долгую!) Задержку.

Код цикла событий:

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);
    }
}

Код для расчета / рендеринга цикла:

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));
    }
}

У кого-нибудь есть идеи?

Ответы [ 4 ]

5 голосов
/ 24 марта 2012

Я не очень разбираюсь в SDL или программировании игр, но вот несколько случайных идей:

Реакция на изменения состояния

Ваш код:

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
}

Это означает, что независимо от того, что на вашей клавиатуре ничего не происходит, вы вычисляете и настраиваете данные.

Если настройка данных дорогая (подсказка: синхронизированные данные), то это будет стоить вам еще больше.

SDL_WaitEvent: измерение состояния

Вы должны ждать, пока не произойдет событие, а не вращение, которое вы написали, что вызывает 100% использование одного процессора.

Вот вариант событияЦикл, который я написал для теста дома:

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

Примечание: это был однопоточный.Я расскажу о потоках позже.

Примечание 2: Смысл в двух логических значениях "m_isRedrawing ..." состоит в том, чтобы принудительно перерисовывать, когда один из этих логических значений равен true, и когда таймер задает вопрос.Обычно перерисовка отсутствует.

Разница между моим и вашим кодом заключается в том, что вы ни в коем случае не позволяете потоку "ждать".

События клавиатуры

СуществуетЯ полагаю, это проблема с обработкой событий клавиатуры.

Ваш код:

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

Допустим, вы нажимаете ВЛЕВО, а затем ВПРАВО, а затем нажимаете ВЛЕВО.То, что я ожидал бы:

  1. нажмите ВЛЕВО: символ уходит влево
  2. нажмите ВПРАВО: символ останавливается (при нажатии и ВЛЕВО и ВПРАВО)
  3. unpress LEFT: символ идет вправо, потому что RIGHT по-прежнему нажата

В вашем случае:

  1. нажмите LEFT: символ идет влево
  2. нажмите ВПРАВО: символ перемещается вправо (как теперь LEFT игнорируется, с x1 = 0)
  3. unpress LEFT: символ останавливается (потому что вы сбросили оба x1 и x2.), несмотря на то, что RIGHT все еще нажата

Вы делаете это неправильно, потому что:

  1. Вы реагируете непосредственно на событие, а не используете таймер, чтобы реагировать на ситуацию каждую n-ю миллисекунду
  2. вы смешиваете события вместе.

Ссылку я найду позже, но вам нужно иметь массив логических состояний для нажатых клавиш.Что-то вроде:

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

Вы инициализируете его с размером доступных ключей:

m_aKeyIsPressed(SDLK_LAST, false)

Затем, при событии key up:

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

и по ключуdown:

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

Таким образом, когда вы проверяете через равные промежутки времени (и , когда вы проверяете партию , важно знать точное мгновенное состояние клавиатуры, и вы можетереагировать на это.

Потоки

Потоки крутые, но тогда вы должны точно знать, с чем имеете дело.

Например, поток цикла событий вызывает следующий метод:

MainSurvivor->SetMovementDirection

Поток разрешения (рендеринга) вызывает следующий метод:

MainSurvivor->MyTurn(Iterator);

Серьезно, вы разделяете данные между двумя разными потоками?

Если вы(и я знаю, что вы есть), тогда у вас есть:

  1. , если вы не синхронизировали доступы, проблема согласованности данных из-за кэшей процессора.Проще говоря, нет никакой гарантии, что один набор данных одним потоком будет рассматриваться как «измененный» другим в разумные сроки.
  2. , если вы синхронизировали доступы (с мьютексом, атомарной переменной и т. Д.)), тогда у вас будет снижение производительности, потому что вы (например) блокируете / разблокируете мьютекс, по крайней мере, один раз на поток за итерацию цикла.

Вместо этого я бы сообщил об изменении из одного потокак другому (например, через сообщение в синхронизированную очередь).

В любом случае многопоточность является серьезной проблемой, поэтому вам следует ознакомиться с этой концепцией, прежде чем смешивать ее с SDL и OpenGL. Блог Херба Саттера - изумительная коллекция статей по темам.

Что вам нужно сделать, это:

  1. Попробуйте написать вещь в одном потоке, используя события, опубликованные сообщения и таймеры.
  2. Если вы обнаружите проблемы с производительностью, переместите событие или поток рисунка в другое место, но продолжайте работать с событиями, отправленными сообщениями и таймерами для связи

PS: Что не так с вашими логическими значениями?

Вы, очевидно, используете C ++ (например, void GameWorld::Movement()), поэтому использование 1 или 0 вместо true или false не сделает ваш код более понятным или быстрым.

1 голос
/ 24 марта 2012

Если вы инициализировали SDL в потоке GameWorld::GenerateCycles() и MovementThread вызывает GameWorld::Movement(), тогда у вас проблема :

  • Не вызывать функции видео / событий SDL из отдельных потоков
0 голосов
/ 24 марта 2012

Я бы посоветовал изучить SDL_EventFilter и связанные с ним функции. Это не метод ввода очереди опроса, поэтому он не требует остановки, хотя, если я правильно помню, это не происходит в основном потоке, что может быть именно тем, что вам нужно для производительности, но может усложнить код.

0 голосов
/ 23 марта 2012

Вы пытались использовать что-то вроде usleep(50000) вместо delay(1)?

Это заставит ваш поток спать в течение 50 мсек между опросом очереди, или, эквивалентно, вы будете проверять очередь 20 раз в секунду.

Кроме того, на какой это платформе: Linux, Windows?

В Windows у вас может не быть usleep(), но вы можете попробовать select() следующим образом:

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

Другим предложением является попытка опроса в узком цикле, пока он не перестанет возвращать события. Если нет ожидающих событий, продолжайте спать в течение 50 мсек между опросами, пока он не начнет возвращать события снова.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...