4

我正在制作一款 Sdl 游戏,它是 2d 射击游戏。我正在使用 SDL 导入表面并使用 OpenGL 在屏幕上绘制它们(这样做是因为它的工作方式比 SDL 更快)。我有两个线程在运行,一个用于处理内容和渲染,另一个用于输入。基本上,处理一个占用我的 CPU 的 1-2%,而输入循环占用 25%(在四核上,所以它是 1 个全核)。我尝试在每个之前执行 SDL_Delay(1)while (SDL_PollEvent(&keyevent))并且它有效!将整个进程的 CPU 负载降低到 3%。但是,有一个令人讨厌的副作用。整个程序输入是有缺陷的:它没有检测到所有按下的键,例如,为了使角色移动,有时需要长达 3 秒的敲击键盘才能做出反应。

我也尝试过使用SDL_PeepEvent()and来解决它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

4 回答 4

5

我对 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...”布尔值的要点是当其中一个布尔值为真时以及计时器提出问题时强制重绘。通常,没有重绘。

我的代码和您的代码之间的区别在于,您绝不会让线程“等待”。

键盘事件

我猜你对键盘事件的处理存在问题。

你的代码:

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

假设您按 LEFT,然后按 RIGHT,然后松开 LEFT。我期望的是:

  1. 按 LEFT :角色向左走
  2. 按 RIGHT :字符停止(同时按下 LEFT 和 RIGHT)
  3. unpress LEFT :角色向右移动,因为仍然按下 RIGHT

在您的情况下,您有:

  1. 按 LEFT :角色向左走
  2. 按 RIGHT :字符向右移动(因为现在 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)

然后,在按键事件中:

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

并按下键:

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 混合之前,您应该熟悉这个概念。Herb Sutter 的博客是关于线程的精彩文章集合。

你应该做的是:

  1. 尝试使用事件、发布的消息和计时器在一个线程中编写内容。
  2. 如果您发现性能问题,则将事件或绘图线程移至其他位置,但继续使用事件、发布的消息和计时器进行通信

PS:你的布尔值有什么问题?

您显然在使用 C++(例如void GameWorld::Movement()),因此使用 1 或 0 代替trueorfalse不会使您的代码更清晰或更快。

于 2012-03-23T20:59:37.837 回答
1

如果您在GameWorld::GenerateCycles()的线程上初始化 SDL 并MovementThread正在调用GameWorld::Movement(),那么您遇到了问题

  • 不要从单独的线程调用 SDL 视频/事件函数
于 2012-03-23T20:09:54.767 回答
0

我建议研究 SDL_EventFilter 和相关功能。它不是轮询队列输入法,因此不需要停止,但如果我没记错的话,它不会发生在主线程上,这可能正是您需要的性能,但可能会使代码复杂化。

于 2012-03-23T20:05:55.780 回答
0

您是否尝试过使用类似的东西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 毫秒,直到它再次开始返回事件。

于 2012-03-23T19:40:55.007 回答