Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Notatki programisty: deska kreślarska jako podstawa projektowania, czyli piszemy edytor mapy siatkowej

W poprzednich wpisach omówiłem pokrótce zasadę działania rdzenia, gier platformowych 2D, wykorzystującego zaawansowaną fizykę. Jak mogliśmy zauważyć w poprzednich przykładach, proces tworzenia obiektów był dość "kodochłonny". W niniejszym tekście zaprezentuje pierwszy krok, który w przyszłości zapewni nam dużo łatwiejsze projektowanie poziomów naszego dema.

Grunt to model

Zacznijmy od konceptu, mianowicie nasza mapa siatkowa będzie prostą dynamiczną tablicą dwuwymiarową zasymulowaną w kodzie wektorami. Każdy element naszej tablicy będzie przechowywać obiekt struktury w którym zapiszemy identyfikator obiektu – coś na kształt imienia, będzie służyć do identyfikacji czy pod tą komórką tablicy znajduje się obiekt ściany czy gracza, wysokość, szerokość i położenie – dane te przydarzą się nam w przypadku tworzenia bardziej „spersonalizowanego” świata Box2D. Dzięki temu zabiegowi wszelkie informację na temat wyglądu i położenia ciała wyrzucimy z kodu, do pliku mapy. Całość o wiele lepiej zobrazuje nam deklaracja klasy.

struct GameMap { public: struct Item { std::string ID; int width; int height; int x; int y; void log(); }; const int width; const int height; std::vector<std::vector<Item>> array; GameMap( const int widthValue, const int heightValue, bool ini = true); void log(); private: void initArray(); }; GameMap::GameMap( const int widthValue, const int heightValue, bool init) : width(widthValue), height(heightValue) { if(init){ this->initArray(); } } void GameMap::initArray() { for(int i = 0; i < height; ++i) { std::vector<Item> newVec; for(int j = 0; j < width; ++j) { Item newItem; newItem.ID = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty); newItem.x = (EditorGlobals::MESH_ITEM_SIZE); newItem.y = (EditorGlobals::MESH_ITEM_SIZE); newItem.x = (j*(EditorGlobals::MESH_ITEM_SIZE/2) * 2); newItem.y = (i*(EditorGlobals::MESH_ITEM_SIZE/2) * 2); newItem.height = 0; newItem.width = 0; newVec.push_back(newItem); } array.push_back(newVec); } } void GameMap::log() { std::cout << "Width: " << width << " Height: " << height << "\n"; for(std::vector<GameMap::Item>& vec : array) { for(GameMap::Item& item : vec){ item.log(); } std::cout << "\n"; } } void GameMap::Item::log() { std::cout << " ID: " << ID << "\n"; std::cout << " x: " << x << "\n"; std::cout << " y: " << y << "\n"; }

Słowem wyjaśnienia dlaczego zmienna init posiada parametr domyślny true i po co ona tutaj jest. Mianowicie, po tym jak zadeklarujemy rozmiar (wysokość, szerokość) naszej mapy będziemy musieli utworzyć stan początkowy mapy aby na starcie zawierała „obojętne” obiekty oraz za-alokować dla naszego wektora odpowiednią ilość pamięci. Wyjątkiem od tej reguły jest sytuacja w której będziemy ładować mapę z pliku ale o tym później.

Jak być może pamiętamy z poprzednich wpisów, w przykładach posługiwaliśmy się prostym systemem identyfikacji typów ciał, posiadaliśmy specjalną klasę do przechowywania informacji o tym jakiego typu jest ciało silnika Box2D. W naszym nowym edytorze wykorzystamy podobny mechanizm do nadawania identyfikatora obiektom mapy. Oto jak będzie wyglądać klasa rejestru.

struct GameObjRegister { public: enum Index { Empty, Player, MapFlor, MapWall, Enemy }; GameObjRegister(); static GameObjRegister* get(); std::map<Index, std::string> list; private: static std::shared_ptr<GameObjRegister> ptr; }; std::shared_ptr<GameObjRegister> GameObjRegister::ptr = nullptr; GameObjRegister::GameObjRegister() { list[Index::Empty] = std::string("EMPTY"); list[Index::Player] = std::string("HERO"); list[Index::Enemy] = std::string("STR_ENEMY"); list[Index::MapFlor] = std::string("MAP_FLOR"); list[Index::MapWall] = std::string("MAP_WALL"); } GameObjRegister* GameObjRegister::get() { if(ptr == nullptr){ ptr = std::make_shared<GameObjRegister>(GameObjRegister()); } return ptr.get(); }

