[2] Pravljenje igre Sokoban u C++ [programiranje]

1

[2] Pravljenje igre Sokoban u C++ [programiranje]

offline
  • Srđan Tot
  • Am I evil? I am man, yes I am.
  • Pridružio: 12 Jul 2005
  • Poruke: 2483
  • Gde živiš: Ljubljana

Napisano: 02 Dec 2012 16:45

Sadržaj


[1] Uvod
[2] Igra i stanja igre
[3] Glavna petlja
[4] Glavni meni
[5] Logika za učitavanje i pomeranje
[6] Prikaz i igranje nivoa
[7] Završavanje i testiranje igre




[1] Uvod


U ovom delu ćemo napraviti igru uz pomoć objektno orijentisanog programiranja (OOP). Podelićemo program na logične celine i time ćemo sebi olakšati pisanje programa. Svaka celina će videti samo onaj deo programa koji je za nju bitan, a to znači da će kod biti mnogo jednostavniji za čitanje i imaćemo manje šansi da napravimo grešku prilikom pisanja.

Za početak, napravite nov projekat, dajte mu ime sfml-sb i podesite sve što je potrebno za upotrebu SFML-a kao što smo uradili u prethodnom uputstvu. Za sada nema potrebe da pravite main.cpp datoteku.



[2] Igra i stanja igre


Da bismo lakše napravili igru, podelićemo je na deo koji je zadužen za kreiranje prozora, čitanje poruka i slično, i na delove koji će obrađivati konkretna stanja igre. U našem primeru ćemo imati samo dva stanja: jedno za glavni meni, jedno za igranje igre.

Prvo ćemo definisati klasu Game, koja će se brinuti o svemu što nema veze sa konkretnim stanjem igre, od kreiranja prozora, preko čitanja poruka, do zatvaranja prozora. Osim rada sa prozorom, ona će morati da zna i koja su sve stanja registrovana i koje je trenutno stanje igre.


Datoteka game.h#ifndef GAME_H_ #define GAME_H_ #include <SFML> #include <map> class GameState; class Game { public:    Game();    ~Game();    void addState(int id, GameState *state);    int run(int id);    void changeState(int id);    void close();    sf::RenderWindow *getWindow();    enum States    {       MainMenuState,       PlayState    }; private:    sf::RenderWindow *window;    std::map<int> states;    int next; }; #endif /* GAME_H_ */

Konstruktor i destruktor ćemo iskoristiti za pripremanje/oslobađanje potrebnih objekata za rad igre.
Funkcija addState će služiti za registrovanje stanja igre. Samo stanja registrovana ovom funkcijom će biti na raspolaganju prilikom pokretanja igre.
Pozivom funkcije run ćemo pokrenuti glavnu petlju i proslediti početno stanje igre.
changeState se koristi u toku izvršavanja igre, kada igra treba da pređe iz jednog u drugo stanje.
Funkcija close služi za zatvaranje prozora i gašenje igre, dok preko funkcije getWindow stanja igre mogu da dostupaju do prozora za crtanje.

Privatne promenljive ćemo koristiti za čuvanje prozora, registrovanih stanja i stanja u koje igra treba da pređe u sledećem izvršavanju glavne petlje.

Sve ovo ćemo detaljnije proći kada stignemo do implementacije klase Game.

Stanja igre moraju imati par unapred definisanih funkcija koje će služiti kao interfejs za komunikaciju između njih i klase Game.


Datoteka gamestate.h#ifndef GAMESTATE_H_ #define GAMESTATE_H_ #include <SFML> #include "game.h" class GameState { public:    GameState();    virtual ~GameState();    virtual void enter();    virtual void leave();    virtual void event(sf::Event *event);    virtual void update(float elapsedTime);    virtual void render() = 0; protected:    Game *game;    friend class Game; }; #endif /* GAMESTATE_H_ */

Ovo je samo osnovna klasa za stanja koja mora biti nasleđena da bi mogli da implementiramo drugačije ponašanje za svako stanje. U svakom slučaju, sva stanja nasleđivanjem ove klase dobijaju sve funkcije koje su ovde definisane.

Funkcije enter i leave će klasa Game pozvati svaki put kada uđe/izađe iz trenutnog stanja. Na taj način se stanja mogu pravilno pripremiti prilikom ulazka, i osloboditi resurse prilikom izlaska.
Funkcija event će biti pozvana svaki put kada se nešto desi s prozorom ili kada igrač nešto uradi na mišu ili tastaturi. Preko ove funkcije će stanja moći da reaguju na igračeve akcije.
Funkciju update će stanja koristiti za osvežavanje pozicija, boja, veličina i svih drugih osobina koje će imati objekti u njima. Kao parametar će dobiti koliko vremena je prošlo od zadnjeg osvežavanja, tako možemo animirati objekte jednako dugo na svim računarima, bez obzira da li su brzi ili spori.
Preko funkcije render će stanja crtati objekte koji moraju biti na ekranu.

