Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Własny język skryptowy, część 1

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest Złośliwiec. Jeżeli został importowany poprawnie, usuń ten szablon!

Wstęp

Przy tworzeniu bardziej rozbudowanych gier, bez względu na ich gatunek, dochodzi się zawsze do momentu, że pojawia się potrzeba programowania jakiejś części gry w języku skryptowym. Częścią tą może być sztuczna inteligencja, interakcja różnych obiektów z graczem, definicje tych obiektów, dialogi z postaciami, a nawet cały tak zwany gameplay.

Wspomniana potrzeba wynika z różnych faktów. Po pierwsze, język interpretowany (a w takim z reguły tworzone są skrypty[1]) ma tę przewagę nad kompilowanym, że nie wymaga obecności środowiska programistycznego, aby można było dokonać zmian w kodzie. Wystarczy zwykle edytować plik skryptu w dowolnym edytorze tekstowym, a następnie ponownie uruchomić grę i już dokonane zmiany stają się widoczne dla użytkownika. Po drugie, język skryptowy jest znacznie prostszy (i "bardziej wysokopoziomowy") od tego, w którym napisana jest sama gra. Dzięki temu podobne efekty osiąga się w nim dużo szybciej, jest też mniej możliwości zrobienia różnego rodzaju błędów, które mogą zastopować całą produkcję. Po trzecie, język skryptowy jest zwykle językiem wyspecjalizowanym - stworzony został wyłącznie na potrzeby danej gry i do wykonywania specyficznych dla niej zadań. Tak więc w przeciwieństwie do języków uniwersalnych w rodzaju C++, zaimplementowanie czegokolwiek przy pomocy języka skryptowego wymaga zwykle jednej lub kilku instrukcji w jednym miejscu jednego pliku źródłowego (żeby zaimplementować w grze coś nowego przy użyciu C++, prawdopodobnie musielibyśmy dokonać wielu zmian w różnych miejscach kodu)[2].

Wykorzystywanie języków skryptowych posiada zapewne jeszcze wiele innych zalet. Jednak wymienianie ich wszystkich nie jest celem tego artykułu. Ważne jest uświadomienie sobie faktu, że skrypty nie są bynajmniej sztuką dla sztuki i mogą proces tworzenia gry bardzo ułatwić.

Warstwowy model języka

A więc skrypty są dobre. Jak więc zabrać się za ich zaimplementowanie w naszej grze? Aby łatwiej było zrozumieć ideę języka skryptowego (i w ogóle języka programowania), wymyśliłem sobie taki oto warstwowy model działania skryptu:

  • warstwa wejścia
  • warstwa parsingu
  • warstwa interpretacji
  • warstwa wykonania

Omówimy sobie teraz po kolei te wszystkie warstwy, a następnie w takiej samej kolejności spróbujemy zbudować jakiś prosty język skryptowy.

Warstwa wejścia

Na warstwę wejścia składa się wszystko to, co związane jest z wprowadzaniem przez użytkownika tekstu skryptu. Gdybyśmy tworzyli specjalny edytor skryptów jako oddzielną aplikację, wówczas aplikacja ta w całości kwalifikowałaby się do tej warstwy. Gdybyśmy z kolei zajmowali się tworzeniem w grze konsoli (okienko, wywoływane zwykle tyldą i służące do wprowadzania różnych poleceń w czasie gry[3]), to duża część kodu obsługującego ową konsolę weszłaby w skład warstwy wejścia. Niniejszy artykuł bazuje jednak przede wszystkim na przykładzie, w którym skrypty wczytywane są z plików. Wówczas warstwa wejściowa obejmuje właściwie tylko proces wczytania pliku do bufora i nic więcej - nie ma zatem potrzeby rozpisywać się o niej zbytnio.

Warstwa parsingu