W przyszłości będziemy chcieli zapisać nasza mapę z pamięci do pliku, stąd stwierdziłem że przejrzyściej będzie operować na słowach niż bezpośrednio na enumie który mógłby być nieopatrznie w przyszłości zedytowany. Dodatkowo ktoś mógłby zapytać, dlaczego by nie zintegrować używanego systemu identyfikacji z „rdzenia” z edytorem, odpowiedź jest prosta: dla zachowania modułowej budowy naszego projektu.

Namaluj mój świat na żółto i na niebiesko

W tym miejscu mamy gotowy model naszej mapy, czas przystąpić do napisania kodu który będzie odpowiedzialny za rysowanie naszej mapy. Otóż, jednym z założeń jest to że siatka mapy będzie składać się z kwadratów o wymiarach 40x40 pikseli. Dodatkowo, roboczo pozycja naszych obiektów będzie odpowiadała prawemu górnemu rogowi kwadratowi siatki. Oczywiście w przyszłości można zmienić koncept na bardziej precyzyjny: mniejsze komórki i pozycja ustalana na podstawie środka czworokąta aczkolwiek ten temat zostawiam w gestii bardziej dociekliwych i ambitnych czytelników. ;) Definicja klasy naszego renderu wygląda następująco:

class RenderNT { public: RenderNT(sf::RenderWindow* window); void drawMesh(); void render(GameMap& map); void refresh(const sf::View& camera); private: struct RenderSurface { sf::Vector2f HeightY; sf::Vector2f WidthX; void update( sf::Vector2f viewCenterPosition, sf::Vector2u windowSize); }; RenderSurface m_renderSurface; struct RenderEngine { struct InfoForEngine { GameMap* map; sf::RenderWindow* window; RenderSurface renderSurface; }; struct Base { virtual ~Base() {} void virtual render(InfoForEngine& data) = 0; }; struct Graphics : public Base { Graphics(); ~Graphics(); void render(InfoForEngine &data); }; }; RenderEngine::Base* m_renderEngine; sf::Vector2i m_meshSize; sf::RenderWindow* m_window; };

Jak widzimy, klasa rysująca używa dwóch tajemniczych klas RenderSurface i RenderEngine. Zaczynając ich omówienie od tej pierwszej: wyobraźmy sobie że nasza mapa ma wymiar 100x100 aczkolwiek w oknie aplikacji są widoczne jedynie komórki z indeksami 20-50 w poziomie i 40-90 w pionie (trochę dziwna proporcja wiem) i teraz pojawia się problem, czy jest sens marnować moc obliczeniową na rysowanie obiektów które znajdują się poza naszym wzrokiem? Otóż odpowiedź brzmi: nie. Klasa RenderSurface przechowuje informację na temat indeksów tablicy które powinny być rysowane w oknie, dzięki temu nasza aplikacja przy mapie o wymiarach np. 512x512 nie będzie pokazem slajdów. Wartości indeksów obliczamy na podstawie obecnego położenia kamery i wielkości okna aplikacji, szczegóły w implementacji. Druga z tajemniczych klas to RenderEngine, otóż rozpatrzmy sytuację w której to chcemy zmienić tryb renderowania naszej mapy z surowych kształtów na teksturowane. W takim wypadku warto zaopatrzyć się w możliwość łatwego przełączania między stylami renderowania. Właśnie do tego będzie nam potrzebna klasa silnika renderującego. Teraz kolejno, implementacja RenderNT:

RenderNT::RenderNT( sf::RenderWindow* window) : m_window(window), m_renderEngine(new RenderEngine::Graphics()) {} void RenderNT::refresh(const sf::View& camera) { m_renderSurface.update( camera.getCenter(), m_window->getSize()); } void RenderNT::drawMesh() { int width = m_meshSize.x; int height = m_meshSize.y; const int pion = height * EditorGlobals::MESH_ITEM_SIZE; const int poziom = width * EditorGlobals::MESH_ITEM_SIZE; /* Linie pionowe */ for (int i = 0; i < width + 1; i++) { sf::Vertex vLine[] = { sf::Vertex(sf::Vector2f( (float)(i * EditorGlobals::MESH_ITEM_SIZE), 0.f)), sf::Vertex(sf::Vector2f( (float)(i * EditorGlobals::MESH_ITEM_SIZE), (float)pion)) }; m_window->draw(vLine, 2, sf::Lines); } /* Linie poziome */ for (int i = 0; i < height + 1; i++) { sf::Vertex vLine[] = { sf::Vertex(sf::Vector2f( 0.f, (float)(i * EditorGlobals::MESH_ITEM_SIZE))), sf::Vertex(sf::Vector2f( (float)poziom, (float)(i * EditorGlobals::MESH_ITEM_SIZE))) }; m_window->draw(vLine, 2, sf::Lines); } } void RenderNT::render(GameMap& map) { m_meshSize.x = map.width; m_meshSize.y = map.height; RenderEngine::InfoForEngine info; info.map = &map; info.window = m_window; info.renderSurface = m_renderSurface; m_renderEngine->render(info); } // RenderSurface: /* Funkcja okresla ktore indeksy beda rysowane. */ void RenderNT::RenderSurface::update( sf::Vector2f viewCenterPosition, sf::Vector2u windowSize) { float haldWindowWidth = windowSize.x / 2; float halfWindowHeight = windowSize.y / 2; this->HeightY = sf::Vector2f( viewCenterPosition.y - halfWindowHeight, viewCenterPosition.y + halfWindowHeight); this->WidthX = sf::Vector2f( viewCenterPosition.x - haldWindowWidth, viewCenterPosition.x + haldWindowWidth); WidthX.x = WidthX.x / EditorGlobals::MESH_ITEM_SIZE; WidthX.y = WidthX.y / EditorGlobals::MESH_ITEM_SIZE; HeightY.x = HeightY.x / EditorGlobals::MESH_ITEM_SIZE; HeightY.y = HeightY.y / EditorGlobals::MESH_ITEM_SIZE; } // RenderSurface: RenderNT::RenderEngine::Graphics::Graphics() {} RenderNT::RenderEngine::Graphics::~Graphics() {} void RenderNT::RenderEngine::Graphics::render(InfoForEngine &data) { /* Okreslenie jaki jest przedzial renderowanych * obiektow na podstawie danych RenderSurface, * w przyszlosci mozna wyrzucic te operacje do * nowej metody. */ int startI = 0; int finishI = data.map->height; int startJ = 0; int finishJ = data.map->width; bool widthCondition = ( data.renderSurface.WidthX.x >= 0 && data.renderSurface.WidthX.y <= data.map->width); bool heightCondition = ( data.renderSurface.HeightY.x >= 0 && data.renderSurface.HeightY.y <= data.map->height); if(widthCondition && heightCondition) { startI = data.renderSurface.HeightY.x; finishI = data.renderSurface.HeightY.y; startJ = data.renderSurface.WidthX.x; finishJ = data.renderSurface.WidthX.y; } for(int i = startI; i < finishI; ++i) { for(int j = startJ; j < finishJ; ++j) { /* Pobieranie informacji z komorki * tablicy i tworzenie surowych obiektow * renderujacych. */ GameMap::Item* item = &data.map->array [ i ] [ j ] ; sf::Vector2f position(item->x, item->y); if (item->ID == GameObjRegister::get()->list.at( GameObjRegister::Index::MapFlor)) { sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Red); data.window->draw(shape); }; if (item->ID == GameObjRegister::get()->list.at( GameObjRegister::Index::MapWall)) { sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Yellow); data.window->draw(shape); }; } } }

Zaczarowany ołówek