Promenljiva game će biti postavljena od strane klase Game prilikom registrovanja stanja, i služiće za dostup do prozora za crtanje, za promenu trenutno aktivnog stanja ili za gašenje igre.

Klasa GameState implementira sve funkcije osim render, tako da ostala stanja ne moraju da implementiraju funkcije koje ih ne zanimaju.


Datoteka gamestate.cpp#include "gamestate.h" GameState::GameState() :    game(0) { } GameState::~GameState() { } void GameState::enter() { } void GameState::leave() { } void GameState::event(sf::Event* event) { } void GameState::update(float elapsedTime) { }



[3] Glavna petlja


Glavna petlja je srce cele igre. Zadužena je čitanje poruka kao što su klik miša ili pritisak tastera na tastaturi, i obradu istih. Mi ćemo našu glavnu petlju tako organizovati da ona samo čita poruke i prosleđuje ih objektu koji predstavlja trenutno stanje igre. U tom objektu će se vršiti obrada poruka. Glavnu petlju ćemo implementirati u klasi Game. Pre nego što je napišemo, obradićemo funkcije koje pripremaju sve za početak izvršavanja, a kasnije i oslobađaju sve resurse pre gašenja igre.


Datoteka game.cpp#include <SFML> #include "game.h" #include "gamestate.h" Game::Game() :    next(-1) {    window = new sf::RenderWindow(sf::VideoMode::GetDesktopMode(), "SFML Sokoban", sf::Style::Fullscreen); } Game::~Game() {    for (std::map<int>::iterator it = states.begin(); it != states.end(); ++it)       delete it->second;    delete window; } void Game::addState(int id, GameState* state) {    states[id] = state;    state->game = this; } void Game::changeState(int id) {    next = id; } void Game::close() {    window->Close(); } sf::RenderWindow *Game::getWindow() {    return window; }

Konstruktor uz pomoć SFML kreira prozor koji će zauzeti ceo ekran.
Destruktor je zadužen za oslobađanje svih registrovanih stanja i gašenje prozora.
Funkcija addState samo dodaje prosleđeno stanje u interni registar i povezuje ga sa prosleđenim id brojem, koji se može koristiti kasnije za lakše pronalaženje registrovanih stanja.
Funkcijom changeState ćemo obavestiti glavnu petlju da u sledećoj iteraciji želimo da promenimo trenutno stanje. Ovu funkciju ćemo koristiti za prelazak iz glavnog menija u samu igru, i nazad.
Funkcije close i getWindow su prilično jasne Smile Koriste se za dostup do prozora za crtanje i njegovo gašenje što će izazvati prekid glavne petlje.

Sada možemo napisati i glavnu petlju:


Datoteka game.cppint Game::run(int id) {    sf::Clock clock;    next = -1;    GameState* state = states[id];    state->enter();    while (window->IsOpened())    {       sf::Event event;       while (window->GetEvent(event))       {          if (event.Type == sf::Event::Closed)             window->Close();          state->event(&event);       }       state->update(clock.GetElapsedTime());       clock.Reset();       window->Clear();       state->render();       window->Display();       if (next != -1)       {          state->leave();          state = states[next];          state->enter();          next = -1;       }    }    state->leave();    return EXIT_SUCCESS; }

Na početku inicijalizujemo interni sat, postavljamo aktivno stanje i pozivamo njegovu funkciju enter, zatim se pokreće glavna petlja, a na kraju funkcija leave trenutno aktivnog stanja.
Glavna petlja će se vrteti sve dok je prozor otvoren. Na ulasku u petlju se proveravaju poruke koje su stigle od završetka zadnje iteracije i prosleđuju se trenutno aktivnom stanju preko funkcije event. U slučaju da smo dobili poruku da je korisnik pritisnuo X na prozoru, Alt+F4 ili neku drugu prečicu koja treba da zatvori prozor, dobićemo poruku sf::Event::Closed, a u tom slučaju zatvaramo prozor.

Posle obrade poruka, dajemo stanju šansu da osveži svoje objekte tako što ćemo pozvati funkciju update, a zatim pozivamo i funkciju render koja će iscrtati objekte na ekran.

Pre završetka iteracije, proveravamo da li je potrebno promeniti stanje. U slučaju da jeste, pozivamo funkciju leave za trenutno stanje, menjamo stanje i obaveštavamo ga da se pripremi tako što pozivamo funkciju enter.