Przy tej warstwie zatrzymamy się nieco dłużej. Parsing to wstępna analiza i przetworzenie tekstu skryptu w taki sposób, aby z postaci czytelnej dla człowieka stał się on przystępny dla komputera. Można to porównać do trawienia pokarmu - powstaje taka papka, którą można poddać dalszej "obróbce" :-). Zazwyczaj parsing polega po prostu na podziale całego skryptu na mniejsze fragmenty (zwane czasem leksemami, tokenami lub noszące inne dziwne nazwy). Jeśli na przykład nasz skrypt ma być po prostu ciągiem poleceń o identycznej składni, umieszczonych każde w osobnym wierszu pliku, to parser będzie się zajmował wyszukiwaniem znaków nowego wiersza, decydowaniem, gdzie kończy się jedno polecenie, a gdzie zaczyna się następne, wyszukiwaniem ewentualnych odstępów między nazwą polecenia a jego parametrami itp. Wreszcie będzie przekazywał poszczególne "zdania" (polecenia wraz z parametrami) do interpretacji.

Warstwa interpretacji

Interpretacja jest procesem ściśle powiązanym z parsingiem. Najczęściej parsing i interpretacja zachodzą naprzemiennie - parser "wyciąga" ze skryptu kolejne polecenie i wywołuje interpreter, aby ten wykonał resztę roboty. Ta reszta to określenie, czym dokładnie jest przetwarzane polecenie. Jeśli na przykład parser pobrał ze skryptu ciąg znaków "stwórz_potwora 17", to zadaniem interpretera jest rozpoznanie tego ciągu jako znanego grze polecenia CMD_CREATE_MONSTER i przypisanie mu parametru liczbowego 17. Dane te (kod polecenia i parametry) mogą być następnie zapisane w odpowiedniej strukturze lub wykonane bezpośrednio. Często trudno jest odróżnić warstwę parsingu od warstwy interpretacji, ponieważ mogą się one znajdować w tym samym miejscu w kodzie gry, a nawet wykonywać za siebie nawzajem swoje zadania - wszystko zależy od specyfiki konkretnego języka skryptowego.

Warstwa wykonania

Gdy interpreter określi już, o jakim wewnętrznym poleceniu traktuje dany fragment skryptu, pozostaje tylko właściwe wykonanie polecenia. Nawiązując do przykładu z tworzeniem potwora, wykonaniem może być wywołanie funkcji CreateMonster z odpowiednim parametrem, która alokuje pamięć dla nowego potworka, zainicjalizuje go odpowiednimi danymi itd.:

CreateMonster(17);

Oczywiście treść funkcji CreateMonster nas w tym momencie nie interesuje - zajmujemy się samymi skryptami, od momentu ich wpisania przez użytkownika po moment wywołania funkcji w rodzaju naszego CreateMonster - nie dalej.

Przykład

Dość się już chyba nagadaliśmy, przydałoby się wreszcie zacząć działać. Na dobry początek stworzymy trywialny język skryptowy, umożliwiający wprowadzanie poleceń w rodzaju omawianego wyżej "stwórz_potwora". Polecenia, jak już powiedzieliśmy, będą wczytywane z pliku. Oto przykład takiego pliku:

// stworzenie orka
stwórz_potwora 17 wredny_ork 5 5
// stworzenie drugiego orka
stwórz_potwora 17 drugi_ork 6 6
// stworzenie kilku przedmiotów
stwórz_przedmiot 2 5 10
stwórz_przedmiot 2 7 9
stwórz_przedmiot 2 11 10
// wyposażenie orka w toporek
wyposaż_potwora wredny_ork 20

Na pierwszy rzut oka widać prostą strukturę naszego języka. Każde polecenie jest w osobnym wierszu, a składa się z nazwy polecenia (pierwszy wyraz w wierszu) oraz określonej liczby parametrów liczbowych lub tekstowych. Zarówno nazwa polecenia, jak i poszczególne parametry oddzielane są od siebie spacjami. Niektóre wiersze zawierają komentarze - będą one ignorowane podczas interpretacji.

Zacznijmy od wczytania pliku do bufora. Buforem tym będzie zmienna typu string, a do wczytywania wykorzystamy strumienie:

