c ++ - En måde at lave tastaturbegivenhedskø både responsiv og ikke tage hele CPU-strøm

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg laver et Sdl-spil, det er 2d skydespil. Jeg bruger SDL til at importere overflader og OpenGL til at tegne dem på skærmen (gør det fordi det fungerer langt hurtigere end bare SDL.) Jeg har to tråde i gang, en til behandling af ting og gengivelse, og en anden til input. Grundlæggende tager processoren en 1-2\% af min CPU, mens inputsløjfen tager 25\% (på quad-core, så det er 1 fuld kerne). Jeg forsøgte at lave SDL\_Delay (1) før hver while (SDL\_PollEvent(&keyevent))]] og det virker! Reducerer CPU-belastningen til 3\% for hele processen. Der er dog en ubehagelig bivirkning. Hele programindgangen er handicappet: det registrerer ikke alle tastetrykket, og for eksempel for at gøre tegnet bevæger sig, tager det nogle gange op til 3 sekunder basing tastaturet, for at den kan reagere.


Jeg har også forsøgt at løse det ved at bruge SDL\_PeepEvent() og SDL\_WaitEvent(), men det forårsager den samme (meget lange!) Forsinkelse.


Begivenhedsløkke kode:


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


Kode til beregning/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.
");
            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));
    }
}


Har nogen nogen ideer?

Bedste reference


Jeg har ikke virkelig oplevet SDL eller spilprogrammering, men her er nogle tilfældige ideer:


Reagerer på tilstandsændringer



Din kode:


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
}


Det betyder, at uanset det faktum, at der ikke sker noget på tastaturet, beregnes og indstilles data.


Hvis indstillingen af ​​data er dyr (hint: synkroniserede data), så vil det koste dig endnu mere.


SDL\_WaitEvent: målingstilstand



Du skal vente på, at en begivenhed skal ske, i stedet for at du har spundet, som forårsager 100\% brug af en processor.


Her er en variation af arrangementsløkken, jeg skrev til en test derhjemme:


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


Bemærk: Dette var enkelt gevind. Jeg vil tale om tråde senere.


Note 2: Pointen om de to bønders to 'm\_isRedrawing ...' er at tvinge omlægning, når en af ​​disse booleaner er sande, og når timeren stiller spørgsmålet. Normalt er der ingen genanvendelse.


Forskellen mellem min kode og din er, at du på et øjeblik ikke lader tråden 'vente'.


Tastaturhændelser



Der er et problem, jeg tror, ​​med din håndtering af tastaturhændelser.


Din kode:


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


Lad os sige, at du trykker VENSTRE og derefter HØJRE, så unpress VENSTRE. Hvad jeg forventer er:



  1. tryk på VENSTRE: tegnet går til venstre

  2. tryk på HØJRE: tegnet stopper (da både VENSTRE og HØJRE trykkes)

  3. unpress VENSTRE: tegnet går rigtigt, fordi RIGHT stadig er presset



I dit tilfælde har du:



  1. tryk på VENSTRE: tegnet går til venstre

  2. tryk på HØJRE: tegnet går til højre (som nu VENSTRE ignoreres, med x1=0)

  3. unpress VENSTRE: tegnet stopper (fordi du afbryder både x1 og x2.), på trods af at RIGHT stadig er presset



Du gør det forkert, fordi:



  1. Du reagerer uændret på en begivenhed, i stedet for at bruge en timer til at reagere på en situation hver anden millisekund

  2. Du blander begivenheder sammen.



Jeg finder linket senere, men hvad du skal gøre er at have et væld af booleanske tilstande til trykte nøgler. Noget som:


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


Du initialiserer det med størrelsen af ​​de tilgængelige nøgler:


m\_aKeyIsPressed(SDLK\_LAST, false)


Så på nøgle op begivenhed:


void MyContext::onKeyUp(const SDL\_KeyboardEvent & p\_oEvent)
{
    this->m\_aKeyIsPressed[p\_oEvent.keysym.sym] = false ;
}


og på nøgle ned:


void MyContext::onKeyDown(const SDL\_KeyboardEvent & p\_oEvent)
{
    this->m\_aKeyIsPressed[p\_oEvent.keysym.sym] = true ;
}


På denne måde, når du kontrollerer med jævne mellemrum (og når du tjekker , er en del vigtig), kender du tastaturets nøjagtige øjeblikkelige tilstand, og du kan reagere på det.


Tråde



Tråde er seje, men så skal du vide præcis, hvad du har at gøre med.


For eksempel kalder hændelsesgarnet følgende metode:


MainSurvivor->SetMovementDirection


Resolutionen (rendering) tråd kalder følgende metode:


MainSurvivor->MyTurn(Iterator);


Alvorligt deler du data mellem to forskellige tråde?


Hvis du er (og jeg ved du er), så har du enten:



  1. Hvis du ikke synkroniserer adgangene, er det et problem med datakoherens på grund af processor caches. Sæt det simpelthen, der er ingen garanti, et datasæt med en tråd vil blive betragtet som 'ændret' af den anden inden for en rimelig tid .

  2. Hvis du synkroniserede adgangene (med en mutex, atomvariabel osv.), har du et resultat, fordi du (for eksempel) låser/låser op en mutex mindst en gang pr. tråd pr. loop-iteration.



Hvad jeg ville gøre i stedet er at kommunikere ændringen fra en tråd til en anden (via en besked til en synkroniseret kø, for eksempel).


Anyway, threading er et stort problem, så du bør være fortrolig med konceptet, før du blander det med SDL og OpenGL. Herb Sutter s blog er en fantastisk samling af artikler om tråde. [26]


Hvad du skal gøre er:



  1. Prøv at skrive ting i en tråd, ved hjælp af begivenheder, meddelelser og timere.

  2. Hvis du finder ydeevneproblemer, skal du flytte begivenheden eller tegnetråden andre steder, men fortsæt med at arbejde med begivenheder, meddelte meddelelser og timere til at kommunikere



P.S .: Hvad er der galt med dine booleaner?



Du bruger selvfølgelig C ++ (f.eks. void GameWorld::Movement()), så at du bruger 1 eller 0 i stedet for true eller false, vil ikke gøre din kode klarere eller hurtigere.

Andre referencer 1


Hvis du initialiserer SDL på GameWorld::GenerateCycles() s tråd og MovementThread ringer GameWorld::Movement(), så har du et problem: [27]



  

      
  • Du skal ikke ringe SDL video/hændelsesfunktioner fra separate tråde

  •   


Andre referencer 2


Har du forsøgt at bruge noget som usleep(50000) i stedet for delay(1)?


Dette ville gøre din tråd sov i 50 msek mellem afstemning køen eller tilsvarende, du vil tjekke køen 20 gange pr. Sekund.


Også hvilken platform er dette på: Linux, Windows?


På Windows har du måske ikke usleep(), men du kan prøve select() som følger:


struct timeval tv;
tv.tv\_sec = 0;
tv.tv\_usec = 50000;
select(0, NULL, NULL, NULL, &tv);


Et andet forslag er at prøve afstemning i en stram sløjfe, indtil den stopper tilbagevendende begivenheder. Når der ikke er nogen hændelser i gang, fortsæt med at sove i 50 msek mellem afstemninger, indtil det begynder at returnere begivenheder igen.

Andre referencer 3


Jeg foreslår at kigge på SDL\_EventFilter og de relaterede funktioner. Det er ikke en afstemningskø indgangsmetode, så det kræver ikke opbevaring, selvom hvis jeg husker det korrekt, sker det ikke på hovedtråden, hvilket kan være præcis hvad du har brug for til ydeevne, men kan komplicere koden.