Ovim se iteracija glavne petlje završava i odmah zatim počinje nova sve dok se prozor ne zatvori. Pisanjem glavne petlje na ovaj način smo kompletnu logiku za obradu poruka, osvežavanje i crtanje objekata izvukli iz klase Game i omogućili svakom stanju da na svoj jedinstven način implementira sve što mu je potrebno.



[4] Glavni meni


Ovo je prvo stanje koje ćemo napraviti. Zadatak glavnog menija je da prikaže dve opcije (početak igre i izlaz)... da bi prozor izgledao lepše, uz te dve opcije ćemo prikazati i pozadinsku sliku.

Klasa glavnog menija će naslediti klasu GameState i dodati funkcije i promenljive koje su joj potrebne za rad:


Datoteka mainmenustate.h#ifndef MAINMENUSTATE_H_ #define MAINMENUSTATE_H_ #include <vector> #include "gamestate.h" class MainMenuState: public GameState { public:    MainMenuState();    void enter();    void leave();    void event(sf::Event *event);    void update(float elapsedTime);    void render(); private:    enum Menu    {       Play,       Exit,       First = Play,       Last = Exit    };    sf::Image *bgImage;    sf::Sprite *bgSprite;    std::vector<sf> menu;    float time;    int currentMenu; }; #endif /* MAINMENUSTATE_H_ */

Dodane interne promenljive služe za učitavanje i prikazivanje pozadinske slike, i pripremanje, animiranje i prikaz opcija menija.

Krenimo redom kroz funkcije koje ova klasa implementira. Prilikom ulaska u stanje se poziva funkcija enter čiji je cilj da pripremi sve resurse za dalji rad:


Datoteka mainmenustate.cppvoid MainMenuState::enter() {    bgImage = new sf::Image();    if (!bgImage->LoadFromFile("resources/mainmenu.jpg"))       bgImage->LoadFromFile("../resources/mainmenu.jpg");    bgSprite = new sf::Sprite();    bgSprite->SetImage(*bgImage);    float scaleX, scaleY, scale;    scaleX = (float)(game->getWindow()->GetWidth()) / bgImage->GetWidth();    scaleY = (float)(game->getWindow()->GetHeight()) / bgImage->GetHeight();    scale = scaleX > scaleY ? scaleX : scaleY;    bgSprite->SetScale(scale, scale);    menu.resize(Last + 1, 0);    menu[Play] = new sf::String("Play");    menu[Exit] = new sf::String("Exit");    float centerX, centerY;    sf::FloatRect rect;    rect = menu[Play]->GetRect();    centerX = (rect.Right - rect.Left) / 2;    centerY = (rect.Bottom - rect.Top) / 2;    menu[Play]->SetCenter(centerX, centerY);    menu[Play]->SetSize(40);    menu[Play]->SetPosition(100, 100);    rect = menu[Exit]->GetRect();    centerX = (rect.Right - rect.Left) / 2;    centerY = (rect.Bottom - rect.Top) / 2;    menu[Exit]->SetCenter(centerX, centerY);    menu[Exit]->SetSize(40);    menu[Exit]->SetPosition(100, 155);    time = 0; }

Sve resurse (slike i nivoe) ćemo snimiti u direktorijum resources u projektu. Za pozadinu glavnog menija ćemo iskoristiti sliku mainmenu.jpg.

Korišćenjem klasa sf::Image i sf::Sprite možemo učitati i prikazati slike. Pošto želimo da se slika ne deformiše na različitim rezolucijama, moramo da izračunamo odnos širine i visine prozora, i na osnovu toga prilagodimo veličinu slike.

Nakon učitavanja pozadine, kreiramo dva objekta tipa sf::String koji znaju da prikažu tekst na ekranu. Uz pomoć te klase možemo lako da podesimo lokaciju, veličinu i druge osobine teksta.

Funkcija leave mora da oslobodi sve resurse koji su kreirani u stanju:


Datoteka mainmenustate.cppvoid MainMenuState::leave() {    delete bgSprite;    delete bgImage;    for (std::vector<sf>::iterator it = menu.begin(); it != menu.end(); ++it)       delete *it;    menu.clear(); }

Sledeća funkcija koju ćemo implementirati je event i ona će obrađivati korisničke akcije:


Datoteka mainmenustate.cppvoid MainMenuState::event(sf::Event* event) {    if (event->Type == sf::Event::KeyPressed)    {       switch (event->Key.Code)       {          case sf::Key::Up:             --currentMenu;             break;          case sf::Key::Down:             ++currentMenu;             break;          case sf::Key::Return:             if (currentMenu == Play)                game->changeState(Game::PlayState);             else if (currentMenu == Exit)                game->close();             break;          default:             break;       }       if (currentMenu <First> Last)          currentMenu = First;       for (std::vector<sf>::iterator it = menu.begin(); it != menu.end(); ++it)          (*it)->SetScale(1, 1);    } }