#include <cstdio> #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; string bufor; int main() { ifstream plik; plik.open("skrypt.txt"); char c; while(true) { c = plik.get(); if(plik.eof()) break; bufor += c; } plik.close(); }

Nie jest to może najwydajniejsza metoda wczytywania plików, ale też nie jest to tematem tego artykułu. Pora przejść do warstwy drugiej, czyli parsingu. Skonstruowanie jej nie jest proste, ale można je sobie znacznie ułatwić, tworząc najpierw kilka pomocniczych funkcji. Bufor z tekstem skryptu jest zwykłym stringiem, tak więc parsing będzie polegał na iterowaniu przez kolejne znaki tego stringa. Przyda nam się do tego zmienna, która będzie nam pokazywała aktualną pozycję w stringu. Nazwiemy tę zmienną pos. Przyglądając się przykładowemu skryptowi dochodzimy do wniosku, że pierwsza funkcja pomocnicza może po prostu pobierać pojedyczne słowa z bufora i oczywiście odpowiednio zwiększać "wskaźnik" pos.

Funkcję taką możemy oczywiście napisać "od zera", ale można prościej: mamy przecież STL. Klasa string zawiera mnóstwo przydatnych metod, a wśród nich takie, jak np. find_first_of, find_first_not_of, find_last_of... Wszystkie one pobierają jako argument zestawy znaków, co czyni je idealnymi murzynami do roboty, którą właśnie wykonujemy :-). Dzięki nim nie musimy się martwić: czy osoba pisząca nasz skrypt rozdziela poszczególne słowa spacjami, czy też woli tabulatory, a może od czasu do czasu zdarza jej się zostawić pusty wiersz między dwoma poleceniami? Możemy napisać parser tak, aby "łykał" wszystko - i powinniśmy tak pisać. Wystarczy stworzyć (najlepiej jako stałą) zestaw tzw. białych spacji (ang. whitespaces), czyli wszelkich znaków, które mogą posłużyć do rozdzielania wyrazów, a nie są widoczne w edytorze tekstu:

const char* WS_SET = " \t\r\n"; const char* BR_SET = "\r\n"; #define NOT_FOUND string::npos #define POS_END string::npos string bufor, cword, err_str; int pos = 0, param = 0;

Przy okazji stworzyliśmy jeszcze dwa przydatne synonimy, które zwiększą nieco czytelność naszego kodu, a także zadeklarowaliśmy kilka zmiennych globalnych obok istniejącej już wcześniej zmiennej bufor. Teraz nasz murzynek:

bool get_token() { int tmp_pos, tmp_pos2; tmp_pos = bufor.find_first_not_of(WS_SET, pos); if(tmp_pos == NOT_FOUND) { cword.clear(); pos = POS_END; return false; } tmp_pos2 = bufor.find_first_of(WS_SET, tmp_pos); if(tmp_pos2 == NOT_FOUND) { pos = bufor.length()-1; cword = bufor.substr(tmp_pos, bufor.length()-tmp_pos); } else { pos = tmp_pos2; cword = bufor.substr(tmp_pos, tmp_pos2-tmp_pos); } return true; }

Była to chyba najważniejsza funkcja całego naszego systemu skryptowego :-). Teraz druga, również dość istotna, ale już dużo mniej (przynajmniej w tej części artykułu) - będzie ona pobierała liczbowy parametr polecenia ze skryptu i zapisywała go w zmiennej param. Konstrukcja tej funkcji będzie podobna do get_token, ale będzie jeszcze dodatkowo wykonywała jedno zadanie - konwersję liczby zawartej w stringu do typu int:

bool get_param() { int tmp_pos, tmp_pos2; string s_number; tmp_pos = bufor.find_first_not_of(WS_SET, pos); if(tmp_pos == NOT_FOUND) { param = 0; pos = POS_END; return false; } tmp_pos2 = bufor.find_first_of(WS_SET, tmp_pos); cword.clear(); if(tmp_pos2 == NOT_FOUND) { tmp_pos2 = bufor.find_last_not_of(WS_SET, tmp_pos); if(tmp_pos2 == tmp_pos) { s_number = bufor[tmp_pos]; } pos = tmp_pos2; } if(s_number.empty()) { s_number = bufor.substr(tmp_pos, tmp_pos2-tmp_pos); pos = tmp_pos2; } param = itoa(s_number.c_str()); return true; }

