SuomiGameHUB

Maailma tarvitsee pelejä

C++/SDL2 liikkuminen ja delta time

Vaikka olemme saavuttaneet liikkumisen perustoiminnallisuuden, sen toteutuksessa on parannettavaa. Nykyinen liikkumismekanismi, joka perustuu näppäinten painallusten suoraan käsittelyyn, aiheuttaa viivettä ja epätasaista liikettä. Tämä johtuu näppäinten toiston viiveestä, joka on suunniteltu kirjoittamista varten, mutta muodostuu haitaksi peleissä, joissa nopea ja jatkuva reagointi on avainasemassa.

Ongelman juuri on Windowsin toistoviive, joka aiheuttaa liikkumisen viivästymisen ja epäjohdonmukaisuuden. Käyttäjän painaessa näppäintä, liike tapahtuu ensin hitaasti ja kiihtyy vasta hetken päästä. Tämä ei ole ihanteellista pelikokemuksen kannalta, jossa tavoitellaan suoraviivaista ja välitöntä reagointia pelaajan komentoihin. Onneksi on olemassa keinoja, joilla voimme kiertää tämän rajoituksen ja tarjota pelaajille paremman pelikokemuksen.

Ratkaisun avain on siirtyminen näppäinten tilojen jatkuvaan seurantaan. Sen sijaan, että keskityttäisiin ainoastaan siihen, onko näppäin parhaillaan alhaalla, voimme rekisteröidä näppäinten painallukset ja vapautukset muuttamaan näppäinten tilaa ohjelmamme sisällä. Tämä tarkoittaa, että luomme loogisen taulukon, jossa jokainen näppäin saa oman paikkansa ja tilansa – painettu tai ei painettu. Tätä varten määritämme näppäimistön koon kattamaan kaikki mahdolliset näppäimet taulukossa (array), käyttäen sen alustamisen kokoon makroa ’KEYBOARD_SIZE’, jonka asetamme arvoon 282. Tämä luku kattaa laajasti eri näppäimistöjen näppäinten määrän.

Näppäinten tilojen seurannalla voimme sitten muuttaa pelihahmon liikettä sujuvammaksi ja tarkemmaksi. Kun pelaaja painaa liikkenäppäintä, kyseisen näppäimen tila päivittyy taulukossa ’painettuksi’. Tämän tilatiedon avulla pelihahmo liikkuu haluttuun suuntaan ilman viivettä. Samalla tavalla, kun näppäin vapautetaan, sen tila päivittyy ’ei painetuksi’, mikä pysäyttää liikkeen. Tämä menetelmä mahdollistaa jatkuvan ja viiveettömän liikkeen hallinnan, parantaen näin pelattavuutta merkittävästi. Muista alustaa näppäimistön tilataulukko alussa nollilla. Näin kaikki näppäimet ovat oletusarvoisesti ei-painettuja (false).

Liikkeessä on edelleen havaittavissa pientä epätasaisuutta, joka voi johtua siitä, että pelin liikkeen päivitys ei ole sidottu aikaan, vaan ohjelman suorituskertojen määrään. Tämä johtaa siihen, että pelin nopeus voi vaihdella eri suorituskykyisillä laitteilla.

Jotta voisimme saavuttaa tasaisen ja laitteesta riippumattoman liikkeen pelissämme, päädymme hyödyntämään delta-aikaa. Delta-aika tarkoittaa aikaa, joka kuluu kahden päivityssyklin välillä, mahdollistaen pelilogiikan sitomisen todelliseen kuluneeseen aikaan, ei pelkästään päivityskertoihin. Toteutamme delta-ajan käytön hankkimalla nykyisen ajan SDL_GetTicks-funktiolla pelisilmukan jokaisen iteraation alussa ja laskemalla kuluneen ajan edellisestä päivityksestä.

Tämä menetelmä mahdollistaa liikkumisnopeuden säätämisen riippumatta laitteiston suorituskyvystä, tarjoten pelaajalle tasaisen pelikokemuksen. Tätä varten lisäämme muuttujat edellisen ja nykyisen ajan tallentamiseksi sekä laskemme delta-ajan näiden avulla. Pelihahmon liikuttamiseen käytämme nyt delta-aikaa, mikä varmistaa, että liike on tasainen ja ennustettavissa riippumatta suoritustiheydestä.