U ovom delu nas interesuju samo poruke koje dolaze sa tastature. U slučaju da igrač pritisne gore ili dole, menjamo trenutno aktivnu akciju. U slučaju da igrač pritisne enter, menjamo trenutno stanje na Game::PlayState ili izlazimo iz igre, u zavisnosti od izabrane akcije.

Na kraju resetujemo veličine akcija na normalu da bi igraču na vizualan način prikazali koja je aktivna akcija.

U update funkciji ćemo animirati trenutno aktivnu akciju tako što ćemo joj menjati veličinu:


Datoteka mainmenustate.cppvoid MainMenuState::update(float elapsedTime) {    time += elapsedTime;    menu[currentMenu]->SetScale(1.2f + sin(time * 15) * 0.05f, 1.2f + sin(time * 15) * 0.05f); }

Aktivna akcija će polako pulsirati, dok će neaktivna biti standardne veličine.

Za kraj nam ostaje funkcija render koja će prikazati pozadinsku sliku i akcije:


Datoteka mainmenustate.cppvoid MainMenuState::render() {    game->getWindow()->Draw(*bgSprite);    for (std::vector<sf>::iterator it = menu.begin(); it != menu.end(); ++it)       game->getWindow()->Draw(**it); }

To je sve što je potrebno da bi glavni meni radio. Kao što vidite, kompletna logika se nalazi u event funkciji... ostale funkcije su tu samo da bi se brinule o resursima i crtanju.



[5] Logika za učitavanje i pomeranje


Da bismo sebi olakšali pisanje programa, napravićemo jednu pomoćnu klasicu čiji zadatak će biti da učitava nivoe i vrši provere i pomeranja objekata po mapi. Vrlo bitna stvar je da ova klasa neće raditi ništa s grafikom. Ona će imati samo memorijski model nivoa, a druge klase će taj model moći da iskoriste za crtanje.

U ovoj klasi ćemo definisati vrste polja koja se mogu nalaziti u mapi, pokrete koje igrač može da napravi i, naravno, mapu nivoa:


Datoteka level.h#ifndef LEVEL_H_ #define LEVEL_H_ #include <vector> #include <SFML> class Level { public:    Level();    enum PlayField    {       Floor,       Wall,       Destination,       Empty    };    enum Direction    {       Left = sf::Key::Left,       Right = sf::Key::Right,       Up = sf::Key::Up,       Down = sf::Key::Down    };    bool loadNext();    void reload();    bool isFinished();    void move(Direction dir);    bool load();    PlayField getField(int x, int y);    int getCrate(int x, int y);    int level;    int width, height;    std::vector<PlayField> playField;    sf::Vector2i player;    std::vector<sf> crates;    Direction lastDir; }; #endif /* LEVEL_H_ */

PlayField predstavlja sve moguće tipove polja na mapi. Empty će predstavljati prazno polje za koje ne treba ništa crtati na ekranu, Wall je zid kroz kojeg igrač ne može da prođe, Floor je pod preko kojeg igrač može da se kreće, a Destination je polje na kojem na kraju moramo postaviti kutiju.

Enumeracija Direction je lista pokreta koje igrač može napraviti.

Od stalih polja su zanimljivi još playField koje služi za čuvanje mape, player koje čuva poziciju igrača, i crates u kojem su sačuvane pozicije svih kutija.

Pređimo sad na funkcije... počećemo od funkcija za učitavanje nivoa:


Datoteka level.cppbool Level::loadNext() {    ++level;    return load(); } void Level::reload() {    load(); } bool Level::load() {    playField.clear();    crates.clear();    lastDir = Down;    std::ifstream lvlFile;    std::stringstream fileName;    fileName << "resources/level" << level << ".txt";    lvlFile.open(fileName.str().c_str());    if (!lvlFile.is_open())    {       fileName.str("");       fileName << "../resources/level" << level << ".txt";       lvlFile.open(fileName.str().c_str());    }    if (!lvlFile.is_open())       return false;    std::string line;    int x, y;    PlayField field;    lvlFile >> width >> height;    playField.resize(width * height, Empty);    y = 0;    std::getline(lvlFile, line);    while (y < height)    {       std::getline(lvlFile, line);       for (x = 0; x < width; ++x)       {          switch (line[x])          {             case '#':                field = Wall;                break;             case 'C':                crates.push_back(sf::Vector2i(x, y));                field = Floor;                break;             case 'P':                player = sf::Vector2i(x, y);                field = Floor;                break;             case '-':                field = Floor;                break;             case 'D':                field = Destination;                break;             default:                field = Empty;                break;          }          playField[x + y * width] = field;       }       ++y;    }    lvlFile.close();    return true; }