Przydatna będzie też taka oto funkcyjka, która ma prostą rolę - przesuwa nasz "wskaźnik" pos do następnego wiersza (lub na koniec pliku, jeśli jesteśmy w ostatnim wierszu) :

void ignore_line() { int tmp_pos; tmp_pos = bufor.find_first_of(BR_SET, pos); if(tmp_pos == NOT_FOUND) { pos = POS_END; return; } tmp_pos = bufor.find_first_not_of(BR_SET, tmp_pos); if(tmp_pos == NOT_FOUND) { pos = POS_END; return; } pos = tmp_pos; }

Teraz dopiero możemy pisać nasz parser! Przy wykorzystaniu dwóch powyższych funkcji nie będzie to takie trudne zadanie.

bool parse() { COMMAND_INFO cmd_info = { CMD_UNKNOWN }; // zaraz wyjaśnimy, co to jest ;-) do { if(!get_token()) return true; if(cword.length() > 1) { if(cword.substr(0, 2) == "//") // komentarz - ignoruj resztę wiersza { ignore_line(); continue; } } // *** if(cword == "stwórz_potwora") { // ... if(!get_param()) return false; // pobranie parametru 1, np. gatunku potwora // ... if(!get_token()) //pobranie tekstowego parametru 2 - identyfikatora potworka { cout << "B£¡D, spodziewałem się jakiegoś słowa..." << endl; return false; } // ... if(!get_param()) return false; // pobranie parametru 3 // ... if(!get_param()) return false; // pobranie parametru 4 // ... } else if(cword == "stwórz_przedmiot") { // ... if(!get_param()) return false; // ... if(!get_param()) return false; // ... if(!get_param()) return false; // ... } else if(cword == "wyposaż_potwora") { // ... if(!get_token()) { cout << "B£¡D, spodziewałem się jakiegoś słowa..." << endl; return false; } // ... if(!get_param()) return false; // ... } else { cout << "Nieznane polecenie: " << cword << endl; return false; } // ... // *** } while(pos < bufor.length()-1) return true; }

Trochę dziwnie tego kod wygląda, nie? Należy się zatem kilka wyjaśnień. Po pierwsze, komentarzem z trzema gwiazdkami oznaczyliśmy sobie najważnieszy fragment funkcji parse. Funkcja ta nie jest jeszcze kompletna i w związku z tym oznaczony fragment będziemy jeszcze modyfikować. Komentarze z trzykropkami oznaczają właśnie te miejsca, gdzie jeszcze dodamy jeszcze kod.

Druga ważna uwaga. Po rzucie oka na powyższą funkcję nasuwa się uprzejme pytanie: po cholerę tu tyle powtórzeń? Nie można by pobierać parametrów niektórych poleceń skryptu w jakichś pętlach? Oczywiście, można by. Sęk w tym, że to by bardzo skutecznie zmniejszyło czytelność naszego kodu. Tymczasem patrząc na każdy blok else if od razu widzimy, ile i jakich parametrów posiada dane polecenie, co ułatwi nam na pewno dalszy rozwój naszego języka (który obecnie liczy sobie ledwie 3 polecenia, a może ich mieć kilkaset i więcej). Poza tym wczytując każdy parametr w oddzielnym wierszu kodu możemy łatwo dodać do niego komentarz (taki właśnie, jak w powyższym kodzie dla polecenia "stwórz_potwora").

Naturalnie, jeśli rozbudujemy nasz język do tych wspomnianych kilkuset poleceń, kod parsera będzie wyglądał jak siedem nieszczęść, zajmie bardzo wiele linii i w dodatku z pewnością nie będzie odpowiednio wydajny. Wówczas możemy pomyśleć o hierarchizacji poleceń oraz ich podziale na grupy o jednolitej składni. Kod stanie się mniej czytelny (przynajmiej dla początkujących programistów), krótszy i działający szybciej, a w dodatku nasz język będzie bardziej usystematyzowany. To jednak wykracza już poza zakres tego artykułu, który ma za zadanie opisać tylko podstawy tworzenia języka skryptowego.

Warto jeszcze wspomnieć, że w chwili obecnej nasz parser rozróżnia duże i małe litery w skrypcie. Wynika to z cech operatora == dla klasy string (a dokładniej z domyślnego parametru szablonu klasy basic_string, czyli char_traits). Co jednak fajne dla miłośników Linuksa czy programistów języków z rodziny C, niekoniecznie musi się podobać osobom, które będą pisały nasze skrypty. Możemy zmienić zachowanie operatora == tej klasy tak, aby ignorował wielkość liter[4]. Możemy też, zamiast używać tego operatora, wykorzystać np. funkcję strcmpi do porównywania, albo zamienić wszystkie litery przed porównaniem na duże za pomocą funkcji toupper.

Interpretacja

Teraz będziemy wypełniać luki, których sobie narobiliśmy poprzednio. Chyba się domyślasz, co powinno być w miejscach oznaczonych trzykropkami? Nietrudno zauważyć, że wprawdzie "wyciągnęliśmy" z bufora poszczególne części poleceń, ale nic z nimi nie robimy... Trzeba tymczasem dokonać ich interpretacji, czyli przetłumaczenia ciągów znaków w rodzaju "stwórz_potwora" na zrozumiałe dla naszej gry stałe, np. CMD_CREATE_MONSTER.

Co prawda, część interpretacji mamy już za sobą (tak jak wspomnieliśmy wcześniej, granice między warstwami parsingu i interpretacji są rozmyte). Mianowicie odrzuciliśmy wiersze zawierające komentarze i dokonaliśmy selekcji poleceń. Niewątpliwie obie te czynności związane są już z interpretowaniem wyrażeń, niebędących białymi spacjami.

Wyniki interpretacji trzeba gdzieś zapisywać. Same kody poleceń można zapamiętywać w zwykłych, skalarnych zmiennych, jednakże ponieważ przy większości poleceń mamy do czynienia również z parametrami, potrzebne nam będą struktury. Najpierw, dla utrzymania, stworzymy sobie typ wyliczeniowy, który posłuży nam do identyfikacji poszczególnych rodzajów poleceń naszego języka:

enum ECmdType { CMD_UNKNOWN, CMD_CREATE_MONSTER, CMD_CREATE_ITEM, CMD_EQUIP_MONSTER };

Następnym krokiem będzie zadeklarowanie struktury, w której będziemy przechowywać dane polecenie. I tu powstaje drobny problem: każde z naszych dotychczas wymyślonych trzech poleceń ma inną składnię, a więc należałoby do każdego użyć innej struktury. Nikt nie powiedział jednak, że musimy oszczędzać każdy bajt, dlatego by nie komplikować sobie życia, zrobimy uniwersalną strukturę, w której będzie można przechowywać każde polecenie, chociaż część tej struktury niemal w każdym przypadku będzie nieużywana. Popatrzmy na nasze dotychczasowe polecenia (jest ich 3); mają one maksymalnie po 3 parametry. Przewidując dynamiczny rozwój naszego wspaniałego języka możemy założyć, że stworzymy nawet trochę bardziej skomplikowane polecenia; ustalamy więc, że struktura do ich przechowywania będzie miała miejsce na maksymalnie 5 parametrów liczbowych i 3 tekstowe:

struct COMMAND_INFO { ECmdType Type; int n1, n2, n3, n4, n5; string s1, s2, s3; };

Ponieważ chcemy zapisać wszystkie polecenia w tablicy, deklarujemy ją sobie:

vector<COMMAND_INFO> Commands;

Jesteśmy już gotowi do dokończenia funkcji parse. Wykropkowane miejsca zastąpimy instrukcjami, wypełniającymi odpowiednie pola naszych struktur. Przypominam, że poniżej podany jest tylko ten fragment funkcji parse, który poprzednio oznaczyłem gwiazdkami:

if(cword == "stwórz_potwora") { cmd_info.Type = CMD_CREATE_MONSTER; if(!get_param()) return false; // pobranie parametru 1, np. gatunku potwora cmd_info.n1 = param; if(!get_token()) //pobranie tekstowego parametru 2 - identyfikatora potworka { cout << "B£¡D, spodziewałem się jakiegoś słowa..." << endl; return false; } cmd_info.s1 = cword; if(!get_param()) return false; // pobranie parametru 3 cmd_info.n2 = param; if(!get_param()) return false; // pobranie parametru 4 cmd_info.n3 = param; } else if(cword == "stwórz_przedmiot") { cmd_info.Type = CMD_CREATE_ITEM; if(!get_param()) return false; cmd_info.n1 = param; if(!get_param()) return false; cmd_info.n2 = param; if(!get_param()) return false; cmd_info.n3 = param; } else if(cword == "wyposaż_potwora") { cmd_info.Type = CMD_EQUIP_MONSTER; if(!get_token()) { cout << "B£¡D, spodziewałem się jakiegoś słowa..." << endl; return false; } cmd_info.s1 = cword; if(!get_param()) return false; cmd_info.n1 = param; } else { cout << "Nieznane polecenie: " << cword << endl; return false; } //dodanie polecenia do tablicy Commands.push_back(cmd_info);

W ten oto sposób powstała nam całkiem przyjemna w użyciu tablica poleceń. Możemy je teraz dość szybko wykonać w dowolnym momencie... No właśnie, "zapomnieliśmy" określić, na czym właściwie polega...

Wykonanie

Ostatnia warstwa systemu skryptowego jest już wręcz trywialna do zaimplementowania. Oczywiście mam na myśli wywołanie odpowiednich funkcji dla poszczególnych poleceń, zapisanych w tablicy. Samo napisanie tych funkcji nie musi być bowiem takie proste, zależy od gry, jaką tworzymy i nie mieści się bynajmniej w tematyce tego artykułu :-). Ze względu na to ostatnie, funkcje te będą u nas na razie tylko atrapami, wypisującymi w konsoli różne głupawe teksty:

void CreateMonster(int nMonsterType, string sMonsterID, int nPosX, int nPosY) { cout << "Tworzę potwornego potwora!" << endl; } void CreateItem(int nItemType, int nPosX, int nPosY) { cout << "Tworzę sobie przedmiocik." << endl; } void EquipMonster(string sMonsterID, int nItemType) { cout << "Daję potworowi przedmiot." << endl; }

Wspomniana funkcja o trywialnej roli wywoływacza innych funkcji:

void execute_commands(const vector<COMMAND_INFO>& cmd_array) { for(int i=0; i<cmd_array.size(); ++i) { switch(cmd_array[i].Type) { case CMD_CREATE_MONSTER: CreateMonster(cmd_array[i].n1, cmd_array[i].s1, cmd_array[i].n2, cmd_array[i].n3); break; case CMD_CREATE_ITEM: CreateItem(cmd_array[i].n1, cmd_array[i].n2, cmd_array[i].n3); break; case CMD_EQUIP_MONSTER: EquipMonster(cmd_array[i].s1, cmd_array[i].n1); break; } } }

Możemy wreszcie to wszystko poskładać do kupy i zobaczyć, jak działa:

#include <cstdio> #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; #pragma warning(disable: 4018 4267) // tutaj umieszczamy deklarację tych wszystkich struktur, // funkcji i innych rzeczy :-) int main() { ifstream plik; plik.open("skrypt.txt"); char c; while(true) { c = plik.get(); if(plik.eof()) break; bufor += c; } plik.close(); bool result; result = parse(); if(!result) { cout << err_str << endl; return 0; } execute_commands(); system("pause"); return 0; }

I to już wszystko, jeśli chodzi o "podstawy podstaw" skryptów... Ktoś mógłby powiedzieć, że dużo się nastukaliśmy w klawiaturę, a osiągnęliśmy niewiele. I po części miałby rację ten ktoś. Skrypty w postaci ciągu poleceń w wielu grach wprawdzie mogą nam się przydać (nawet bardzo), ale na przykład w takich grach RPG (gatunek zadziwiająco ostatnio popularny ;-)) zdecydowanie nie wystarczą. Dlatego też w drugiej części tego artykułu dowiemy się, jak rozszerzyć nasz język skryptowy o zmienne i możliwość wykonywania prostych operacji na nich, a także jak skonstruować instrukcje warunkowe, które mogłyby testować wartości zmiennych.