Słowem krótkiego podsumowania: na chwile obecną mamy model naszej mapy dwuwymiarowej oraz prostą klasę do jej rysowania w oknie Box2D. Lecz jak nazwa wskazuje, edytor służy do edycji więc przyszedł czas na opracowanie mechanizmu edycji komórek naszej tabeli. Jak wspomniałem w jednym ze wcześniejszych akapitów, do identyfikacji obiektów siatki będziemy używać prostej klasy rejestru i na chwilę obecną tylko tyle nas interesuje. Zmiana pola ID w strukturze Item. Jednak aby wykonywać tą operację potrzebujemy mechanizmu który odczyta nam pozycję myszy w oknie aplikacji i określi indeks komórki na którą kliknęliśmy. Co więcej musimy wziąć pod uwagę przesuniecie kamery w chwili obliczania odpowiedniego indeksu jak i wprowadzić jakiekolwiek zabezpieczenie przed wyjściem poza mapę. Jako że kod wytłumaczy dużo więcej niż sam suchy tekst, przejdźmy do deklaracji i implementacji klasy odpowiedzialnej za cały wyżej wymieniony mechanizm:

class MouseManager { public: struct Info { int x; int y; bool isOk; }; MouseManager(sf::RenderWindow* window); Info getIndex(GameMap* map, sf::View* view); private: sf::RenderWindow* m_window; int m_startPositionX; int m_startPositionY; }; MouseManager::MouseManager(sf::RenderWindow* window) : m_window(window) { m_startPositionX = m_window->getDefaultView().getCenter().x; m_startPositionY = m_window->getDefaultView().getCenter().y; } MouseManager::Info MouseManager::getIndex(GameMap* map, sf::View* view) { int iMousePositionX = (int)sf::Mouse::getPosition(*m_window).x + (view->getCenter().x - m_startPositionX); int iMousePositionY = (int)sf::Mouse::getPosition(*m_window).y + (view->getCenter().y - m_startPositionY); int iSecondParam = (int)iMousePositionX / EditorGlobals::MESH_ITEM_SIZE; int iFirstParam = (int)iMousePositionY / EditorGlobals::MESH_ITEM_SIZE; bool heightCondition = iFirstParam >= 0 && iFirstParam < map->height; bool widthCondition = iSecondParam >= 0 && iSecondParam < map->width; bool mainChangeCondition = heightCondition && widthCondition; Info toReturn; toReturn.x = iFirstParam; toReturn.y = iSecondParam; toReturn.isOk = mainChangeCondition; return toReturn; }

Jak możemy zauważyć wbrew pozorom nie jest to nic specjalnie trudnego. Ot klasa na podstawie prostych obliczeń łopatologicznych wylicza indeks biorą pod uwagę punkt odniesienia (punkt startowy kamery), wielkość okna, pozycje myszy oraz wielkość siatki.

Przez cały kod przewijały się tajemnicze odniesienia do klasy EditorGlobals, oto i ona:

struct EditorGlobals { static const int MESH_ITEM_SIZE = 40; };

Więcej zależności czyli Qt w akcji

Wszystkie prezentowane przykłady z serii, opracowywałem w środowisku QtCreatora przez co wprowadzenie dla mnie dodatkowej zależności w postaci biblioteki z frameworka Qt nie stanowi większego problemu. Oczywiście dla kogoś kto używał Visual Studio, sprawa delikatnie się komplikuje. Dlaczego o tym wspominam? Otóż model naszej mapy będziemy zapisywać do pliku w formacie json a do tego celu wykorzystamy biblioteki Qt. Sama idea i koncept dekoratora który posłuży nam do zapisu i odczytu jest dość uniwersalny stąd jeżeli drogi czytelniku czujesz się na siłach i chcesz użyć innej biblioteki/formatu – wszystko w twoich rękach. ;) Ze względu na lekkość i licencję ten projekt wydaje się atrakcyjniejszy, aczkolwiek na razie rozważania te odłóżmy na bok. Jak będzie wyglądać nasz moduł odpowiedzialny za zapis do pliku, zbrodniczo prosto:

namespace Json { class Fields { public: const QString ID; const QString WIDTH; const QString HEIGHT; const QString CONTENT; const QString ROW; const QString X; const QString Y; Fields(); static Fields* get(); private: ~Fields(); static Fields* m_ptr; }; class Generator { public: Generator(GameMap* base = nullptr); QJsonDocument getJsonDoc(); GameMap* getGameMapFromJson(QJsonDocument& source); private: GameMap* m_ptr; }; struct File { public: File(); bool saveJsonMap( QJsonDocument& document, QString filePath) const; QJsonDocument readJsonMap(QString filePath) const; }; }