Kuitenkin törmäämme uuteen haasteeseen: koska SDL_Rect-komponentit ovat kokonaislukutyyppiä ja delta-aika tuottaa liukulukuarvoja, liikkeen pienet muutokset eivät välttämättä näy suoraan pelihahmon sijainnissa. Ratkaisuna otamme käyttöön liukulukumuuttujat pelihahmon sijainnin tallentamiseen ja päivitämme SDL_Rect-komponentit vastaamaan näitä arvoja vain, kun todellinen sijaintimuutos on tapahtunut.

Kokonaisuudessaan, kun säädämme pelihahmon nopeuden ja otamme käyttöön delta-ajan, pystymme saavuttamaan sujuvan ja laitteesta riippumattoman liikkeen pelissämme. Tämä avaa ovet monipuolisemmalle pelinkehitykselle, mahdollistaen esimerkiksi erilaisten pelimekaniikkojen, kuten vihollisten ja esteiden, lisäämisen peliin.

#include <SDL.h>
#include <SDL_image.h>
#include <string>
#include <Windows.h>
#define KEYBOARD_SIZE 282

int main(int argc, char* argv[])
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window = NULL;
  SDL_Surface* screenSurface = NULL;
  SDL_Surface* kuva = NULL;
  SDL_Surface* o_kuva = NULL;
  window = SDL_CreateWindow("SGH SDL Esimerkki", 50, 50, 1280, 720, SDL_WINDOW_SHOWN);
  int imgFlags = IMG_INIT_PNG;
  IMG_Init(imgFlags);
  screenSurface = SDL_GetWindowSurface(window);

  char path[MAX_PATH];
  GetModuleFileNameA(NULL, path, MAX_PATH);
  std::string exePath(path);
  std::string dirPath = exePath.substr(0, exePath.find_last_of("\\/"));
  std::string kuva_osoite = dirPath + "\\kuva.png";

  kuva = IMG_Load(kuva_osoite.c_str());
  o_kuva = SDL_ConvertSurface(kuva, screenSurface->format, 0);

  float pos_x = 100.0;
  float pos_y = 100.0;

  SDL_Event e;
  bool quit = false;
  bool key[KEYBOARD_SIZE] = { 0 };
  Uint32 edellinenAika = SDL_GetTicks();
  int nopeus = 300;
  
  while (quit == false)
  {
    Uint32 nykyinenAika = SDL_GetTicks();
    Uint32 deltaTimeMS = nykyinenAika - edellinenAika;
    float deltaTime = deltaTimeMS / 1000.0f;
    edellinenAika = nykyinenAika;

    SDL_FillRect(screenSurface, NULL, SDL_MapRGB(screenSurface->format, 0xFF, 0xFF, 0xFF));
    while (SDL_PollEvent(&e))
    {
      if (e.type == SDL_QUIT) quit = true;
      else if (e.type == SDL_KEYDOWN)
      {
        int scancode = e.key.keysym.scancode;
        key[scancode] = true;
      }
      else if (e.type == SDL_KEYUP)
      {
        int scancode = e.key.keysym.scancode;
        key[scancode] = false;
      }
    }

    if (key[SDL_SCANCODE_LEFT]) pos_x -= nopeus * deltaTime;
    if (key[SDL_SCANCODE_RIGHT]) pos_x += nopeus * deltaTime;
    if (key[SDL_SCANCODE_UP]) pos_y -= nopeus * deltaTime;
    if (key[SDL_SCANCODE_DOWN]) pos_y += nopeus * deltaTime;

    SDL_Rect kuvaRect = { pos_x, pos_y, 100, 100 };
    SDL_BlitSurface(o_kuva, NULL, screenSurface, &kuvaRect);
    SDL_UpdateWindowSurface(window);
  }
  SDL_FreeSurface(kuva);
  SDL_FreeSurface(screenSurface);
  SDL_DestroyWindow(window);
  SDL_Quit();
  return 0;
}

 

Edellinen osa: Kontrollit C++/SDL2-pelimoottoriin