------------------
[1] Wiele gier korzysta jednak również z kompilowanych skryptów. Są to po prostu instrukcje skryptu już po parsingu i interpretacji, zapisane w postaci binarnej (a więc nieczytelne dla (normalnego) człowieka). Powód kompilowania skryptów jest prosty: wczytują się znacznie szybciej. Ponadto użytkownik końcowy nie może wówczas grzebać w "sekretach" gry (chyba, że naprawdę bardzo chce ;-)).
[2] Aby uniknąć jałowych polemik od razu tłumaczę, że mam tu ma myśli praktykę programistyczną, a nie teorię, która mówi zawsze o sytuacjach idealnych. W teorii bowiem programy powinno się projektować tak, aby wprowadzanie w nich wszelkich zmian było możliwie najprostsze. Jak wiemy, różnie z tym bywa ;-).
[3] O wykorzystywaniu windowsowej konsoli w grach i innych aplikacjach dowiesz się z artykułu Adama Sawickiego "Asynchroniczna konsola Windows" http://www.regedit.i365.pl/warsztat/articles.php?x=view&id=204
[4] B. Eckel "Thinking In C++, Volume 2", str. 134

[[Kategoria:c++]] http://darkcult.warsztat.gd/dnld/skrypty.pdf

Tekst dodał:
Złośliwiec
13.08.2006 09:56