Ot mamy klasę do przechowywania łańcuchów znaków których używamy wewnątrz pliku, dodatkowo klasa która nam generuje na podstawie modelu obiekt dokumentu jsona i w drugą stronę oraz klasę która ten dokument zapisuje/odczytuje z pliku. Implementacja raczej też straszna nie jest.

//Json::Fields Json::Fields* Json::Fields::m_ptr = nullptr; Json::Fields::Fields() : ID("ID"), WIDTH("width"), HEIGHT("height"), CONTENT("content"), ROW("row"), X("x"), Y("y") { } Json::Fields* Json::Fields::get() { if(m_ptr == nullptr){ m_ptr = new Fields(); } return m_ptr; } Json::Fields::~Fields(){ if(m_ptr != nullptr){ delete m_ptr; } } // Json::Generator Json::Generator::Generator(GameMap* base) : m_ptr(base) {} QJsonDocument Json::Generator::getJsonDoc() { QJsonObject main; main[Fields::get()->WIDTH] = m_ptr->width; main[Fields::get()->HEIGHT] = m_ptr->height; int line = 0; QJsonArray array; for(std::vector<GameMap::Item>& vec : m_ptr->array) { QJsonArray lineArray; for(GameMap::Item& item : vec) { QJsonObject newJson; newJson[Fields::get()->ID] = QString::fromStdString(item.ID); newJson[Fields::get()->X] = QString::number(item.x); newJson[Fields::get()->Y] = QString::number(item.y); newJson[Fields::get()->WIDTH] = QString::number(item.width); newJson[Fields::get()->HEIGHT] = QString::number(item.height); lineArray.append(newJson); } QJsonObject additionalInfo; additionalInfo[Fields::get()->ROW] = line; lineArray.append(additionalInfo); ++line; array.append(lineArray); } main[Fields::get()->CONTENT] = array; return QJsonDocument(main); } GameMap* Json::Generator::getGameMapFromJson(QJsonDocument &source) { QJsonObject jsonObj = source.object(); const int height = jsonObj[Json::Fields::get()->HEIGHT].toInt(); const int width = jsonObj[Json::Fields::get()->WIDTH].toInt(); GameMap* newMap = new GameMap(width, height, false); QJsonArray content = jsonObj[Json::Fields::get()->CONTENT].toArray(); for(int i = 0; i < content.size(); ++i) { std::vector<GameMap::Item> newLine; QJsonArray singleRow = content.at(i).toArray(); for(int j = 0; j < singleRow.size()-1; ++j) { QJsonObject item = singleRow.at(j).toObject(); GameMap::Item newItem; newItem.ID = item[Json::Fields::get()->ID].toString().toStdString(); newItem.x = item[Json::Fields::get()->X].toString().toInt(); newItem.y = item[Json::Fields::get()->Y].toString().toInt(); newItem.height = item[Json::Fields::get()->HEIGHT].toString().toInt(); newItem.width = item[Json::Fields::get()->WIDTH].toString().toInt(); newLine.push_back(newItem); } newMap->array.push_back(newLine); } return newMap; } // Json::File Json::File::File() {} bool Json::File::saveJsonMap(QJsonDocument& document, QString filePath) const { bool toReturn = false; QString filePathToSave = filePath; if(!filePathToSave.isEmpty()){ QFile outputFile(filePathToSave); outputFile.open(QIODevice::WriteOnly); outputFile.write(document.toJson()); outputFile.close(); toReturn = true; } return toReturn; } QJsonDocument Json::File::readJsonMap(QString filePath) const { QJsonDocument jsonDocReaded; if(QFile::exists(filePath) && !filePath.isEmpty()) { QString fileContentString; QFile jsonFile(filePath); if(jsonFile.open(QIODevice::ReadOnly)){ fileContentString = jsonFile.readAll(); jsonDocReaded = QJsonDocument::fromJson(fileContentString.toUtf8()); } jsonFile.close(); } return jsonDocReaded; }

Tym razem było dość sporo zagadnień do omówienia, model, render mapy, obsługa edytującego kursora i moduł obsługi zapisu/odczytu mapy z pliku. Jeżeli dobrnąłeś do tego momentu czas na satysfakcjonujący koniec, czyli funkcja startowa aplikacji. U siebie w kodzie trzymam ją w metodzie statycznej w klasie Editor więc starczy tej magii na dzisiaj.