Funkcije loadNext i reload su samo pomoćne funkcije koje zovu load da bi učitale sledeći ili ponovo učitale trenutni nivo. Kompletna logika za učitavanje se nalazi u funkciji load.

Na početku otvaramo datoteku čije ime konstruišemo uz pomoć promenljive level, koja drži broj trenutno aktivnog nivoa. U slučaju da datoteka ne postoji ili je došlo do greške, vraćamo false što će obavestiti ostatak igre da nema više nivoa za igranje.

U slučaju da datoteka postoji i možemo da je otvorimo, prvo pročitamo širinu i visinu nivoa, i na osnovu ta dva podatka kreiramo dovoljno velik niz da u njemu sačuvamo celu mapu.

Zatim čitamo datoteku red po red, i na osnovu predefinisanih znakova popunjavamo mapu, postavljamo poziciju igrača, i dodajemo kutije. Posle čitanja datoteke, imamo sve podatke potrebne za igranje igre.

Da bismo lakše dostupali do podataka o mapi, napravićemo i par pomoćnih funkcija:


Datoteka level.cppbool Level::isFinished() {    for (std::vector<sf>::iterator it = crates.begin(); it != crates.end(); ++it)       if (getField((*it).x, (*it).y) != Destination)          return false;    return true; } Level::PlayField Level::getField(int x, int y) {    return playField[x + y * width]; } int Level::getCrate(int x, int y) {    for (std::vector<sf>::size_type i = 0; i < crates.size(); ++i)       if (crates[i].x == x && crates[i].y == y)          return i;    return -1; }

Funkcija isFinished proverava da li je ispod svake kocke polje Destination. Ako jeste to znači da smo sve kocke postavili na pravo mesto i da je nivo završen.

Funkcija getField je nam omogućava da dođemo do vrste polja preko x, y koordinata u mapi, dok funkcija getCrate proverava da li se na nekoj x,y poziciji nalazi kocka.

Sad kada imamo ove pomoćne funkcije, pisanje logike za pomeranje će biti prilično lako:


Datoteka level.cppvoid Level::move(Direction dir) {    lastDir = dir;    int dX = 0, dY = 0;    switch (lastDir)    {       case Left:          dX = -1;          break;       case Up:          dY = -1;          break;       case Right:          dX = 1;          break;       default:          dY = 1;          break;    }    sf::Vector2i newPos = sf::Vector2i(player.x + dX, player.y + dY);    int crate = getCrate(newPos.x, newPos.y);    sf::Vector2i newCratePos = sf::Vector2i(newPos.x + dX, newPos.y + dY);    if (crate != -1)    {       if (getField(newCratePos.x, newCratePos.y) != Wall && getCrate(newCratePos.x, newCratePos.y) == -1)       {          crates[crate] = newCratePos;          player = newPos;       }    }    else if (getField(newPos.x, newPos.y) != Wall)       player = newPos; }

Na počeku određujemo u kom smeru igrač želi da se pomeri i proveravamo da li se ispred njega nalazi kocka. U slučaju da kocka nije ispred njega, proveravamo još da li je možda ispred zid. Ako nije, upišemo novu poziciju igrača.

U slučaju da je ispred igrača bila kocka, moramo proveriti još jedno polje ispred. Ako na sledećem polju nije zid ili neka druga kocka, znači da možemo slobodno da "poguramo" kocku napred i upišemo novu poziciju igrača.

I, to je to... to je kompletna logika za učitavanje nivoa i pomeranje igrača po njoj.



[6] Prikaz i igranje nivoa


Zahvaljujući prethodnoj klasi, pisanje stanja za igranje će biti prilično jednostavno. U direktorijum za resurse, moramo dodati slike za polja na mapi, za igrača i kutije. Naravno, da bi sve izgledalo lepše, dodaćemo i sliku za pozadinu.

Kreiranje stanja za igranje će biti skoro isto kao i stanja za glavni meni. Prvo moramo napraviti klasu i dodati joj promenljive koje su joj potrebne za rad:


Datoteka playstate.h#ifndef PLAYSTATE_H_ #define PLAYSTATE_H_ #include <vector> #include "gamestate.h" #include "level.h" class PlayState: public GameState { public:    PlayState();    void enter();    void leave();    void event(sf::Event *event);    void update(float elapsedTime);    void render(); private:    void calculateZoom();    sf::Image *bgImage;    sf::Sprite *bgSprite;    sf::String *status;    Level *level;    sf::View *view;    std::vector<sf> playField;    sf::Image *crate;    sf::Image *player;    sf::Sprite *sprite; }; #endif /* PLAYSTATE_H_ */