Ostatnia edycja:
Złośliwiec
13.08.2006 09:56

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 13.08.2006 09:56 Złośliwiec 25.16 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • ~Katsuragi 24 czerwca 2007 13:50
    słaby artykuł. Dodawanie nowej komendy prowadzi do poprawiania kodu źródłowego w wielu miejscach. Instrukcja switch do wywoływania f-cji? nie, nie, nie :/
  • ~Neonek 17 stycznia 2008 18:37
    Jak dla mnie art spoko. Przydałby się jeszcze art o własnym języku znacznikowym :)
  • ~Koroner69 10 lipca 2008 18:03
    Ten tekst jest niezgodny z prawem i podrzega do nienawisci rasowej :P
  • cybek (@cybek) 03 stycznia 2009 10:59
    param = itoa(s_number.c_str()); Nie powinno czasem byc atoi() ?
  • ~makhzi 24 marca 2009 12:21
    Warto przeglądnąć antLR - do gramatyk LL(k)
  • Marek Zając (@marek1) 05 sierpnia 2009 20:15
    cybek: też to właśnie zauważyłem. Mam nadzieje że to poprawią. Bo itoa działa w drugą stronę (int->string) i początkującym może sprawić to kłopot.
  • kamilojza (@funmaker) 15 sierpnia 2009 09:37
    w kompilacji wyswietla mi błąd : 201 J:\Dev-Cpp\BezNazwy1.cpp too few arguments to function `void execute_commands(const std::vector<COMMAND_INFO, std::allocator<COMMAND_INFO> >&)'
  • codemasterpl (@codemasterpl) 07 września 2009 11:33
    Fajny art, uczy programowania a nie wykorzystywania gotowców.

    bravo :)
  • Damian (@Ktos) 04 stycznia 2012 16:50
    Swienty art : ) ale fakt faktem ze mozna zrobic to latwiej : P
  • starjacker0 (@starjacker0) 14 kwietnia 2014 08:23
    Skąd można pobrać ten plik skrypty.pdf? Link już nie działa.
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)