void Editor::run() { int width = 0; std::cout << "Get width (X): "; std::cin >> width; int height = 0; std::cout << "Get height(Y): "; std::cin >> height; std::shared_ptr<GameMap> map(new GameMap(width,height)); sf::RenderWindow window( sf::VideoMode(800,600, 16), std::string("hello"), sf::Style::Close); sf::View camera; camera = window.getDefaultView(); sf::Vector2f startCameraPosition = camera.getCenter(); const int viewMoveValue = 5; RenderNT render(&window); render.refresh(camera); MouseManager mouseIndex(&window); std::string selectedString = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty); while(window.isOpen()) { sf::Event myEvent; while(window.pollEvent(myEvent)) { if(myEvent.type == sf::Event::Closed){ window.close(); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Q)) { Json::Generator dec(map.get()); QJsonDocument jsonDoc = dec.getJsonDoc(); const std::string fileName = std::string("hello.json"); Json::File fileDec; fileDec.saveJsonMap(jsonDoc, QString::fromStdString(fileName)); std::cout << std::string("Saved as: ") + fileName; } if(sf::Keyboard::isKeyPressed(sf::Keyboard::W)) { const std::string fileName = std::string("hello.json"); Json::File fileDec; QJsonDocument jsonDoc = fileDec.readJsonMap(QString::fromStdString(fileName)); Json::Generator dec; map.reset(dec.getGameMapFromJson(jsonDoc)); std::cout << std::string("Readed from: ") + fileName; } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Tab)) { window.setActive(false); int index = 0; for(auto& pair :GameObjRegister::get()->list ) { std::cout << "No. " << pair.first << " Content: " << pair.second << "\n"; } std::cin >> index; selectedString = GameObjRegister::get()->list.at( (GameObjRegister::Index)index); window.setActive(true); } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { if(camera.getCenter().x >= startCameraPosition.x+10) { render.refresh(camera); camera.move(-viewMoveValue, 0); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { if(camera.getCenter().x + (window.getSize().x/2) <= (map->width*40)-10) { render.refresh(camera); camera.move(viewMoveValue, 0); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { if(camera.getCenter().y >= startCameraPosition.y+10) { render.refresh(camera); camera.move(0, -viewMoveValue); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { if(camera.getCenter().y + (window.getSize().y/2) <= (map->height*40)-10) { render.refresh(camera); camera.move(0, viewMoveValue); window.setView(camera); } } if (sf::Mouse::isButtonPressed(sf::Mouse::Left)) { MouseManager::Info infoMouse = mouseIndex.getIndex(map.get(), &camera); if (infoMouse.isOk) { GameMap::Item* itemPtr = &map->array[infoMouse.x][infoMouse.y]; itemPtr->ID = selectedString; itemPtr->height = 40; itemPtr->width = 40; } } if (sf::Mouse::isButtonPressed(sf::Mouse::Right)) { MouseManager::Info infoMouse = mouseIndex.getIndex(map.get(), &camera); if (infoMouse.isOk) { GameMap::Item* itemPtr = &map->array[infoMouse.x][infoMouse.y]; itemPtr->ID = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty); } } } window.clear(sf::Color::Black); render.drawMesh(); render.render(*map); window.display(); } }

Jak widzimy wszystkie omówione elementy zostały wykorzystane w przykładzie. Dodatkowo w przypadku poruszania się wprowadzone zostały warunki aby nie było możliwości wyjechania poza siatkę. Kosmetyka ale przydatna. ;) Pod klawiszem Tab mamy do dyspozycji proste konsolowe menu do wybierania identyfikatorów obiektów z rejestru. Całość prezentuje się mało widowiskowo aczkolwiek od teraz wiemy skąd memy w stylu: co widzi klient, co widzi programista.

Jak zawsze, dzięki za uwagę!

 

programowanie gry hobby

Komentarze

0 nowych
SpaceM7c5   6 #1 07.02.2017 18:14

Trochę się musiałeś natrudzić nad napisaniem tego.
Dzięki że zechciałeś się podzielić.