Osim standardnih funkcija, ova klasa ima funkciju calculateZoom čiji cilj je da odredi koliko slika treba da bude povećana ili smanjena da bi se videla cela mapa.

Dodane promenljive su uglavnom za učitavanje i prikaz slika i teksta, ali imamo i promenljivu level koja će držati instancu klase za rad sa nivoima.

Pređimo na implementaciju:


Datoteka playstate.cppvoid PlayState::enter() {    bgImage = new sf::Image();    if (!bgImage->LoadFromFile("resources/play.jpg"))       bgImage->LoadFromFile("../resources/play.jpg");    bgSprite = new sf::Sprite();    bgSprite->SetImage(*bgImage);    float scaleX, scaleY, scale;    scaleX = (float)(game->getWindow()->GetWidth()) / bgImage->GetWidth();    scaleY = (float)(game->getWindow()->GetHeight()) / bgImage->GetHeight();    scale = scaleX > scaleY ? scaleX : scaleY;    bgSprite->SetScale(scale, scale);    status = new sf::String("Press F5 to restart level or Esc to quit.");    status->SetSize(16);    status->SetPosition(10, game->getWindow()->GetHeight() - 30);    level = new Level();    level->loadNext();    calculateZoom();    playField.resize(Level::Empty);    playField[Level::Floor] = new sf::Image();    if (!playField[Level::Floor]->LoadFromFile("resources/floor.png"))       playField[Level::Floor]->LoadFromFile("../resources/floor.png");    playField[Level::Wall] = new sf::Image();    if (!playField[Level::Wall]->LoadFromFile("resources/wall.png"))       playField[Level::Wall]->LoadFromFile("../resources/wall.png");    playField[Level::Destination] = new sf::Image();    if (!playField[Level::Destination]->LoadFromFile("resources/destination.png"))       playField[Level::Destination]->LoadFromFile("../resources/destination.png");    crate = new sf::Image();    if (!crate->LoadFromFile("resources/crate.png"))       crate->LoadFromFile("../resources/crate.png");    player = new sf::Image();    if (!player->LoadFromFile("resources/player.png"))       player->LoadFromFile("../resources/player.png");    sprite = new sf::Sprite(); } void PlayState::leave() {    delete bgSprite;    delete bgImage;    delete status;    delete level;    for (std::vector<sf>::iterator it = playField.begin(); it != playField.end(); ++it)       delete *it;    playField.clear();    delete crate;    delete player;    delete sprite;    delete view;    view = 0; } void PlayState::calculateZoom() {    if (view != 0)       delete view;    float ratio = (float)(game->getWindow()->GetWidth()) / (float)(game->getWindow()->GetHeight());    float count = (float)(level->width + 1) / ratio > level->height + 1 ? (float)(level->width + 1) / ratio : level->height + 1;    float width = 64.0f * ratio * count;    float height = 64.0f * count;    view = new sf::View();    view->SetFromRect(sf::FloatRect(0, 0, width, height));    view->Move((32.0f * ratio) * ((float)(level->width) / ratio - count), 32.0f * ((float)(level->height) - count)); }

Isto kao i u klasi za glavni meni, i ovde u funkciji enter učitavamo potrebne objekte među kojima je i prvi nivo. Po završetku ove funkcije, klasa je spremna za obradu ulaznih poruka i crtanje. Funkcija leave će se pobrinuti da na kraju oslobodimo sve resurse koje smo zauzeli.

Funkcija calculateZoom na osnovu širine i visine prozora i mape, postavlja veličinu objekata za crtanje tako da se vidi cela mapa. To radimo da bi što bolje iskoristili prostor na velikim rezolucijama, ali i da ne bi izgubili parče mape na manjim.


Datoteka playstate.cppvoid PlayState::event(sf::Event* event) {    if (event->Type == sf::Event::KeyPressed)    {       switch (event->Key.Code)       {          case sf::Key::Escape:             game->changeState(Game::MainMenuState);             break;          case sf::Key::F5:             level->reload();             calculateZoom();             break;          case sf::Key::Left:          case sf::Key::Right:          case sf::Key::Up:          case sf::Key::Down:             level->move((Level::Direction)(event->Key.Code));             break;          default:             break;       }    } }

Ovde vidimo kako obrada ulaznih poruka ne predstavlja nikakvu teškoću. Pritiskom na escape se vraćamo na glavni meni, F5 nam ponovo učitava nivo, a strelice zovu pomoćni objekat level i prepuštaju mu da odradi sve što treba da bi se igrač pomerio.


Datoteka playstate.cppvoid PlayState::update(float elapsedTime) {    if (level->isFinished())    {       if (!level->loadNext())          game->changeState(Game::MainMenuState);       else          calculateZoom();    }    else    {    } }

