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

我也尝试过使用 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));
    }
}

有没有人有任何想法?

最佳答案

我对 SDL 或游戏编程并没有真正的经验,但这里有一些随机的想法:

对状态变化使用react

您的代码:

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...” bool 值的要点是当这些 bool 值之一为真时强制重绘,并且当计时器提出问题时。通常,没有重绘。

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

键盘事件

我想,您对键盘事件的处理存在问题。

您的代码:
        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。我期望的是:
  • press LEFT : 角色向左
  • press RIGHT :字符停止(同时按下 LEFT 和 RIGHT)
  • unpress LEFT :字符向右移动,因为 RIGHT 仍然按下

  • 在你的情况下,你有:
  • press LEFT : 角色向左
  • 按 RIGHT :字符向右移动(因为现在忽略 LEFT,x1 = 0)
  • unpress LEFT :字符停止(因为您取消设置 x1 和 x2。),尽管 RIGHT 仍然按下

  • 你做错了,因为:
  • 您正在立即对事件使用react,而不是每隔 n 毫秒使用计时器对情况使用react
  • 您正在将事件混合在一起。

  • 我稍后会找到链接,但您应该做的是为按下的键设置一组 bool 状态。就像是:
    // C++ specialized vector<bool> is silly, but...
    std::vector<bool> m_aKeyIsPressed ;
    

    您使用可用 key 的大小对其进行初始化:
    m_aKeyIsPressed(SDLK_LAST, false)
    

    然后,在 key up 事件上:
    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 ;
    }
    

    这样,当您定期检查时(以及检查零件的时间很重要),您就可以知道键盘的确切瞬时状态,并且可以对其使用react。

    线程

    线程很酷,但是,您必须确切地知道您在处理什么。

    例如,事件循环线程调用以下方法:
    MainSurvivor->SetMovementDirection
    

    解析(渲染)线程调用以下方法:
    MainSurvivor->MyTurn(Iterator);
    

    说真的,您是否在两个不同的线程之间共享数据?

    如果你是(我知道你是),那么你有:
  • 如果您没有同步访问,这是由于处理器缓存导致的数据一致性问题。简而言之,不能保证一个线程设置的一个数据会在合理的时间内被另一个线程视为“更改”。
  • 如果您确实同步了访问(使用互斥锁、原子变量等),那么您的性能会受到影响,因为您(例如)每次循环迭代每个线程至少锁定/解锁一次互斥锁。

  • 相反,我会做的是将更改从一个线程传递到另一个线程(例如,通过向同步队列发送消息)。

    无论如何,线程是一个沉重的问题,因此在将其与 SDL 和 OpenGL 混合之前,您应该熟悉这个概念。 Herb Sutter's blog 是关于线程的精彩文章集合。

    你应该做的是:
  • 尝试在一个线程中编写事物,使用事件、发布的消息和计时器。
  • 如果发现性能问题,则将事件或绘图线程移到别处,但继续使用事件、发布的消息和计时器进行通信

  • PS:你的 bool 值有什么问题?

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

    10-08 09:18
    查看更多