Pre svakog crtanja, proveravamo da li pomoćna klasa kaže da je nivo završen. U slučaju da jeste, učitavamo sledeći ako ga ima ili se vraćamo na glavni meni.

Za kraj smo ostavili funkciju za crtanje:


Datoteka playstate.cppvoid PlayState::render() {    Level::PlayField field;    game->getWindow()->Draw(*bgSprite);    game->getWindow()->Draw(*status);    game->getWindow()->SetView(*view);    for (int x = 0; x <level>width; ++x)       for (int y = 0; y <level>height; ++y)       {          field = level->getField(x, y);          if (field != Level::Empty)          {             sprite->SetImage(*playField[field]);             sprite->SetPosition(64 * x, 64 * y);             game->getWindow()->Draw(*sprite);          }       }    sprite->SetImage(*crate);    for (std::vector<sf>::iterator it = level->crates.begin(); it != level->crates.end(); ++it)    {       sprite->SetPosition(64 * (*it).x, 64 * (*it).y);       game->getWindow()->Draw(*sprite);    }    sprite->SetImage(*player);    int offset;    switch (level->lastDir)    {       case Level::Left:          offset = 1;          break;       case Level::Up:          offset = 2;          break;       case Level::Right:          offset = 3;          break;       default:          offset = 0;          break;    }    sprite->SetSubRect(sf::IntRect(64 * offset, 0, 64 * (offset + 1), 64));    sprite->SetPosition(64 * level->player.x, 64 * level->player.y);    game->getWindow()->Draw(*sprite);    sprite->SetSubRect(sf::IntRect(0, 0, 64, 64));    game->getWindow()->SetView(game->getWindow()->GetDefaultView()); }

Na početku iscrtavamo pozadinsku sliku, a odmah za njom i tekst na kojem piše da F5 resetuje nivo.

Nakon toga, crtamo redom polje po polje mape. Zahvaljujući funkciji calculateZoom ovde ne moramo da se brinemo o veličini polja i da li će stati na ekran. Svaki put crtamo sliku u normalnoj veličini i prepuštamo SFML-u da se brine o skaliranju.

Preko polja, crtamo kutije i igrača. Obratite pažnju da za igrača uzimamo deo slike koji je okrenut u pravu stranu da bi igra izgledala lepše. U ovom trenutku je crtanju kraj i na ekranu imamo nacrtanu celu mapu i sve objekte na njoj.



[7] Završavanje i testiranje igre


Do sad smo pisali klase koje rade sve od učitavanja, preko igranja do gašenja igre. Nedostaje nam još samo main funkcija koja će sve to da pokrene:


Datoteka main.cpp#include "game.h" #include "mainmenustate.h" #include "playstate.h" int main() {    Game game;    game.addState(Game::MainMenuState, new MainMenuState());    game.addState(Game::PlayState, new PlayState());    return game.run(Game::MainMenuState); }

To je, dragi moji, to. Cela igra je napisana. Ostaje vam još samo da je iskompajlirate i pokrenete Smile

Za one koji žele malo vežbe, slika igrača ima još par dodatnih frejmova uz pomoć kojih možete napraviti da igrač korača kad se kreće. Moraćete da popravite funkcije update i render u klasi PlayState.

Preuzimanje ::Kompletan kod i resurse možete preuzeti ovde: [url=https://www.mycity.rs/must-login.png

Za one koji su samo došli da bace pogled, evo i par sličica:









SFML ::Kompletnu dokumentaciju za SFML možete pogledati na zvaničnom sajtu: link.

Dopuna: 15 Jan 2013 15:21

Prošlo je već neko vreme i nakupilo se dosta pregleda... da li je nekom uspelo da iskompajlira igru i da je pokrene? Smile



Registruj se da bi učestvovao u diskusiji. Registrovanim korisnicima se NE prikazuju reklame unutar poruka.
offline
  • Programer
  • Pridružio: 23 Maj 2012
  • Poruke: 4575

Sad sam baš bacio pogleda na članak i našao jedan deo koji mi nije jasan. Je li u pitanju neka greška ili...? Konkretno mislim na ovo:

Citat:#ifndef GAME_H_
#define GAME_H_


Čemu služi #ifndef?



offline
  • Srđan Tot
  • Am I evil? I am man, yes I am.
  • Pridružio: 12 Jul 2005
  • Poruke: 2483
  • Gde živiš: Ljubljana

To je prilično standardno za C/C++ header datoteke. Kada u kodu upišeš #include "nesto.h", to znači da se na tom mestu ubaci kod iz datoteke nesto.h. Ako neku header datoteku koristiš na više mesta, doći će do toga da će njen kod biti više puta ubačen u glavnu glavnu datoteku što će u većini slučajeva dovesti do greške u kompajliranju.

Kada ceo kod u header datoteci okružiš #ifndef oznaka #define oznaka i #endif, kod upisan u header datoteci će biti samo jednom kompajliran iako je više puta dodan. Prilikom parsanja koda će kompajlier prvi put videti da oznaka nije definisana, zatim će je desifnisati i iskompajlirati kod. Prilikom nailaska na drugu kopiju header datoteke će oznaka već biti definisana i komjaler će preskočiti ceo kod do #endif.

Kao što već rekoh... to je jedna od najosnovnijih stvari koje je potrebno znati za programiranje C/C++ programa.

offline
  • C# and PHP Developer
  • Pridružio: 16 Feb 2011
  • Poruke: 1630
  • Gde živiš: Pancevo

Srki mogao bi kada zavrsis ovo da napises neki tutorial za OpenGL i DirectX. Kad budes imao vremena i ako si voljan. Verujem da bi mnogima pomoglo a pogotovo meni!!!

offline
  • Srđan Tot
  • Am I evil? I am man, yes I am.
  • Pridružio: 12 Jul 2005
  • Poruke: 2483
  • Gde živiš: Ljubljana

Kad završim koje? Ovo što vidiš je gotovo... objašnjenje za pravljenje cele igre, a uputstvo pre toga za podešavanje IDE-a i instalaciju biblioteke Smile Ne znam da li se isplati da pišem drugo uputstvo, kad se niko ne trudi ni da pročita ova koja već postoje Very Happy

offline
  • C# and PHP Developer
  • Pridružio: 16 Feb 2011
  • Poruke: 1630
  • Gde živiš: Pancevo

Ima dosta ljudi koji nisu registrovani i neucestvuju u diskusijama vec samo citaju Wink Kako god zelis dao sam predlog!

offline
  • bocke  Male
  • Moderator foruma
  • Glavni moderator Linux foruma
  • Veliki Pingvin
  • Guru
  • Pridružio: 16 Dec 2005
  • Poruke: 12486
  • Gde živiš: Južni pol

@ELITE

Linije koje počinju sa tarabom su direktive preprocesora. Kao što je napisao Srki iznad, kod se prvo "provlači" kroz preprocesor koji zatim ove "zatarabljene" direktive prevodi u oblik s kojim kompajler može da radi. Tek nakon toga se taj transformisani izvorni kod kompajlira.

#ifdef će se izvršiti samo ako je određena preprocesorska konstanta definisana. #ifndef je suprotan slučaj.

Recimo u praksi bi mogao da naiđeš na nešto poput ovoga (C):
#ifdef __STDC__ /* ako je u pitanju ANSI C kompajler */ #include <stdlib.h> #include <string.h> #else /* u suprotnom je u pitanju k&r kompajler */ extern char *malloc(); extern char *strcpy(); #endif

offline
  • Programer
  • Pridružio: 23 Maj 2012
  • Poruke: 4575

Srki mi je to malo bolje pojasnio Very Happy Ali hvala na objašnjenju , sad mi je jasnije.

offline
  • Pridružio: 10 Dec 2014
  • Poruke: 29

Imam ovakav problem:
Unable to start program 'D:\VisualC++\igra\Debug\igra.exe'.
The system canot find the file specified.

online
  • Milan
  • Pridružio: 17 Dec 2007
  • Poruke: 14809
  • Gde živiš: Niš

Da li ste na toj putanju nalazi exe fajl koji se pominje? Probaj sa restartom Visual Studia (ili Visual C++-a, nisam siguran šta koristiš). Ako neće ni tako, isključi VS, proveri u Task Manageru da li je igra.exe aktivna i isključi taj proces, pa odi na zadatu putanju, obriši .exe, a zatim pokreni VS, kompajliraj aplikaciju i trebalo bi da radi bez problema.

Ko je trenutno na forumu
 

Ukupno su 1334 korisnika na forumu :: 37 registrovanih, 11 sakrivenih i 1286 gosta   ::   [ Administrator ] [ Supermoderator ] [ Moderator ] :: Detaljnije

Najviše korisnika na forumu ikad bilo je 3466 - dana 01 Jun 2021 17:07

Korisnici koji su trenutno na forumu:
Korisnici trenutno na forumu: ajo baba, amaterSRB, Andrija357, Asparagus, bladesu, BORUTUS, debeli, Dimitrise93, Dorcolac, DPera, draganl, dushan, Georgius, hyla, jackreacher011011, Karla, kihot, kovinacc, kuntalo, Leonov, Lieutenant, mgolub, Mi lao shu, MikeHammer, milenko crazy north, milos.cbr, MilosKop, nemkea71, nextyamb, procesor, royst33, srbijaiznadsvega, Stanlio, Trpe Grozni, vasa.93, vathra, W123