This is a tutorial for LPC and basic LPmud mudlib. The document was written for the Genesis driver and Genesis mudlib. Gamedriver version CD.04.02, Mudlib CD.00.31 though in all likelyhood it will still be usable for three-four versions further. Copyright (C) 1996 Ronny Wikh The use of this manual in any commercial venture is expressly forbidden, neither may it be offered as inducement to purchase other services or products. Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved in full on all copies. Permission is granted to copy and distribute modified versions of this manual under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one. Permission is granted to copy and distribute translations of this manual into another language, under the above conditions for modified versions, except that this permission notice may be stated in a translation approved by the holders of the above copyright. Przetlumaczyl na polski Jabub Szewczyk, Alvin@Arkadia Translated into polish by Jakub Szewczyk Ostatnia zmiana 25 sierpnia 1996 Spis tresci *********** .Spis tresci .LPC .Warunki rozpowszechniania .Wstep Podziekowania Forma podrecznika Historia LPC Gamedriver/Mudlib Struktura administracyjna Pisanie kodu 1. Wprowadzenie w LPC 1.1. Podstawowe zagadnienia zwiazane z programowaniem 1.1.1. Co to jest programowanie? 1.1.2. Kompilowany/interpretowany kod 1.1.3. Programy 1.1.4. Obiekty 1.1.5. Struktura obiektu 1.2. Podstawy LPC 1.2.1. Komentarze 1.2.2. Typy danych 1.2.3. Deklaracje zmiennych 1.2.4. Deklaracje funkcji 1.2.5. Instrukcje i wyrazenia 1.2.5.1. Instrukcje 1.2.5.2. Wyrazenia 1.2.5.3. Instrukcje grupujace (bloki) 1.2.5.4. Instrukcja ';' 1.2.6. Zasieg i prototypy 1.2.7. Operatory 1.2.7.1. Operatory rozne 1.2.7.2. Operatory arytmetyczne 1.2.7.3. Operatory binarne (booleanowskie) 1.2.7.4. Operatory warunkowe (logiczne) 1.2.7.5. Operatory przypisania 1.2.8. Podwojne operatory przypisania 1.2.9. Priorytet operatorow i kolejnosc wykonywania dzialan 1.2.10. Instrukcje warunkowe 1.2.10.1. Instrukcje if/else 1.2.10.2. Instrukcja switch 1.2.10.3. Wyrazenie ?: 1.2.11. Petle 1.2.11.1. Instrukcja while 1.2.11.2. Instrukcja for 1.2.11.3. Instrukcje break i continue 1.2.12. Tablice i mappingi 1.2.12.1. Jak deklarowac i uzywac tablic 1.2.12.2. Jak deklarowac i uzywac mappingow 1.3. Preprocesor 1.3.1. Instrukcja #include 1.3.2. Instrukcja #define 1.3.3. Instrukcje #if, #ifdef, #ifndef, #else, #elseif 2. Podstawy LPC i Mudliba 2.1. Male wybiegniecie w przyszlosc. 2.2. Drugi rzut oka na LPC 2.2.1. Wywolania funkcji 2.2.1.1. Tworzenie wewnatrz-obiektowych wywolan funkcji 2.2.1.2. Tworzenie prostych zewnatrz-obiektowych wywolan funkcji 2.2.1.3. Tworzenie wielokrotnych zewnatrz-obiektowych wywolan funkcji 2.2.2. Dziedziczenie obiektow 2.2.3. Cienie: Maskowanie funkcji w czasie dzialania programu 2.2.4. Identyfikacja typow 2.2.5. Kwalifikatory typow 2.2.5.1. Kwalifikator zmiennej static 2.2.5.2. Kwalifikator funkcji static 2.2.5.3. Kwalifikator funkcji/zmiennej private 2.2.5.4. Kwalifikator funkcji/zmiennej nomask 2.2.5.5. Kwalifikator funkcji/zmiennej public 2.2.5.6. Kwalifikator funkcji varargs 2.2.6. Drugi rzut oka na typy danych 2.2.7. Drugi rzut oka na instrukcje switch/case 2.2.8. catch/throw: Obsluga bledow w czasie dzialania programu 2.2.9. Wskazniki do tablic i mappingow 2.3. Interfejs LPC/Mudliba 2.3.1. Definicja obiektow standardowych i bibliotecznych 2.3.1.1. Podstawowy obiekt: /std/object.c 2.3.1.2. Standardowe klasy obiektow 2.3.1.3. Standardowe obiekty biblioteczne 2.3.2. Jak zdobyc odnosniki do obiektow 2.3.2.1. Odnosniki do aktualnego obiektu 2.3.2.2. Tworzenie obiektow 2.3.2.3. Odnajdywanie odnosnikow do innych obiektow 2.3.2.4. Odnosniki do interaktywnych obiektow 2.3.2.5. Niszczenie obiektow 2.3.3. Obsluga polecen przez obiekty 2.3.4. Alarmy: Niesynchroniczne wywolania funkcji 2.3.5. Srodowisko i zawartosc 2.3.6. Funkcje obslugujace lancuchy znakowe 2.3.7. Funkcje obslugujace znaczniki bitowe 2.3.8. Funkcje obslugujace czas 2.3.9. Konwersje tablica-string 2.3.10. Funkcje obslugujace tablice 2.3.11. Funkcje obslugujace mappingi 2.3.12. Konwersja typow 2.3.13. Funkcje matematyczne 2.3.14. Obsluga plikow 2.3.15. Oblsuga katalogow 2.3.16. Wejscie/wyjscie konsoli N3. Zaawansowany LPC i Mudlib N1. Pisanie efektywnego kodu N1. Efektywne petle LPC *** Granie w LPmudy jest bardzo ekscytujacym zajeciem, a pisanie ich jeszcze bardziej. Jest bardzo niewiele rzeczy, ktorych nie da sie zakodowac. Nieraz trzeba jednak uzywac roznych sztuczek. Najwieksza zaleta mudow jest naturalnie to, ze moze w nie grac naraz wielu graczy; widok setek, a nawet tysiacy ludzi uzywajacych i czerpiacych radosc z Twojego kodu sprawia wielka przyjemnosc. Ten podrecznik ma na celu nauczenie Cie niezbednych podstaw potrzebnych do tworzenia kodu w LPmudach i w Genesis w szczegolnosci. Nie jest to jednak latwa lektura, wiec spodziewaj sie, ze zajmie Ci kilka ladnych dni przyswojenie sobie tego, czego sprobuje Cie nauczyc. Pomimo tego groznie brzmiacego ostrzezenia, zycze Ci przyjemnego kodowania, po tym, jak juz przelamiesz pierwsze lody. -- Ronny Wikh, 25 styczen 1996 Warunki rozpowszechniania ************************* Podrecznik ten zostal napisany w dobrej wierze ludziom, ktorzy pisza mudy dla wlasnej przyjemnosci. Oznacza to, ze nie bede wymagal zadnych oplat za uzywanie i rozprowadzanie tego tekstu, pod warunkiem, ze taki wlasnie jest cel jego uzytkowania. W szczegolnosci chce sie upewnic, ze ci, ktorzy czerpia korzysci finansowe z prowadzenia muda nie uzywaja tego dokumentu jako pomocy w zarabianiu pieniedzy. Notka tlumacza ************** Tlumaczenie moze byc miejscami niezreczne. Jest tez spora szansa, ze napotkasz jakies bledy. W obu przypadkach wal z nimi prosze do mnie, czyli do Alvina. Moj email: alvin@imik.wip.pw.edu.pl. Mam nadzieje, ze Ci sie na cos zda to tlumaczenie. Wstep ***** Na poczatku moim zamierzeniem bylo, by dowolna osoba mogla korzystac i uczyc sie z tego dokumentu. Teraz jednak wydaje mi sie to raczej niemozliwe, wiec skoryguje to troszeczke. To jest podrecznik, z ktorego moze korzystac kazdy, kto ma przynajmniej blade pojecie o programowaniu oraz checi do nauki. Nie musisz znac C zanim zaczniesz i mam nadzieje, ze nawet kompletni laicy beda w stanie sie nauczyc kodowania, choc oczywiscie beda mieli o wiele wiecej roboty. Doswiadczeni programisci, a nawet ci, ktorzy pisali juz kiedys mudy beda musieli zaznajomic sie z tym podrecznikiem, gdyz wyjasnia on pojecia charakterystyczne tylko dla tej gry, aczkolwiek beda mogli po prostu przejrzec wiekszosc tekstu i skupic sie na bardziej zawilych fragmentach. Tobie, czytelniku, zostawiam wybor tego co jest dla Ciebie wazne, a co nie, gdyz chyba tylko Ty wiesz czego sie jeszcze powinienes nauczyc. Jako, ze jezyk LPC w aktualnej postaci jest mocno powiazany z mudlibem, ktory jest w nim napisany, zahacze rowniez o jego czesc. Jednakze *nie* bede Cie uczyl szczegolowo jak z niego korzystac - bedziesz musial przeczytac inne podreczniki. Wszystko to czyni ten dokument wyspecjalizowanym na LPmudy, a na Genesis w szczegolnosci. Pamietaj o tym, jesli piszesz pod innym mudem. Mam nadzieje, ze ten podrecznik okaze sie pomocny i warty wysilku, ktory trzeba poswiecic na jego przeczytanie. Z pewnoscia napisanie go nie bylo takie proste, wiec niech lepiej jego lektura nie bedzie bezowocna! :) i - Podziekowania ================= Chcialbym zaczac od podziekowan dla Thorsten'a Lockert'a i Christian'a Markus'a, byc moze lepiej znanych jako Plugh i Olorin, za korekte tekstu, dobre pomysly i ogolnie za pomocna dlon w czasie tworzenia tego podrecznika. Bez nich, napisanie tego raczej dlugiego projektu zajeloby duzo wiecej czasu, a koncowy efekt bylby znacznie gorszy. ii - Forma podrecznika ====================== Podrecznik jest podzielony na trzy rozdzialy, o rosnacym stopniu zaawansowania. Pierwszy rozdzial wyjasni podstawy kodowania i LPC bez zaglebiania sie w szczegoly. Drugi rozdzial traktuje o trudniejszych sprawach; wyjasni w pelni wszystkie aspekty funkcji i operatorow, ktore mogly zostac troszeczke zbyt pobieznie potraktowane w rozdziale pierwszym. Trzeci, koncowy rozdzial traktuje o tym, co moglo zostac pominiete w dwoch poprzednich. Tak na prawde, to nie o *wszystkim*; podrecznik nie wyjasni wszystkich zawilosci gamedrivera i mudliba. Jesli szukasz informacji o tworzeniu wlasnego mudliba albo o robieniu innych bardzo zaawansowanych rzeczy, bedziesz musial sam do tego dojsc, czytajac kod zrodlowy. Jesli jestes poczatkujacym wizardem, mozesz byc troche przytloczony na poczatku, patrzac na ten obszerny podrecznik. Jednakze jest wazne, zebys przeczytal *wszystko* i nie zostawial niczego na pozniej. Bez watpienia bedziesz musial poznac zagadnienia ze wszystkich trzech rozdzialow, choc przewaznie bedziesz korzystal z wiedzy zawartej w dwoch pierwszych. Kurcze! Jest wielu starych wizardow, ktorzy ledwo co opanowali pierwszy rozdzial! Jest to jeden z podstawowych powodow, dla ktorych pisze ten podrecznik... Ten podrecznik jest dosc rozlegly, pomimo tego, ze jest przeznaczony *tylko* do nauki kodowania w domenach. Oznacza to, ze nie jest to kompletna instrukcja LPC. Kilka rzadziej uzywanych efunkcji jest pominiete, gdyz sa one uzywane tylko przez ludzi piszacych oraz ulepszajacych mudliba i gamedrivera. Przykro mi, jesli wlasnie tego tu szukales - bedziesz sie musial rozejrzec za jakims innym zrodlem informacji. Mala uwaga o plci. Przez caly czas uzywam meskiego rodzaju. Nie dlatego, ze dyskrimunuje programistki, ale dlatego, ze jezyk angielski jest nastawiony na meski rodzaj. (przyp. tlum. hmm.. nie jestem pewien jak z polskim, ale to co autor dalej pisze to prawda) Mysle, ze moglbym za kazdym razem dopisywac tez zenska forme, lecz uwazam, ze byloby to troche glupawe. Dopoki angielski nie stanie sie w pelni neutralnym plciowo jezykiem, bede sie trzymal meskiego rodzaju. Rozdzialy, ktore szegolowo opisuja efunkcje/sfunkcje/lfunkcje/makra, beda mialy wymienione w nawiasach odwolania do nazw powiazanych z omawianymi rzeczami, aby ulatwic Ci pozniejsze wyszukiwanie tych informacji. iii - Historia LPC ================== LPC jest mudowym jezykiem programowania, stworzonym przez Lars'a Pensjoe'go na potrzeby wlasnego pomyslu LPMUDa, interaktywnego, wieluzytkownikowego srodowiska, przeznaczonego do kilku roznych celow, wsrod ktorych gra nie byla jedynym. Od czasu pierwszego pojawienia sie go, w 1988 roku, jezyk znacznie sie zmienil. Prace nad nim zostaly przejete okolo 1990 roku, przez innych ludzi z Chalmers Datofrerening, glownie przez Jakob'a Hallen'a, Lennarta Augustsson'a i Anders'a Chrigstroem'a. Rozszezyli i udoskonalili znacznie jezyk, aczkolwiek, jak nazwa LPC wskazuje, wciaz wykazuje on powiazania z jezykiem 'C'. Roznice glownie polegaja na dolaczeniu struktury obiektowej, jak rowniez na dodaniu kilku typow danych, w celu uprzystepnienia programowania. LPC nie jest tak swobodnym jezykiem jak klasyczny 'C', choc z drugiej strony bardziej sie nadaje do celu, dla jakiego zostal stworzony - do pisania srodowiska gry. iv - Gamedriver/Mudlib ====================== Definicja roznicy pomiedzy gamedriverem i mudlibem moze sie zdawac trudna do okreslenia, lecz tak na prawde jest bardzo prosta. Gamedriver jest to program, ktory jest uruchomiony na serwerze muda. Jest interpreterem polecen polaczonym z jadrem zarzadzania obiektami (object management kernel). Mozna nawet powiedziec, ze jest to swoisty system operacyjny. Definiuje i rozumie jezyk LPC, interpretuje i wykonuje instrukcje podane mu poprzez obiekty LPC. Mudlib jest zbiorem obiektow LPC, ktory tworzy podstawowe srodowisko gry. Gamedriver nie ma pojecia co ono robi (mozna powiedziec, ze mudlib nie ma pojecia jak to jest robione, podczas gdy gamedriver ma). Zawiera podstawowy zestaw obiektow LPC, ktore sa uzywane w grze: obiekty graczy, potworow, pokoi itd. To co Ty piszesz, czyli kod domenowy, jest przechwowywane i obslugiwane osobno i korzysta to zarowno z gamedrivera, jak i z mudliba. v - Struktura administracyjna ============================= Mozna powiedziec, ze gra jest podzielona na trzy charakterystyczne czesci, tak jak to juz zostalo powyzej zasugerowane; Gamedriver, mudlib i "kod domenowy". Gamedriver i mudlib juz zostaly opisane. Domeny sa sposobem na zorganizowanie pisanania kodu. Domena to grupa wizardow, pracujacych ku z gory okreslonemu celowi. Moze nim byc jakas ograniczona przestrzen, fragment swiata gry, lub rzeczy takie jak gildie badz sekty, ktorych czlonkami gracze moga zostac. W kazdej domenie jest jeden szef, Lord domeny. Jest on swego rodzaju nadzorca. Decyduje co sie w domenie ma dziac i w jakim kierunku prace powinny zdazac. W domenie caly kod moze byc latwo wymieniany i jest mocno ze soba powiazany. Oczywiscie, sa rowniez wiezy pomiedzy roznymi domenami, aczkolwiek sa one zazwyczaj slabsze, a kod jest rzadko wymieniany. Jako poczatkujacy wizard, bedziesz probowal znalezc sobie jakas domene, w ktorej praca wyda ci sie interesujaca i inspirujaca. vi - Pisanie kodu ================= Moze sie wydac nieco przedwczesnym mowienie Ci jak kod powinien wygladac, zanim nawet nie nauczyles sie jak go pisac. Jednakze jest to fundamentalna sprawa o wielkiej waznosci. Ktos fajnie powiedzial, ze wlasciwe pisanie kodu uczyni Twe zeby bielszymi, wlosy ciemniejszymi i pozytywnie wplynie na twoje zycie seksualne. Tego chyba nie moge zagwarantowac, ale na pewno drastycznie polepszy to jakosc, tego co piszesz, przy bardzo niskich nakladach pracy. Polega to glownie na samodyscyplinie. Oto kilka dobrych argumentow na pisanie poprawnego kodu: * Czyni Twoj kod znacznie bardziej przejrzystym, nie tylko dla innych, lecz rowniez dla Ciebie, szczegolnie jesli sam bedziesz musial przeczytac i poprawic go szesc miesiecy po tym, jak go napisales. * Jesli jest on bardziej przejrzysty dla innych, to latwiej im bedzie zrozumiec to, co skleciles i co za tym idze latwiej bedzie im pomoc ci w razie problemow. * Wlasciwe pisanie kodu czyni go *lepszym*, mozesz wierzyc lub nie. Przyczyna jest prosta: niepoprawne pisanie programu zwieksza bardzo szanse na to, ze przeoczysz jakies banalne bledy, schowane posrod scisnietego kodu. * Moze ci byc naprawde ciezko znalezc ludzi, ktorzy beda chcieli odpluskwic zle sformatowany kod. Ja osobiscie *nie* pomoge osobom w usunieciu bledow z programu, ktory wyglada okropnie. Przyczyna jest prosta: jest to gra nie warta swieczki. W zle wygladajacym kodzie kryje sie wiele glupich bledow (przewaznie brakujace, albo przemieszczone nawiasy), ktore wyjda na wierzch od razu po tym, jak wlasciwie sformatujesz program. Teraz bedzie troche przydluga instrukcja mowiaca o tym, jak nalezy ukladac kod w czasie pisania. Przeczytaj to, nawet jesli w pelni nie rozumiesz o czym jest mowa, a pozniej wroc i przeczytaj raz jeszcze po tym, jak zdobedziesz niezbedne umiejetnosci. W ten sposob utrwali Ci sie wlasciwy sposob pisania programow. 1. Jedno wciecie ma dlugosc 4 spacji, nie mniej, nie wiecej. Nowe wciecie zaczyna sie na poczatku kazdego bloku. 2. Pomiedzy slowami kluczowymi a otwierajacym nawiasem '(', jesli takowy jest, powinna byc jedna spacja. while (test) wyrazenie; 3. Nawiasy otwierajace i zamykajace ten sam blok powinny byc w tej samej kolumnie; w kolumnie pierwszej litery wyrazenia otwierajacego blok. if (to) { wyrazanie; } else if (tamto) { inne_wyrazenie; } else { standardowe_wyrazenie; } Natrafiamy teraz na punkt o prawie religijnym znaczeniu dla niektorych programistow. Przedstawiciele innej sekty wyznaja, ze nawias otwierajacy blok powinien znajdowac sie na koncu lini z wyrazeniem otwierajacym blok. Rob tak jak ja mowie, albo utkniesz w tym cholernym COBOLu na zawsze :) Teraz na serio, wybierz jedna metode i trzymaj sie jej. Jest na prawde *bardzo* wazna rzecza to, abys utrzymywal te sama dlugosc wciecia przez caly czas. Zmienne dlugosci wciec to cos, co nie moze byc tolerowane. 4. Liczne argumenty oddzielone od siebie przecinkami, maja po kazdym takowym jedna spacje. Listy argumentow oddzielone ';' i operatory numeryczne maja po jednej spacji przed i po operatorze. int a, b, c; for (a = 0 ; a < 10 ; a++) { b = wywolanie_funkcji(a, b * 2); c = b * 3 / 4; } 5. Jesli wyrazenie petli jest puste, umiesc konczacy ';' w oddzielnej linii. while (!(zmienna = funkcja(zmienna))) ; Przyczyna jest to, ze jakbys umieszczal ';' w tej samej linii, byloby *bardzo* latwo przeoczyc prawdziwe bledy takie jak ten, wynikajacy z czystego lentistwa. for (i = 0 ; i < 100 ; i++); { } 6. Wszystkie wyrazenia '#define' i '#include' powinny byc umieszczone na samym poczatku pliku. Mozliwe jest ich rozrzucenie, lecz to tylko gmatwa. 7. To samo z prototypami funkcji i zmiennymi typu global/static. Zbierz je do kupy, z odpowiednim komentarzem w naglowku i umiesc na poczatku pliku. Oczywiscie mozesz je rozrzucic po calym pliku, ale jakze latwe bedzie przeoczenie ich pozniej... 8. Przy deklaracji funkcji, typ zwracanej wartosci umieszczaj w linii przed nazwa funkcji. public void moja_funkcja(int a, int b) { < kod > } 9. Lam dlugie linie kodu w odpowiednich miejscach tak, zeby nie wychodzily poza 80-znakowy ekran. Wyglada to potwornie i ciezko sie to czyta, nie mowiac o problemach zwiazanych z pozniejszym drukowaniem. 10. Plik powinien sie zaczynac nastepujacym naglowkiem: /* * * * * * Copyright (C): * */ Juz *TERAZ* przeczytaj prawa autorskie, by wiedziec jakie zasady obowiazuja co do kodu, ktory piszesz dla gry. Powininny one sie znajdowac w pliku '/doc/COPYRIGHT'. Jesli ich tam nie ma, to po prostu spytaj jednego z administratorow gry. (przyp. tlum. U nas jeszcze nie ma czegos takiego i nie bedzie do czasu az ktos nie napisze ;) ) 11. *Kazda* funkcje zaczynaj od naglowka, ktory wyglada tak: /* * Nazwa funkcji: * Opis: * Argumenty * Zwraca: */ Jesli funkcja nie potrzebuje zadnych argumentow, lub nie zwraca niczego, to po prostu usun te linie z naglowka. 12. Umiesc stosowne komentarze przy kodzie tu i owdzie, tam gdzie uznasz, ze moze on byc niezrozumialy. Pamietaj rowniez o tym, ze na twoim (zakladanym) poziome kompetencji wiele rzeczy moze sie wydawac niezrozumialymi :) Czyn wedlug wlasnego uznania. /* * Komentarz poprzedzajacy kod, * opisujacy co on robi */ < kod > 13. Upewnij sie, ze nazwy funkcji i zmienne lokalne sa zapisane malymi literami alfabetu, z ewentualnymi odstepami pomiedzy wyrazami w postaci podkreslenia (np. 'nazwa_funkcji()'). Zmienne globalne powinny miec duze pierwsze litery slow (np. 'int GlobalTime;'). '#define' powinny byc zapisane duzymi literami (np. '#define AMEBA "jednokomorkowe zyjatko"'). W ten sposob latwo bedzie rozpoznac, jaki jest to rodzaj symbolu. Najprostszym sposobem zalapania w jaki sposob nalezy pisac, jest uzywanie edytora emacs, ustawionego na tryb c++. Taki tryb rozumie operatory '::', aczkolwiek wymaga kilku podpowiedzi w materii tabulacji itp. Umiesc te linie w pliku .emacs i wszystko powinno dzialac jak powinno: ;; emacs lisp script start (setq auto-mode-alist (append '( ("\\.l" . my-c++-mode) ("\\.y" . my-c++-moe) ("\\.c" . my-c++-mode) ("\\.h" . my-c++-mode)) auto-mode-alist)) (defun my-c++-mode () (interactive) (c++-mode) (setq c-indent-level 4) (setq c-brace-offset 0) (setq c-label-offset -4) (setq c-continued-brace-offset -4) (setq c-continued-statement-offset 4)) ;; emacs end Emacs ma jeszcze wbudowana mozliwosc, ktora sie przydaje przy odpluskwianiu wypocin innych ludzi. Poprawienie wciec w kodzie jest tak proste, jak wstukanie 'M-<', 'M->', 'M-x indent-region'. 1 - Wprowadzenie w LPC ********************** Ten rozdzial nauczy cie zupelnych podstaw programowania, potrzebnych do zrozumienia calosci. Oprocz tego wprowadzi cie w programowanie obiektowe oraz opisze kawalek mudliba. 1.1 - Podstawowe zagadnienia zwiazane z programowaniem ====================================================== Zaczynamy od podstawowych zagadnien zwiazanych z programowaniem, sktruktura i srodowiskiem LPC. 1.1.1 - Co to jest programowanie? --------------------------------- To jest calkiem filozoficzne pytanie. Trzymajmy sie jednakze praktycznej strony i zostawmy caly bitowy zen tym, ktorzy sie tym zajmuja. W zasadzie programowanie to sztuka identyfikowania problemu i zamieniania rozwiazania w symbole, ktore komputer jest w stanie zrozumiec. Dobry programista ma wysoko rozwinieta zdolnosc widzenia, jak problem moze byc podzielony na mniejsze problemiki, z ktorych kazdy mozna rozwiazac na kilka sposobow. Wie on takze, ktore z rozwiazan jest najefektywniejsze i powinno byc uzyte w danej sytuacji, biorac pod uwage szybkosc dzialania i zuzycie pamieci. Programista, jak juz poprzednio zasugerowalem, mowi komputerowi jak rozwiazac problem. Maszyna nie moze sama wymyslic rozwiazania. Jednakze jest ona o wiele szybsza od czlowieka, wiec problemy ktore mozesz rozwiazac, ale ktore zajelyby ci to mnostwo czasu, sa szybciutko rozwiazywane przez komputer. To, czego musisz sie nauczyc, to sposob myslenia, ktory umozliwi ci 'rozbijanie' problemow na kroki, ktore musisz wykonac od stanu poczatkowego do rozwiazanego problemu. Musisz takze poznac metody czynienia tych krokow. Naturalnie, ten podrecznik nie nauczy Cie jak programowac, a jedynie przedstawi Ci jezyk, za pomoca ktorego, bedziesz mogl wpisac program. Kto w takim razie nauczy cie programowania, jesli nie wiesz jak? Hmm.. W pierwszym rzedzie inni wizardzi oraz ty sam. Innymi slowy ciezka praca. Niestety *nigdy* nie ma zadnych skrotow, niezaleznie od tego, czego potrzebujesz sie nauczyc. Jednakze poniewaz to jest swietna zabawa, miejmy nadzieje ze zdobywanie tych umiejetnosci sprawi Ci wielka frajde. 1.1.2 - Kompilowany/interpretowany kod -------------------------------------- Programy sa niczym wiecej, jak tylko plikami zawierajacymi zbior instrukcji zrozumialych dla komputera. Programowac oznacza wpisywac listy komend w taki sposob, by komputer doszedl do zamierzonego przez nas celu. Zazwyczaj program jest kompilowany - tlumaczony - na niskopoziomowy kod wyrazony w symbolach binarnych (wysokie i niskie stany napiec w pamieci komputera), ktore maszyna rozumie bez problemu. Jezyk, w ktorym programujesz jest po prostu wygodnym kompromisem, ktory rozumiesz zarowno Ty jak i komputer. Przyczyna dla ktorej kompilujesz, jest to, ze tlumaczenie kodu jest dosc skomplikowane i zajmuje sporo czasu. Lepiej zrobic to raz, przechowac wyniki i odwolywac sie bezposrednio do nich w razie potrzeby. LPC jednakze nie jest kompilowany - jest interpretowany. Instrukcje sa czytanie i tlumaczone jedna po drugiej, wykonane i zapomniane. W sumie nie jest to tak do konca prawda. W rzeczywistosci gamedriver, ktory jest programem uruchomionym na serwerze, tlumaczy program w LPC do prostej, przejsciowej postaci kodu intstrukcji. Ten zestaw kodow komend tworzy tak zwany 'master object' i jest przechowywany w pamieci komputera. Kiedy uruchamiasz program LPC, instrukcje sa przegladane linia po lini, tak jak zostalo to powyzej opisane, powodujac wykonanie poprzednio okreslonego zestawu czynnosci zdefiniowanego przez instrukcje. Roznica pomiedzy posiadaniem zinterpretowanego kodu, a posiadaniem skompilowanego polega na tym, ze o ile skompilowany kod jest szybszy, to zinterpretowana wersja jest o wiele latwiejsza do zmodyfikowania. Jesli chcesz dokonac zmiany w skompilowanej wesji, musisz zmienic kod zrodlowy, zrekompilowac, przechowac nowa wersje i dopiero wyprobowac. Gdy masz zinterpretowany kod, wystarczy ze zmienisz zrodlo i uruchomisz go. W LPC musisz jeszcze poinstruowac gamedrivera, aby zniszczyl stary 'master object', ale o tym pozniej. 1.1.3 - Programy ---------------- Programy LPC, jak zostalo wyzej powiedziane, maja postac plikow zawierajacych instrukcje dla komputera, napisane w jezyku LPC. Ich nazwy musza sie konczyc literami '.c' (np. 'test.c'), dzieki czemu gamedriver wie z czym ma doczynienia. Nazwa pliku moze byc dowolnym lancuchem znakowym o dlugosci mniejszej niz 32 znaki, zaczynajacym sie od litery alfabetu. W praktyce jednakze, lepiej jest ograniczac nazwy plikow do 16 znakow, na ktore skladaja sie tylko male litery alfabetu. Jesli chcesz by nazwa skladala sie z dwoch slow, mozesz je oddzielic za pomoca znaku '_' (np. 'moja_nazwa_pliku.c'). 1.1.4 - Obiekty --------------- "Obiekt" w LPC jest po prostu kopia istniejacego i zaladowanego w pamieci komputera programu. Kiedy program jest ladowany do pamieci, by utworzyc "master object", kod jest kompilowany i wytworza poprzednio opisany zestaw instrukcji. Dolaczany przy tym jest takze maly obszar pamieci przeznaczony dla "zmiennych globalnych" (opisane dalej). Kiedy kopia, "klon" programu jest tworzony, specjalny odnosnik zwany "wskaznikiem obiektu" (object pointer) jest kreowany. Dostaje on adres do master object'u oraz unikalny obszar pamieci. Kiedy klonujesz obiekt jeszcze raz, tworzony jest nowy wskaznik i przydzielana jest nowa pamiec. Gdy obiekt jest niszczony, zaalokowana pamiec zostaje uwolniona, tak aby inne obiekty mogly z niej korzystac. Sam master object, czyli wyzej opisana lista intrukcji zostaje nienaruszona. Obiekt *zawsze* jest klonowany z master object'u. Jesli chcesz klonowac nowa wersje obiektu, najpierw musisz uaktualnic (update) master object, aby gamedriver wiedzial, ze nowa lista instrukcji ma byc kompilowana ze zmienionego kodu zrodlowego. W takiej sytuacji, istniejace klony nie beda zmieniane tylko z tego powodu, ze master object ulegl zmianie. Beda mialy odniesienie do starej listy instrukcji. Wazne jest abys pamietal, ze zachowanie starych klonow nie zmienia sie tylko dlatego, ze uaktualniles master object. Jak widzisz jest mozliwe posiadanie klonow obiektow, ktore sie zachowuja roznie, po prostu dlatego, ze sa one skompilowane z roznych zrodel, sklonowane pomiedzy uaktualnieniami i zmianami w kodzie. Moze to byc przyczyna duzych nieporozumien, wiec pamietaj o tym. 1.1.5 - Struktura obiektu ------------------------- Obiekt jest zlozony z "funkcji" i "zmiennych". Funkcja to zestaw instrukcji, do ktorych mozna sie odwolac za pomoca nazwy. Zmienna to swego rodzaju pojemnik, w ktorym mozna przechowywac dane do uzytku przez funkcje. Kilka funkcji jest zdefiniowane z gory w gamedriverze i sa one nazywane "funkcjami zewnetrznymi" (external funcions), albo efunkcjami (efuns). Funkcje zdefiniowane w kodzie LPC sa nazywane "lokalnymi funkcjami" (local functions), albo "lfunkcjami" (lfuns). Zeby jeszcze bardziej pogmatwac sprawe sa funkcje, ktore zachowuja sie jak efunkcje, ale sa napisane w kodzie LPC. Sa one zwane "symulowanymi efunkcjami" (simulated efuns), albo "sfunkcjami" (sfuns). Efunkcja to taka, ktorej sie nie da stworzyc w LPC. Na przyklad funkcja "write()", ktora umozliwia wyswietlenie tekstu na ekranie gracza. Niemozliwe jest stworzenie jej z innych funkcji LPC, wiec musi ona byc w gamedriverze. Ta efunkcja jest dostepna dla *wszystkich* programow LPC. Efunkcje wogole nie wiedza w jakim srodowisku sa uzywane i nie martwia sie o to, czy sa stosowane do symulowania smaku truskawki, czy jako czesc gry. Funkcja taka jak 'add_exit()', ktora dodaje wyjscie z pokoju jest dostepna tylko obiektom typu pokoj i napisano ja w LPC. Lfunkcje z zasady sa czescia struktury srodowiska w jakim obiekty sa uzyte. Nasz przykladowy 'add_exit()' swietnie radzi sobie z takimi rzeczami, jak kierunki wyjsc i koszty podrozy(zmeczenie), ale jest ograniczony tylko do tego - nie potrafi nic wiecej. Funkcja 'creator()' jest dobrym przykladem trzeciego rodzaju. Jest ona dostepna wszedzie. Zwraca kto stworzyl podany obiekt. Ta informacja jest jednak bardzo charakterystyczna dla srodowiska, poniewaz uzywa takich pojec jak lokalizacja kodu. Taki rodzaj funkcji jest latwy do napisania w LPC, ale z drugiej strony musi byc dostepny we wszystkich obiektach, tak jak by to byly efunkcje. Z tej przyczyny zostal stworzony specjalny obiekt '/secure/simul_efun.c', ktory jest dostepny ze wszystkich innych obiektow w grze. Znajdziesz tam wszystkie sfunkcje. To wszystko jest w sumie niewidzialne dla ciebie; ty po prostu uzywasz ich jako efunkcji, bez zawracania sobie glowy, ze jest to jakas sfunkcja. 1.2 - Podstawy LPC ================== LPC jest bardzo podobny do jezyka C, choc mozna sie dopatrzec kilku roznic. Jak doswiadczony programista zapewne dostrzeze, jest troche uproszczony poprzez dodanie dla wygody kilku nowych typow i zestawu funkcji obslugujacych je. Roznice nie sa jednak na tyle powazne, by mogly spowodowac jakies problemy, o ile sie o nich pamieta. 1.2.1 - Komentarze ------------------ Moze to troche dziwnie wygladac, ze zaczynam akurat od tego, ale komentarze wystepuja wszedzie, wiec musisz umiec rozpoznawac je od samego poczatku. Sa dwa typy komentarzy: // To jest komentarz trwajacy do konca tej linii. /* To jest komentarz ograniczony */ Jak widzisz, pierwszy typ komentarzy zaczyna sie od znakow `//' i trwa az do konca linii. Jesli chcesz miec wiecej linii komentarzy, to musisz na poczatku kazdej napisac `//'. Drugi typ jest taki, ze ma okreslona dlugosc. Zaczyna sie od znakow `/*' i konczy znakami `*/'. Ten rodzaj komentarzy jest uzyteczny, gdy chcesz zawrzec tekst, ktory zajmuje wiele linii. *UWAGA!* Komentarz `/* */' *nie* moze byc zagniezdzany, tzn nie mozesz zrobic czegos takiego jak w tym przykladzie: /* Komentarz /* Zagniezdzony komentarz */ kontynuacja pierwszego */ W takiej sytuacji komentarz skonczy sie na pierwszym napotkanym `*/', pozostawiajac tekst `kontynuacja pierwszego */' kompilatorowi, ktory bedzie to probowal zinterpretowac tak, jakby to byl kod LPC. Oczywiscie cos takiego nie zadziala i otrzymasz komunikat bledu. 1.2.2 - Typy danych ------------------- Obiekty przechowuja informacje w "zmiennych". Jak sama nazwa wskazuje, sa one oznaczonymi schowkami, ktore moga magazynowac informacje, ktore sie zmieniaja od czasu do czasu. Obiekt operuje na nich poprzez funkcje, ktore to zarowno uzywaja jak i zwracaja dane roznych typow. W zasadzie tylko jeden typ danych jest potrzebny, cos w rodzaju uniwersalnego pojemnika, ktory moze przechowywac cokolwiek zechcesz. W rzeczywistosci jest o wiele lepiej, jesli rozroznisz rozne typy informacji od siebie. Moze sie wydawac, ze to tylko przysparzy ci jeszcze wiecej problemow, ale tak na prawde to redukuje to ryzyko bledu, podnosi czytelnosc oraz *znacznie* przyspiesza kodowanie i odpluskwianie obiektu. W LPC jest mozliwosc uzywania tylko danych 'ogolnego uzytku', o ktorych wczesniej mowilem. We wczesniejszych wersjach jezyka, byl to jedyny dostepny typ. Jednakze w LPC, ktorego uzywamy dzis, wielce korzystne jest unikanie tego jak sie tylko da. Zaczynaj swe programy ta instrukcja: #pragma strict_types Mowi ona gamedriverowi by sprawdzil wszystkie funkcje, czy sa dostosowane do sytuacji w jakiej sa uzywane i co za tym idzie, czy nie powoduja zadnych bledow. Jest to wielka pomoc we wczesnym wykrywaniu bledow i dzieki temu nie bedziesz musial sie pozniej glowic, kiedy program nie bedzie dzialal do konca tak, jak bys chcial. Istnieja nastepujace typy danych: `void' `Nic' Ten typ danych jest uzywany w funkcjach, ktore nie zwracaja zadnych danych. 'int' `Calkowite' Wszystkie liczby z zakresu od -2147483648 do 2147483647. np. `3, 17, -32, 999'. `float' `Liczby zmiennoprzecinkowe' Ulamki dziesietne mieszczace sie w przyblizonym zakresie od 1.17549435e-38 do 3.40282347e+38, np. `1.7, -348.4, 4.53e+4'. Zasieg jest przyblizony dlatego, ze moze byc on inny w roznych mudach, w zaleznosci od systemu na jakim sa uruchomione. Jesli sa tu jacys milosnicy FORTRANa, to niech uwazaja na to, ze numery typu `1.' albo `.4711' *nie* sa rozpoznawane jako zmiennoprzecinkowe. Musicie podac zarowno calkowita, jak i ulamkowa czesc, nawet jesli ktoras z nich jest zerowa. `string' `Lancuch znakow' Stringi to po prostu lancuchy znakow, zawarte w cudzyslowy, np. `"x", "lancuch", "Kolejny dlugi lancuch z numerkiem 5 w srodku"'. Stringi moga zawierac specjalne znaki, takie jak znak nowej linii ('"\n"'). Wiele wyrazen jezyka LPC moze obslugiwac stringi bezposrednio, w przeciwienstwie do C. Czyni to je bardzo uzytecznymi i latwymi w obsludze. `mapping' `Lista powiazan' Mappingi to kolejny bardzo wygodny wynalazek LPC (uwaga, bardzo pamieciozerny, uzywaj go z umiarem). Mapping jest to lista polaczonych ze soba wartosci. Zalozmy, ze chcesz przechowac wiek kilku osob, np. ze Olek ma 23 lata, Piotr 54, a Ania 17. W LPC mozna to zapisac w ten sposob: `([ "Olek":23, "Piotr":54, "Ania":17 ])'. Jak widzisz wartosc po lewej stronie zostala powiazana z wartoscia po prawej. Mozesz wylowic jakies powiazanie, poprzez podanie lewej strony. `object' `Wskaznik obiektu' Jest to odnosnik do klonu programu LPC, ktory jest zaladowany do pamieci. `function' `Wskaznik funckji' Odnosnik do funkcji. `Array' `Tablica' Wszystkie powyzsze typy moga wystepowac jako tablice. Wtedy, w definicji, przed nazwa kazdej zmiennej wstawia sie `*', np. `int *a, b;' definiuje dwie zmienne: tablice a oraz pojedyncza zmienna b. W LPC tablice wygladaja bardziej jak listy, niz jak prawdziwe tablice. Istnieje wiele funkcji, ktore zostaly napisane by przyspieszyc i ulatwic ich obsluge. `mixed' No i na koniec ogolny typ, ktory zastepuje wszystkie inne, swego rodzaju klucz uniwersalny. Pod zmienne mixed mozna podstawic wartosc dowolnego innego typu. Jeszcze raz powtorze, ze uzywanie go poza sytuacjami, kiedy to jest *absolutnie* konieczne, tylko prowokuje bledy. Hmm, jak mi zwrocono uwage, to co napisalem moze brzmiec ciut za surowo. Typ mixed jest calkiem czesto uzywany. Chodzilo mi o to, ze o ile da sie zastosowac jakis normalny typ, to sie go powinno uzyc. Nie zastepuj go mixed'em tylko dlatego, ze jestes leniwy. 1.2.3 - Deklaracje zmiennych ---------------------------- Zmienna jest to lancuch znakow identyfikujacy 'skrytke', w ktorej dane sa przechowywane. Skrytka ma podana nazwe, skladajaca sie z maks. 32 znakow, gdzie pierwszy z nich musi byc litera alfabetu. Ustalono, ze wszystkie zmienne zdefiniowane wewnatrz funkcji beda sie skladaly z samych malych liter. Globalne zmienne zas, beda mialy pierwsza litere duza i reszte mala. Ustalono rowniez, ze nie bedzie sie oddzielalo wyrazow znakami innymi niz '_'. Nazwy zmiennych *zawsze* powinny odzwierciedlac to, do czego sa uzywane. Zmienne deklarujemy w nastepujacy sposob: , , ..., ; np: int licznik; float wysokosc, waga; mapping map_wieku; Zmienne musza byc zadeklarowane na poczatku bloku (tuz po znaku otwierajacym blok '{'), przed kodem wlasciwym. Zmienne globalne, czyli takie, ktore sa dostepne w calym programie, powinny byc zadeklarowane na poczatku pliku. Kiedy deklaracje sa wykonywane w czasie uruchamiania programu, sa one ustawiane na 0, a *NIE* na ich 'puste'(null) wartosci. Innymi slowy np. mappingi, tablice i stringi beda zawsze ustawiane na 0, a nie na `([])', `({})' i `""' tak, jak bys mogl przypuszac. Mozliwe jest zainicjalizowanie zmiennych juz w samej deklaracji. Jest to nawet bardzo dobry zwyczaj. A robi sie to tak: = , itp. np. int licznik = 8; float wysokosc = 3.0, waga = 1.2; mapping map_wieku = ([]); object *potwory = ({}); Powodem dla ktorego tablice i mappingi powinny byc inicjalizowane na swoje wartosci 'puste' (czyli kolejno `({})' i `([])') jest to, ze inaczej beda ustawione na 0, co moze byc przyczyna niekompatybilnosci typow i moze spowodowac pozniej jakies problemy. 1.2.4 - Definicje funkcji ------------------------- Fukcja musi dac znac jaki typ danych zwraca, o ile wogole cos zwraca. Tak jak zmienne, funkcje to sa nazwy skladajace sie z maks. 32 znakow, gdzie pierwszy z nich to litera. Przyjete jest, ze nazwy wszystkich funkcji beda pisane malymi literami, a do oddzielania wyrazow bedzie sie uzywalo tylko znaku '_'. Nazywaj funkcje tak, aby jasno odzwierciedlaly to co robia. Deklaracja funkcji wyglada tak: /* * Nazwa funkcji: * Opis: * Argumenty: * Zwraca: */ () { } /* * Function name: oblicz_srednice * Description: Oblicza srednice okregu przy podanym obwodzie * Variables: obwod - obwod okregu * name - nazwa dana okregowi * Returns: Srednice. */ float oblicz_srednice(float obwod, string nazwa) { float rval; // Obwod = pi * srednica rval = obwod / 3.141592643; write("Srednica okregu " + nazwa + " wynosi " + ftoa(rval) + "\n"); return rval; } Argumenty sa od siebie oddzielone przecinkami, tak jak w deklaracjach zmiennych. Ustalasz tu jaki typ danych bedzie wyslany do funkcji i kojarzysz nazwe zmiennej z tym typem, do pozniejszego uzytku przez funkcje. Dane z argumentow, beda mogly byc uzyte *tylko* wewnatrz funckji, no chyba, ze wyslesz je na zewnatrz poprzez wywolanie innej funkcji. (Zeby zachowac troche miejsca oraz zwiekszyc przejrzystosc podrecznika nie bede umieszczal naglowka przed wszystkimi moimi krotkimi przykladami funkcji). Funkcja, ktora nie zwraca zadnych danych, powinna miec typ zadeklarowany jako `void'. void write_all(string komunikat) { users()->catch_msg(komunikat); } 1.2.5 - Instrukcje i wyrazenia ------------------------------ Zebym mogl cokolwiek wyjasnic, wpierw okresle co oznacza "instrukcja" (statement), a co "wyrazenie" (expression). 1.2.5.1 - Instrukcje .................... Instrukcja jest swego rodzaju zdaniem, stworzonym z jednego lub wiecej wyrazen. Instrukcje zazwyczaj zajmuja jedna linie kodu. Czasem zachodzi potrzeba zlamania ich gdy sa za dlugie, w celu zwiekszenia czytelnosci. W wiekszosci instrukcji wystarczy, ze zlamiesz je w przerwie pomiedzy dwoma wyrazami. Inaczej jest w lancuchach znakowych - w takim przypadku musisz wstawic backslash (`\') na koncu pierwszej lamanej linii. Robi sie to po to, by gamedriver wiedzial co jest grane. write("To jest przyklad \ zlamanego lancucha(stringa).\n"); Jednakze lamanie stringow za pomoca backslash'a wyglada bardzo nieladnie i czyni tekst trudnym do przeczytania. Zazwyczaj jest mozliwosc lamania linii naturalnie, na koncu stringa, albo nawet podzielenia lancucha w polowie i dodaniu obu czesci za pomoca operatora `+'. Tak na prawde, jedynym miejscem gdzie backslash jest naprawde niezbedny, to instrukcja `#define' - ale o tym pozniej. write("To jest lepszy sposob " + "lamania stringow.\n"); Instrukcje w LPC zazwyczaj sa zakonczone srednikiem `;'. Jest to rowniez dobre miejsce na zakonczenie linii. Nic nie wstrzymuje cie przed zaczynaniem kolejnej instrukcji zaraz po poprzedniej, w tej samej linii, tylko ze wyglada to potwornie. 1.2.5.2 - Wyrazenia ................... Wyrazenie jest to opis jednej lub wiecej czynnosci, ktorych wykonanie powoduje uzyskanie danych jakiegos rodzaju. Na przyklad taki `+'. Uzywa on dwoch wyrazen, ktore w sumie daja jakis wynik. Zmienna tez jest wyrazeniem, poniewaz przechowuje ona dane jako wynik. Nastepujaca kombinacja dwoch wyrazen i operatora tez jest poprawnym wyrazeniem: `a + b' - gdzie `a' oraz `b' sa zmiennymi (wyrazeniami), a `+' jest operatorem. `a = b + c;' jest pelna instrukcja konczaca sie na sredniku `;'. Wywolania funkcji sa poprawnymi wyrazeniami. Sa zapisane w postaci nazwy oraz pary nawiasow, w ktorych znajduja sie argumenty, ktore funkcja wykorzystuje wewnatrz. Na przyklad funkcja `max()', ktora zwraca wyzszy z dwoch podanych argumentow. Aby znalezc wyzsza liczbe, np sposrod `4' i `10', trzeba napisac `max(4, 10)', co zwroci wartosc `10' i dzieki temu mozemy to nazwac wyrazeniem. Oczywiscie warto by jakos ten wynik przechowac. 1.2.5.3 - Instrukcje grupujace (bloki) ...................................... Jest wiele instrukcji, na przyklad instrukcje warunkowe, ktore w okreslonych warunkach tylko *raz* wykonaja podana instrukcje. Zalozmy, ze w takim wypadku chcesz wykonac kilka instrukcji, a nie tylko jedna. Do tego celu istnieje specjalna instrukcja zwana blokiem (instrukcja grupujaca). Znak `{' definiuje poczatek bloku, a `}' jego koniec. Wewnatrz tych nawiasow mozesz umiescic tyle instrukcji (wlaczajac w to definicje zmiennych), ile tylko zechcesz. Instrukcja grupujaca nie konczy sie srednikiem `;' i nic nie zmieni, jesli przez przypadek umiescisz jeden. 1.2.5.4 - Instrukcja `;' ........................ Jak juz powiedzialem, `;' jest przewaznie uzywany to zakanczania instrukcji, ale powinienes wiedziec, ze srednik `;' jest rowniez instrukcja sama w sobie. Sam `;' bedzie po prostu pusta instrukcja, ktora nie powoduje niczego. Jest ona uzyteczna, gdy potrzebujesz zastosowac petle testowe (petle beda opisane pozniej), ktore beda sie tylko wykonywaly (i nie beda robily przy tym zadnych innych rzeczy, jak to zwykle bywa z petlami). 1.2.6 - Zasieg i prototypy -------------------------- "Zasieg" to termin okreslajacy gdzie funkcja lub deklaracja zmiennej jest poprawna. Poniewaz programy sa czytane od gory do dolu, od lewej do prawej (zupelnie tak, jak ty czytasz te strone), zadeklarowane funkcje i zmienne sa dostepne na prawo i ponizej ich deklaracji. Zasieg ich, moze byc jednak ograniczony. Zmienna zadeklarowana wewnatrz funkcji, jest dostepna az do znaku zakonczenia bloku `}', w ktorym to ta zmienna jest zdefiniowana. < Poczatek pliku > int GlownyLicznik; // Tylko GlownyLicznik jest tu dostepny void var_func(int arg) { int zmienna_1; // GlownyLicznik, arg oraz zmienna_1 sa tu dostepne < kod > { string zmienna_2; // GlownyLicznik, arg, zmienna_1 i zmienna_2 sa dostepne w tym // bloku < kod > } // GlownyLicznik, arg i zmienna_1 sa tu dostepne < kod > { int zmienna_2; mapping zmienna_3; // GlownyLicznik, arg, zmienna_1, zmienna_2 oraz zmienna_3 sa tu // dostepne // *UWAGA* zmienna_2 jest NOWA i nie ma nic wspolnego z ta // zdefiniowana funkcje wczesniej < kod > } // GlownyLicznik, arg oraz zmienna_1 sa tu dostepne < kod > } // Tu tylko GlownyLicznik (i funkcja var_func) jest tu dostepny Deklaracje funkcji obowiazuje ta sama zasada. Nie mozesz tylko zadeklarowac jednej funkcji wewnatrz drugiej. Wyobraz sobie jednak sytuacje, gdzie masz dwie funkcje i jedna z nich korzysta z drugiej. int /* Definicja func_1. */ func_1() { < kod > func_2("test"); /* Wywolanie func_2 z argumentem "test". */ } void /* Definicja func_2. */ func_2(string dane) { < kod > } I tu natrafiasz na problem, poniewaz pierwsza funkcja probuje wykorzystac druga, zanim tamta jest zadeklarowana. Jesli poinstruowales gamedriver, zeby wymagal zgodnosci typow poprzez napisanie `#pragma strict_types', to wyskoczy ci komunikat bledu. Aby tego uniknac mozesz przekomponowac program w taki sposob, by `func_2' byla zadeklarowana _przed_ `func_1'. Czasem jednak nie zawsze jest to mozliwe, a poza tym moze na tym ucierpiec wyglad programu. Jest jeszcze inny, lepszy sposob. Polega on na napisaniu "prototypu funkcji". Powinien on zostac umieszczony na poczatku pliku, tuz po instrukcjach `inherit' i `#include'. Powinien wygladac *identycznie* jak deklaracja funkcji. A wiec mamy: < poczatek pliku, instrukcje `inherit' i `#include' > void func_2(string dane); // <- to jest prototyp funkcji func_2 < definicje zmiennych globalnych, itp. > void func_1() { < kod func_1() > func_2("jakis string"); // wywolanie func_2() } void func_2(string dane) { < kod funkcji func_2 > } 1.2.7 - Operatory (operator expressions) ---------------------------------------- W jezyku LPC jest zdefiniowana spora grupa operatorow, czyli takich cosikow, ktore operuja na wyrazeniach. Bede uzywal pewnego skrotowego sposobu notacji, dzieki ktoremu zaoszczedzi sie sporo miejsca. `W' Bedzie oznaczalo dowolne wyrazenie, nawet bardzo pogmatwane. `Z' Bedzie oznaczalo jakas zmienna. 1.2.7.1 - Operatory rozne ......................... 1. (W) W jest obliczane przed robieniem czegokolwiek poza nawiasami. Jest to uzyteczne w izolowaniu wyrazen, ktore trzeba wykonac w jakiejs okreslonej kolejnosci, albo gdy nie jestes pewien jaka jest kolejnosc dzialan (o tym pozniej). 2. W1, W2 W1 jest wykonywane jako pierwsze i jego rezultat zostaje zapamietany, wtedy wykonywane jest W2, a jego rezultat jest wyrzucany. Na koniec przechowany rezultat W1 jest zwracany jako wynik calego wyrazenia. Instrukcja `a = 1, 2, 3;' ustawi wartosc `a' na `1'. 3. Z = W Zmienna ma przypisywana wartosc W. Rezultatem calego wyrazenia jest rowniez wartosc W. `a = b = 4;' ustali wartosc a oraz b na 4. Moze to byc tez zapisane w ten sposob: `a = (b = 4)', co ilustruje kolejnosc wykonywania. 1.2.7.2 - Operatory arytmetyczne ................................ 1. W1 + W2 Wyrazenia sa obliczane i ich rezultaty zostaja do siebie dodane. Mozesz sumowac ze soba integery, floaty, stringi, tablice i mappingi. Stringi, tablice i mappingi sa po prostu ze soba wiazane - poczatek drugiego jest przylaczany do konca pierwszego argumentu. Istnieje takze mozliwosc dodawania integerow do stringow. W takiej sytuacji integer bedzie przekonwertowany w stringa i doklejony do niego. 2. W1 - W2 W2 jest odejmowane od W1. Mozesz odejmowac integery, floaty i dowolne tablice, ktore maja ten sam typ. Jesli element z odejmowanej tablicy istnieje w tej, od ktorej sie odejmuje, to jest on po prostu usuwany. Jesli nie ma takowego, to tablica pozostaje nienaruszona. 3. W1 * W2 W1 jest mnozone przez W2. Dziala tylko na integerach i floatach. 4. W1 / W2 W1 jest dzielone przez W2. Dziala tylko na integerach i floatach. 5. W1 % W2 Reszta z dzielenia 'W1 / W2' jest zwracana. Dziala to tylko dla integerow. '14 % 3' zwroci 2, poniewaz '14 / 3 - 2 = 0'. 6. -W Zwroci W z odwroconym znakiem. Dziala to tylko dla integerow i floatow. 7. W++ (lub ++W) Zwieksza W o 1. Jesli plusy znajduja sie przed wyrazeniem, to wyrazenie zwraca nowa wartosc. Jesli sa za wyrazeniem, to zwraca jeszcze stara. 8. W-- (lub --W) Zmniejsza W o 1. Jesli plusy znajduja sie przed wyrazeniem, to wyrazenie zwraca nowa wartosc. Jesli sa za wyrazeniem, to zwraca jeszcze stara. 1.2.7.3 - Operatory booleanowskie(binarne) .......................................... Operatorow booleanowskich mozna uzywac tylko na integerach. Wyjatkiem jest operator `&', ktory moze byc zastosowany rowniez na tablicach. Integer ma dlugosc 32 bitow. Jednakze w przykladach bede ukazywal tylko 10 ostatnich bitow, jako ze pozostale beda mialy wartosc 0 i beda one mogly byc przez to zignorowane, z wyjatkiem operatora `~'. 1. W1 & W2 W1 i W2. (iloczyn logiczny) 1011101001 (= 745) 1000100010 & (= 546) ------------ 1000100000 (= 544) => 745 & 546 = 544 Uzyty na dwoch tablicach, zwroci nam nowa tablice zawierajaca wszystkie elementy, ktore wstepuja w obu tablicach jednoczesnie. 2. W1 | W2 W1 lub W2. (suma logiczna) 1011101001 (= 745) 1000100010 | (= 546) ------------ 1011101011 (= 747) => 745 | 546 = 747 3. W1 ^ W2 W1 xor (exclusive or - nierownowaznosc; LUB wykluczajace) W2. 1011101001 (= 745) 1000100010 ^ (= 546) ------------ 0011001011 (= 203) => 745 ^ 546 = 203 4. ~W 1-complement of E (odwrocenie W). 00000000000000000000001011101001 ~ (= 745) ---------------------------------- 11111111111111111111110100010110 (= -746) => ~745 = -746 5. W1 << W2 W1 jest przesuwane w lewo o W2 krokow. 5 << 4 => 101(b) << 4 = 1010000(b) = 80 6. W1 >> W2 W1 jest przesuwane w prawo o W2 krokow. 1054 >> 5 => 10000011110(b) >> 5 = 100000(b) = 32 1.2.7.4 - Operatory warunkowe(logiczne) ....................................... 1. W1 || W2 Zwraca prawde jesli W1 lub W2 zwraca prawde. Nie obliczy W2 jesli W1 zwraca prawde. 2. W1 && W2 Zwraca prawde, jesli zarowno W1 jak i W2 zwraca prawde. Nie wykona W2 jesli W1 zwraca falsz. 3. !E Zwraca prawde, jesli W jest falszywe i vice versa. 1.2.7.5 - Operatory porownawcze ............................... 1. W1 == W2 Zwraca prawde, jesli W1 jest rowne W2. Moze byc uzyty na wszystkich typach. Obejrzyj specjalny rozdzial o tablicach i mappingach, gdyz ten operator dziala na nich inaczej, niz by ci sie moglo wydawac. 2. W1 != W2 Zwraca prawde jesli W1 jest rozne od W2. Moze byc uzyty na wszystkich typach. Obejrzyj specjalny rozdzial o tablicach i mappingach, gdyz ten operator dziala na nich inaczej, niz by ci sie moglo wydawac. 3. W1 > W2 Zwraca prawde, jesli W1 jest wieksze od W2. Moze byc uzyte na wszystkich typach poza tablicami i mappingami. 4. W1 < W2 Zwraca prawde, jesli W1 jest mniejsze od W2. Moze byc uzyte na wszystkich typach z wyjatkiem tablic i mappingow. 5. W1 >= W2 Zwraca prawde, jesli W1 jest wieksze lub rowne W2. Moze byc uzyte na wszystkich typach poza tablicami i mappingami. 6. W1 <= W2 Zwraca prawde, jesli W1 jest mniejsze lub rowne W2. Moze byc uzyte na wszystkich typach z wyjatkiem tablic i mappingow. 1.2.8 - Podwojne operatory przypisania -------------------------------------- Wszystkie arytmetyczne i booleanowskie operatory mozna zapisac w prostszy sposob, o ile chcesz jedynie obliczyc dzialanie jednej zmiennej z jakims wyrazeniem, a wynik przechowac w tej zmiennej. Powiedzmy, ze chcesz wykonac `a = a + 5;'. Lepiej mozna to zapisac tak: `a += 5;'. Wartosc drugiego wyrazenia jest dodawana do pierwszego i w nim przechowana (czyli w tym przypadku w zmiennej a). Wszystkie inne operatory mozna zapisac w ten sam sposob, tj. napierw zmienna, potem `=' poprzedzone operatorem, a na koncu wyrazenie. a >>= 5; /* a = a >> 5; */ b %= d + 4; /* b = b % (d + 4); */ c ^= 44 & q; /* c = c ^ (44 & q); */ 1.2.9 - Priorytet operatorow i kolejnosc wykonywania dzialan ------------------------------------------------------------ W ponizszej tabeli zawarte sa reguly dotyczace kolejnosci i laczenia wszystkich operatorow, wlaczajac w to te, o ktorych jeszcze nie bylo mowy. Operatory bedace w tej samej linii w tabeli maja taki sam priorytet, a operatory w kolejnych rzedach maja malejaca waznosc. Na przyklad `*', `/' i `%' maja taki sam priorytet i jest on wyzszy od tego, ktory maja `+' i `-'. Zauwaz, ze priorytet logicznych operatorow bitowych `&', `^' i `|' jest nizszy od priorytetu `==' i `!='. Powoduje to, ze wyrazenia testujace bity, takie jak if ((x & MASKA) == 0) ... powinny byc cale ujete w nawiasy, zeby daly poprawne wyniki. 1. () [] Od lewej do prawej 2. ! ~ ++ -- - (typ) * & Od prawej to lewej 3. * / % Od lewej do prawej 4. + - Od lewej do prawej 5. << >> L->P 6. < <= > >= L->P 7. == != L->P 8. & L->P 9. ^ L->P 10. | L->P 11. && L->P 12. || L->P 13. ?: L->P 14. = += == etc. P->L 15. , L->P 1.2.10 - Instrukcje warunkowe ----------------------------- W LPC czesto korzysta sie z instrukcji warunkowych. Jest wiele metod ich zapisywania. Bardzo wazna rzecza jest to, ze w testach zero `0' jest traktowane jako "falsz", zas dowolna inna wartosc jako "prawda". Oznacza to, iz puste tablice `({})', puste stringi `""' oraz puste mappingi `([]) rowniez sa "prawda", poniewaz nie sa `0'. Jesli chcesz poznac ich rozmiar, albo zbadac ich zawartosc, to musisz sie posluzyc specjalnymi funkcjami - o nich bedzie mowa pozniej. 1.2.10.1 - Instrukcje if/else ............................. `if' stanowi najczesciej spotykana instrukcje warunkowa. Jest latwa w uzyciu i moze byc laczona z instrukcja `else' w razie potrzeby obsluzenia obu wynikow testu (tj. falszu i prawdy). Stosuje sie ja w taki sposob: if (wyrazenie) instrukcja; np. if (a == 5) a -= 4; Jesli chcesz obsluzyc falszywy wynik testu (jest on wtedy, gdy wyrazenie w nawiasie zwrocilo falsz), to musisz dodac intrukcje `else': if (wyrazenie) intrukcja_prawda else instrukcja_falsz; np. if (a == 5) a -= 4; else a += 18; Jesli zmienna `a' bedzie sie rownala 5, to od niej zostanie odjete 4, w przeciwnym wypadku, do zmiennej `a' zostanie dodane 18. Pamietaj, ze mozesz w miejsce instrukcji wstawic rowniez jakis blok. 1.2.10.2 - Instrukja switch ........................... Jeslibys chcial przetestowac jedna zmienna na wiele roznych wartosci, to skonczyloby sie to na dosyc dlugiej liscie instrukcji `if-else-if-else'. Jednakze, jesli typ wartosci na ktore testujesz zmienna jest integerem, floatem lub stringiem to mozesz uzyc o wiele lepszej i bardziej skondensowanej metody testowania. Zalozmy, ze chcesz napisac nastepujacy kod: if (imie == "lewy") // jesli imie == "lewy" to... { miasto = "lu"; opis = "wesoly"; } else if (imie == "alvin") // w przeciwnym wypadku, jesli imie == "alvin" { miasto = "wa"; opis = "bursztynooki"; } else if (imie == "athmagor") // jesli nie "alvin" i nie "lewy", to moze "athmagor" { miasto = "lu"; opis = "piekny"; } else { miasto = "x"; opis = "nieznany"; } Lepszym sposobem na zapisanie tego jest: switch (imie) { case "lewy": miasto = "lu"; opis = "wesoly"; break; case "alvin": miasto = "wa"; opis = "bursztynooki"; break; case "athmagor": miasto = "lu"; opis = "piekny"; break; default: miasto = "x"; opis = "nieznany"; } Ta instrukcja dziala bardzo prosto: `switch' bierze wyrazenie spomiedzy nawiasow i porownuje je z kazdym z wyrazen wystepujacych po `case'. *UWAGA* Wyrazanie po `case' *musi* byc wartoscia stala. Nie moze to byc zmienna, wywolanie funkcji, ani zadne inne wyrazenie tego typu. Po tym, jak kompilator stwierdzi, ze wartosc w nawiasie rowna sie wartosci po `case', wszystkie instrukcje wystepujace po `case' zostana wykonane, az do momentu kiedy natrafi na instrukcje `break'. Jesli nie znajdzie zadnych rownajacych sie wartosci, wykonywane zostana instrukcje po `default'. *UWAGA!* Nie ma przymusu pisania czesci `default'. Jest to jednak bardzo polecane, gdyz wykonanie jej zazwyczaj oznacza, ze stalo sie cos, co nie bylo przewidziane w czasie pisania programu. Jesli uwazasz, ze testowana zmienna bedzie przyjmowala jakis ograniczony zestaw wartosci, to warto miec w zapleczu jakis komunikat bledu, ktory zostanie wyswietlony uzytkownikowi, gdy stanie sie cos nieoczekiwanego. Gdy zapomnisz umiescic komendy `break', kolejne wyrazenia `case' beda wykonywane. Moze to nie brzmiec tak, jakbys chcial, ale na przyklad jesli powyzsze imiona `alvin' i `lewy' mialyby wygenerowac wspolne czynnosci, to moglbys napisac: case "lewy": /* nie ma break */ case "alvin": < kod > break; ... i oszczedzic troche miejsa. Uzywanie switch'a nie czyni kodu szybszym, jedynie zwieksza czytelnosc i zmniejsza ryzyko popelnienia jakiegos bledu w czasie pisania. Pamietaj umiescic jakis komentarz, w miejscu gdzie powinien byc break, gdyz potem mozesz zapomniec o tym, iz specjalnie go nie umiesciles. Dobrym pomyslem jest wstawianie jednej pustej linii po kazdym break'u, zeby dac 'chwile oddechu', co zwiekszy czytelnosc. 1.2.10.3 - Wyrazenie ?: ....................... Jest to bardzo skondensowana forma pisania instrukcji `if/else' i zwracania roznych wartosci w zaleznosci od tego, jak wypadl test. Oczywiscie nie jest to instrukcja, lecz wyrazenie, poniewaz zwraca jakas wartosc. Nie mowilem o tym jak omawialem wyrazenia, bo bylo by mi ciezko to wytlumaczyc, przed wyjasnianiem instrukcji `if/else'. Zalozmy, ze chcesz napisac cos takiego: if (testowane_wyrazenie) zmienna = wyrazenie_if; else zmienna = wyrazenie_else; Mozesz to napisac w znacznie bardziej skondensowany sposob: zmienna = testowane_wyrazenie ? wyrazenie_if : wyrazenie_else; np. nazwa = dzien == 2 ? "wtorek" : "inny dzien"; Jest kwestia sporna, czy ten sposob zapisywania czyni kod bardziej, czy mniej czytelnym. Ale mysle, ze jedno wyrazenie tego typu ulatwia czytanie kodu, zas kombinacja kilku pogarsza tylko czytelnosc. Cos takiego, jak w nastepujacym przykladzie *z pewnoscia* nie jest ulepszeniem: nazwa = dzien == 2 ? godzina == 18 ? "czas na kwas" : "wtorek" : "inny dzien"; 1.2.11 - Petle -------------- Petla to rodzaj instrukcji, ktora wykonuje okreslone czynnosci dopoki nie zostana spelnione pewne wczesniej zaprogramowane warunki. Sa dwa rodzaje petli i oba uzywaja instrukcji warunkowych. 1.2.11.1 - Instrukcja while ........................... Instrukcja "while" jest bardzo prosta w uzyciu. Juz z samej nazwy ("while" oznacza "dopoki") mozna latwo wywnioskowac co ona robi. Bedzie ona caly czas wykonywala inna instrukcje, dopoki wyrazenie kontrolne nie zwroci falszu. Ma ona nastepujaca skladnie: while () instrukcja_wykonywana; Pamietaj, ze wyrazenie kontrolne jest sprawdzane na samym poczatku, jeszcze przed wywolaniem instrukcji po raz pierwszy. Jesli juz na samym poczatku wyrazenie kontrolne zwroci falsz, to instrukcja nigdy nie zostanie wykonana. a = 0; while (a != 4) { a += 5; a /= 2; } Dopoki a ma wartosc rozna od 4, jest do niego dodawane 5, a potem jest dzielone przez 2. (Wiem, wiem, ten przyklad nie ma najmniejszego sensu :)). 1.2.11.2 - Instrukcja for ......................... Jesli potrzebujesz zwyklego licznika, to powinienes uzyc instrukcji `for'. Ma ona nastepujaca skladnie: for (instrukcja_inicjalizujaca; wyrazenie_kontrolne; intrukcja_konca_cyklu) instrukcja_powtarzana; Cala instrukcje mozna zapisac w inny rownowazny sposob, za pomoca instrukcji "while": instrukcja_inicjalizujaca; while (wyrazenie_kontrolne) { instrukcja_powtarzana; intrukcja_konca_cyklu; } Na samym poczatku, instrukcja `for' wykonuje intrukcje_inicjalizujaca. Stosuje sie ja do poczatkowego ustawienia licznikow, lub innych wartosci uzywanych w czasie wykonywania petli. Od tego momentu trwa jej wlasciwa czesc. Kazdy cykl zaczyna sie od obliczenia "wyrazenia_kontrolnego" i sprawdzenia jego wyniku. Jesli jest prawdziwy, czyli wyrazenie zwroci cokolwiek roznego od 0, to zostanie wykonana "instrukcja_powtarzana", a od razu po niej "instrukcja_konca_cyklu". W "instrukcji_powtarzanej" zawarte jest zazwyczaj to, co sie chce by petla cyklicznie wykonywala, a w "instrukcji_konca_cyklu" zazwyczaj zwieksza lub zmniejsza sie liczniki. W poprzednim ustepie wiele razy uzylem slowa *zazwyczaj*. Zrobilem to dlatego, ze *nie musisz* robic tego w ten sposob; nie ma niczego co by cie przymuszalo do takiego wykorzystania tej instrukcji, jaki podalem. Zalozmy, ze chcesz obliczyc sume wszystkich liczb calkowitych (integerow) z przedzialu od 7 do 123 i nie znasz na to wzoru ((x1^2 + x2^2) / 2). Najlatwiejsza (co nie oznacza najefektywniejsza) metoda, jest uzycie petli. wynik = 0; for (licznik = 7 ; licznik < 124 ; licznik++) wynik += licznik; Na poczatku wynik jest zerowany. Po tym nastepuje wlasciwa czesc instrukcji `for'. Zaczyna sie od ustawienia zmiennej licznik na 7. Teraz nastepuje wejscie do petli i test, czy licznik (rowny 7) jest mniejszy niz 124. Jest tak, wiec wartosc licznika jest dodawana do zmiennej wynik. Zmienna licznik jest zwiekszana o jeden i nastepuje ponowne wejscie do petli. Dzieje sie tak, do momentu az wartosc licznika osiagnie 124. Poniewaz nie jest on wtedy mniejszy od 124, wykonywanie petli zostaje zakonczone. *UWAGA!* Wartosc licznika po wykonaniu instrukcji `for' jest rowna 124, a *nie* 123, jak niektorzy ludzie sadza. "Wyrazenie_kontrolne" musi zwrocic falsz, zeby petla mogla sie skonczyc i co za tym idzie licznik musi miec wartosc 124. 1.2.11.3 - Instrukcje break i continue -------------------------------------- Czasem, podczas wykonywania instrukcji `switch', `for' lub `while' zachodzi potrzeba wyjscia z petli i kontynuacji poza nia. W tym celu uzywa sie instrukcji `break'. while (warunek_koncowy < 9999) { // Jesli funkcja time() zwroci 29449494, to przerwij wykonywanie // petli if (time() == 29449494) break; < kod > } // Kompilator wykonuje ten kod zarowno po uzyciu instrukcji `break', // jak i gdy petla sie skonczy. < kod > Nieraz chcesz, by petla wykonala sie dodatkowo podczas uzycia instrukcji `for' lub `while'. Do tego celu uzywa sie instrukcji `continue'. // Dodaje wszystkie parzyste liczby suma = 0; for (i = 0 ; i < 10000 ; i++) { // Zacznij petle od poczatku, gdy `i' zwraca nieparzysta liczbe if (i % 2) continue; suma += i; } Kazde uzycie continue w tym przykladzie powoduje przeskoczenie jednego cyklu, gdyz licznik zostaje zwiekszony, ale zasadnicza czesc kodu pozostaje nie wykonana, bo instrukcja `continue' znajduje sie przed nia. 1.2.12 - Tablice i Mappingi --------------------------- Nadszedl czas na zaglebienie sie w dwa specjalne typy: "tablice" i "mappingi". Ich zastosowanie moze wygladac podobnie, lecz w rzeczywistosci bardzo sie od siebie roznia. Dla obu typow napisano wiele uzytecznych efunkcji, ktore operuja i pobieraja z nich informacje. Jednakze zostana opisane one pozniej, a na razie przedstawie tylko kilka z nich. 1.2.12.1 - Jak uzywac i deklarowac tablice .......................................... Tablice w LPC to tak na prawde nie sa prawdziwe tablice. Lepiej okresla je to, ze sa to listy z okreslonym porzadkiem. Roznica moze sie wydawac niewielka, lecz dla informatykow jest ogromna. Tablice maja okreslony typ, co oznacza, ze moga przyjmowac tylko takie elementy, u ktorych jest on zgodny z tablica. Innym ograniczeniem jest to, ze tablice moga byc tylko jednowymiarowe. Istnieje jednak mozliwosc obejscia tego, gdyz tablice z elementami typu mixed moga przyjmowac wartosci dowolnego typu, czyli nawet inne tablice. Powinienes jednak pamietac o ostroznym poslugiwaniu sie typami tablic, zeby nie popelnic jakiegos bledu. Tablice definiuje sie w ten sposob: *; np. int *moja_tab, *twoja_tab; float *inna_tab; object *tab_ob; Poczatkowa wartoscia tych tablic jest `0', a *nie* pusta tablica. Powtarzam: sa one ustawiane na zero - `0', *nie* na pusta tablice - `({ })'. Pamietaj o tym! Tablice alokuje i inicjalizuje sie w ten sposob: = ({ elem1, elem2, elem3, ..., elemN }); np. moja_tab = ({ 1, 383, 5, 391, -4, 6 }); Dostep do elementow tablicy mozna uzyskac dzieki podaniu numeru indeksu w nawiasach, po nazwie zmiennej zawierajacej tablice. (Zalozmy, ze zmienna wart jest zadeklarowana jako integer). = []; np. wart = moja_tab[3]; LPC, tak samo jak C, zaczyna liczenie od 0 i przez to czwarty element ma numer 3. Zeby ustawic wartosc juz istniejacego elementu na nowa, uzywa sie operatora `='. moja_tab[3] = 22; // => ({ 1, 383, 5, 22, -4, 6 }) moja_tab[3] = 391; // => ({ 1, 383, 5, 391, -4, 6 }) W celu pobrania wycinka tablicy, wystarczy w nawiasach podac od ktorego, do ktorego elementu chce sie wyciac. = [..]; np. twoja_tab = moja_tab[1..3]; ... spowoduje, ze nowa tablica `twoja_tab' bedzie miala wartosc `({ 383, 5, 391 });'. Jesli starej tablicy przypisesz nowa wartosc, poprzednia zawartosc zostanie stracona. np. moja_tab = ({ }); ... spowoduje, ze `moja_tab' stanie sie pusta tablica. Stara zostanie zdealokowana, a pamiec przez nia zajeta uwolniona. Jesli podasz indeks, ktory wyjdzie poza rozmiar tablicy, to wyskoczy blad i wykonywanie obiektu zostanie przerwane. Jednakze, gdy indeks przy podawaniu zasiegu, np. indeks "dokad" wyjdzie poza tablice, to blad nie zostanie zakomuniukowany, zasieg zas zostanie tak ograniczony, by sie zmiescil w rozmiarze tablicy. Jesli chcesz stworzyc pusta tablice, z wartosciami ustawionymi na 0 (typ nie gra roli) o okreslonej dlugosci, to mozesz uzyc efunkcji `allocate()'. = allocate(); np. twoja_tab = allocate(3); // => twoja_tab = ({ 0, 0, 0 }); Dodawanie tablic do siebie daje sie latwo dokonac za pomoca operatora `+'. Wystarczy, ze zsumujesz je tak, jak sie sumuje liczby. Operator `+=' rowniez do tego celu swietnie pasuje. moja_tab = ({ 9, 3 }) + ({ 5, 10, 3 }); // => ({ 9, 3, 5, 10, 3 }) Uzycie operatorow `-/-=' to najlatwiejszy sposob na usuwanie elementow z tablicy. Uwazaj jednakze, gdyz operator ten usunie *wszystkie* elementy, ktore beda mialy wartosc taka sama jak elementy, ktore chcesz usunac. moja_tab -= ({ 3, 10 }); // => ({ 9, 5 }) I jeszcze maly przyklad na wycinanie i doklejanie elementow tablic. moja_tab = ({ 9, 3, 5, 10, 3 }); moja_tab = moja_tab[2..4] + moja_tab[0..0]; // => ({ 5, 10, 3, 9 }) moja_tab[0..0] // = ({ 9 }) moja_tab[0] // = 9 *UWAGA!* Zwroc uwage na roznice! W piewszej linii jest tablica, a w drugiej integer! 1.2.12.2 - Jak deklarowac i uzywac Mappingow ............................................. Mapping to lista powiazanych par wartosci. Maja one wszystkie typ `mixed'. Oznacza to, ze czesc indeksowa pary nie musi miec przez caly czas tego samego typu. Jest to jednak bardzo doradzane, z powodu bledow jakie nieostrozne uzywanie typu `mixed' moze spowodowac. Zarowno indeks, jak i wartosc moze przyjmowac dowolny typ. Istnieje tylko jedno ograniczenie tyczace sie indeksow - *nie* moze byc dwoch indeksow o takiej samej wartosci w jednym mappingu. Moze to troche pogmatwanie brzmiec z poczatku, ale tak na prawde to to jest bardzo banalne. Chyba latwiej zrozumiec bedzie jednak, jesli od razu przystapie do ilustrowania tego przykladami. Mappingi deklaruje sie tak jak wszystkie inne zmienne. Zacznijmy wiec od kilku prostych deklaracji. mapping moj_map; int wartosc; Alokowac i inicjalizowac mozna na trzy rozne sposoby. 1: = ([ :, :, ... ]); 2: [] = wartosc; 3: = mkmapping(, ); Pierwszy sposob jest prosty i naturalny. 1: my_map = ([ "adam":5, "brunon":8, "czeslaw":-4 ]); Dzialanie drugiego sposobu polega na tym, ze gdy nie istnieje w mappingu para o podanym indeksie - to jest tworzona, jesli istnieje - to wartosc pary jest zastepowana nowa. 2: my_map["adam"] = 1; // Tworzy pare "adam":1 my_map["brunon"] = 8; // Tworzy pare "brunon":8 my_map["adam"] = 5; // Zamienia stara wartosc w "adam" na 5. ... Trzeci sposob wymaga dwoch tablic - jednej zawierajacej indeksy, a drugiej przechowujacej wartosci. Tworzenie tablic zostalo opisane w poprzednim rozdziale. 3: moj_map = mkmapping( ({ "adam", "brunon", "czeslaw" }), ({ 5, 8, -4 }) ); W przeciwienstwie do tablic, mappingi nie maja ustalonego porzadku. Wartosci sa poukladane w taki sposob, by maksymalnie skrocic czas ich wyszukania. Istnieja funkcje, ktore umozliwiaja pobranie listy elementow (indeksow, lub wartosci) z mappinga. Pamietaj tylko, ze moga one byc w *dowolnej* kolejnosci i nigdy nie ma pewnosci, ze beda tak samo ulozone pomiedzy dwoma wywolaniami funkcji "pobierajacych". W praktyce jednak, kolejnosc jest zmieniana tylko w czasie dodawania lub usuwania elementow. Mappingi laczy sie za pomoca operatorow `+/+=', zupelnie tak samo jak w przypadku tablic. moj_map += ([ "dobroslawa":5, "edmund":33 ]); Usuwanie elementow z mappingu juz nie jest takie proste. Sluzy do tego specjalny efun `m_delete()' (rowniez opisany dalej). moj_map = m_delete(moj_map, "brunon"); moj_map = m_delete(moj_map, "dobroslawa"); Jak widzisz, elementy usuwa sie po kolei, podajac indeks jako identyfikator pary. Inna rzecza, ktora sobie musisz szybko uswiadomic jest to, ze indeksy musza byc unikalne, nie moze byc dwoch takich samych "uchwytow" do par. Jednakze wartosci oczywiscie moga byc takie same. Poszczegolne wartosci mozna pobrac poprzez zwykle podanie indeksu. wartosc = moj_map["czeslaw"]; // => -4 Proba pobrania elementu o nieistniejacym indeksie *nie* wywola zadnego bledu, a jedynie zwroci 0. Musisz wiec byc *bardzo* ostrozny, gdyz 0 moze oznaczac rozne rzeczy, tzn. wartosc 0 moze oznaczac, ze element o podanym indeksie nie ma czesci z wartoscia, ale moze oznaczac tez, ze wartoscia *jest* 0. wartosc = moj_map["rgojhijaeh"]; // => 0 1.3 - Preprocesor ================= Preprocesor *nie* jest czescia jezyka LPC. Jest to specjalny proces, ktory jest uruchamiany przed rozpoczeciem kompilacji. Mozna go postrzegac jako bardzo sprytnego "podstawiacza" stringow; Okreslone lancuchy znakowe sa zastepowane innymi. Pierwszym znakiem kazdej dyrektywy preprocesora jest `#'. Poza tym musza sie one zaczynac w pierwszej kolumnie (na samym poczatku linii). Mozesz je umiescic gdziekolwiek w kodzie, acz jak sie dowiesz pozniej, wiekszosc z nich jest przypisana poczatkowi programu. 1.3.1 - Instrukcja #include --------------------------- Jest to najczesciej uzywana dyrektywa preprocesora. Mowi ona mu, aby zastapil linie w ktorej ona sie znajduje zawartoscia *calego* pliku, o nazwie podanej po instrukcji. Dane, ktore umieszczasz we wlaczanyum pliku to takie, ktorych raczej nie bedziesz nigdy zmienial i takie, ktore bedziesz wlaczal w wiele plikow. Zamiast wpisywania tego samego tekstu w rozne pliki w nieskonczonosc i co za tym idzie zwiekszania szansy na jakies kretynskie bledy, po prostu zbierasz powtarzajace sie dane w jeden lub kilka plikow i wlaczasz(include) je w te programy, w ktore trzeba. Skladnia jest bardzo prosta: #include #include "plik_dodatkowy" *UWAGA!* Zauwaz, ze nie ma `;' na koncu linii! Dwa rozne sposoby zapisywania nazw plikow zaleza od tego, gdzie sie one znajduja. Jest spora liczba standardowych bibliotek w grze, rozrzuconych po sporej liczbie katalogow. Zamiast zapamietywania dokladnie gdzie one sa, wystarczy ze podasz sama nazwe pliku, ktory chcesz wlaczyc. #include #include Gdy chcesz wlaczyc jakies pliki wlasne, ktore nie sa zadna standardowa biblioteka, to musisz dokladnie podac ich lokacje. Mozesz to zrobic zarowno poprzez podanie sciezki od katalogu, gdzie sie znajduje program, albo poprzez podanie pelnej, absolutnej sciezki od glownego katalogu. #include "/d/Standard/login/login.h" #include "moje_def.h" #include "/sys/adverbs.h" // Ten sam, co krotszy przyklad powyzej Gdy chcesz wlaczyc standardowe biblioteki, to zawsze uzywaj notacji < >. (czyli podawaj sama nazwe bilbioteki, ujeta w nawiasy < > ). Powodem nie jest tylko to, ze tak jest krocej, ale to, ze gdy biblioteki zostana przemieszczone gdzie indziej, twoj program przestanie dzialac. Gdy uzyjesz notacji <> to zawsze zostana znalezione. Wlaczane pliki moga miec dowolna nazwe, ale ustalono, ze beda mialy koncowke '.h', zeby moc je jasno odroznic od innych plikow. Jest nawet mozliwe wlaczenie plikow `c', tzn. calych plikow zawierajacych programy. Jednakze, jest to bardzo zla rzecza. *Nie* rob tego *NIGDY*! Czemu? Po pierwsze programy sledzace bledy gubia numeracje linii we wlaczanych plikach i przez to podaja zle numery linii. Po drugie, gdy wlaczasz nieskompilowany kod w wiele roznych obiektow, marnujesz pamiec oraz CPU, gdyz ten sam plik musi byc wielokrotnie kompilowany i przechowywany osobno, dla kazdego obiektu, ktory go uzywa. A poza tym samo czytanie takiego programu moze byc istna tortura. Co ma wiec tak na prawde rozszerzenie nazwy pliku do jego zawartosci? Tak na prawde to nic nie ma... Jednakze jest przyjete, ze kod i funkcje sa przechowywane w plikach z rozszerzeniem `.c', a definicje z rozszerzeniem `.h'. Mudlib zazwyczaj korzysta z tego rozdzialu i moze nie rozpoznawac jako kodu zrodlowego niczego, poza plikami z koncowka `c'. 1.3.2 - Instrukcja #define -------------------------- Jest to bardzo potezne "makro", komenda preprocesora, ktora jest ciagle naduzywana. Madrze postapisz, jesli bedziesz uzywal jej ostroznie i tylko do prostych rzeczy. Ma ona nastepujaca skladnie: #define #undef Dowolny taki sam tekst w pliku jak `' zostanie zastapiony `', jeszcze przed kompilacja. `#define' jest wazne od linii, w ktorej zostalo zdefiniowane do konca pliku, albo do momentu wykonania komendy `#undef', ktora usuwa makro. Pomimo tego, ze makrem moze byc dowolny tekst, jest w zwyczaju(rob tak!), ze nazwe makra pisze sie wylacznie duzymi literami. Jest to po to, zeby mozna bylo wyroznic makra w tekscie, w ktorym kazdy(ty tez!) pisze nazwy funkcji i zmiennych malymi literami. Umieszczaj wszystkie definicje na poczatku pliku, albo biedny koles, ktory bedzie ci pomagal w wylapaniu bledow, bedzie mial prawdziwe piekielko z poszukiwaniem pochowanych definicji. Jesli to bedzie ktos, kogo sam poprosiles o pomoc (gdyz masz bledy powstale z powodu brzydko napisanego kodu), to najprawdopodobniej powie ci zebys sobie wsadzil taki program w wiadome miejsce i wrocil dopiero wtedy, jak sie nauczysz poprawnie i ladnie pisac. Prostymi definicjami sa na przyklad sciezki, nazwy i wszystkie inne stale dowolnego rodzaju, ktorych nie chcesz w kolko zapisywac, albo chcesz miec mozliwosc latwej ich modyfikacji, bez zmieniania tego samego w dziesiatkach miejsc. #define MAX_LOGIN 100 /* Maksymalna liczba zalogowanych graczy */ #define LOGIN_OB "/std/login" /* Obiekt logowania */ #define POWIT_TEKST "Witamy!" /* Komunikat logowania */ Gdziekolwiek wystapi identyfikator makra, zostanie on zastapiony tym, co sie znajduje w definicji pomiedzy identyfikatorem, a koncem linii. Podchodza pod to takze komentarze, ktore jednak i tak zostana pozniej wyrzucone. tell_object(gracz, POWIT_TEKST + "\n"); Komentarz typu `//' nie jest w takiej sutuacji dobra rzecza, gdyz konczy sie on dopiero na koncu linii. #define POWIT_TEKST "Witamy!" // Komunikat logowania ...bedzie zamienione w poprzednim przykladzie na: tell_object(gracz, "Witamy!" // Komunikat logowania + "\n"); ...co spowoduje ujecie w komentarz wszystkiego, co wystapi po `//', az do konca linii. Gdy makro wychodzi poza linie, mozesz przedluzyc ja za pomoca znaku `\' ktory zaznacza, ze definicja jest kontynuowana w nastepnej linii. Jednakze musisz zakonczyc linie tuz po `\' i *NIE* nie moze byc za nim zadnych spacji, ani innych znakow. #define DLUGA_DEFINICJA "poczatek stringa \ i jego koniec." Definicje nasladujace funkcje sa czesto stosowane i nieraz naduzywane. Jedyna na prawde wazna zasada, obowiazujaca przy pisaniu takich makr jest to, ze kazdy argument *musi* byc ujety w nawiasy. Jesli napiszesz inaczej, to mozesz otrzymac bardzo dziwne wyniki. 1: #define POMNOZ_TO(a, b) a * b /* Zle */ 2: #define POMNOZ_TO(a, b) (a * b) /* Nie wystarczajaco */ 3: #define POMNOZ_TO(a, b) ((a) * (b)) /* Wlasciwie */ Co za roznica pewnie zapytasz? Spojrz w takim razie na ten przyklad: wynik = POMNOZ_TO(2 + 3, 4 * 5) / 5; Po podstawieniu wyglada to tak: 1: wynik = 2 + 3 * 4 * 5 / 5; // = 14, Zly 2: wynik = (2 + 3 * 4 * 5) / 5 // = 12, Tez zly 3: wynik = ((2 + 3) * (4 * 5)) / 5 // = 20, Wlasciwy! Naduzycia definicji polegaja zazwyczaj na zlym ich ulozeniu i uzyciu skomplikowanych makr wewnatrz innych makr (co czyni kod prawie niemozliwym do zrozumienia). Podstawowa zasada jest pisanie krotkich i prostych makr. Rob tak, albo marny bedzie twoj koniec ;) 1.3.3 - Instrukcje #if, #ifdef, #ifndef, #else i #elseif -------------------------------------------------------- Sa to dyrektywy preprocesora, ktore sluza selekcjonowaniu okreslonych czesci kodu i usuwaniu innych w zaleznosci od stanu zmiennych preprocesora. Dzieki nim czesc kodu moze zostac uniewidoczniona dla kompilatora - cos jak inteligentne komentarze. Instrukcja `#if' jest bardzo podobna do zwyklej instrukcji if. Jest tylko troche inaczej zapisywana. Zalozmy, ze *mozesz* miec gdzies nastepujaca definicje: #define CODE_VAR 2 lub #define CODE_VAR 3 Wtedy mozesz napisac: #if CODE_VAR == 2 #else #endif Mozesz wogole nie pisac instrukcji `#else' jesli tego nie chcesz. Wystarczy napisac nastepujaca instrukcje, zeby zaistniala dana definicja preprocesora: #define CODE_VAR /* Definiuje istnienie CODE_VAR */ I teraz mozesz napisac: #ifdef CODE_VAR #else #endif lub #ifndef CODE_VAR #else #endif I ponownie, instrukcje `#else' mozna sie ominac. Komendy preprocesora `#if/#ifdef/#ifndef' sa prawie wylacznie uzywane do dodania odpluskwiajacego kodu, ktory nie powinien byc caly czas aktywny lub do pisania rzeczy, ktore beda roznie pracowaly w zaleznosci od bardzo rzadko zmieniajacych sie parametrow. Cala konfiguracja mudliba jest sprawdzana w ten sposob. Na przyklad jest definicja MET_ACTIVE, ktora sluzy ustalaniu czy system met/nonmet (nie kazdy zna kazdego gracza) ma byc wlaczony czy nie. Gdy administrator muda zdecyduje sie na wylaczenie tego, wystarczy ze w jednym pliku konfiguracyjnym zamieni '#define MET_ACTIVE' na '#undef MET_ACTIVE'. (W Arkadii wiele takich definicji znajdziesz w '/config/sys/local.h'). 2 - Podstawy Mudliba i LPC ************************** W rozdziale tym nauczysz sie wszystkiego, czego potrzebujesz by pisac kod w srodowisku gry. Bede unikal bardziej skomplikowanych tematow, pozostawiajac ich omowienie na trzeci rozdzial. Dowiesz sie tu wielu rzeczy na temat mudliba i pracy gamedrivera - wiedzy niezbednej do pisania dzialajacego i efektywnego kodu. 2.1 - Male wybiegniecie w przyszlosc ==================================== Aby moc pokazac ci przyklady tego, czego bede cie probowal nauczyc, musze ci wpierw z wyprzedzeniem wyjasnic kilka funkcji. Bedzie to powtorzone w odpowiednim kontekscie pozniej. Tutaj jest zamieszczony jedynie krotki przeglad, zebys wiedzial co robie. Zeby pokazac pokazac jakis tekst na ekranie gracza, uzywa sie efunkcji `write()'. Istnieja dwa specjalne znaki, ktore sa dosc czesto uzywane do formatowania tekstu. Sa to: znak "tabulacji" i znak "nowej linii". Zapisuje je sie w ten sposob: (kolejno) `\t' i `\n'. Znak "tabulacji" wstawia osiem spacji, a znak "nowej linii", jak sama nazwa wskazuje lamie linie, czyli konczy stara i przechodzi do nowej. void write(string tekst) np. write("Siemanko!\n"); write("\tTo jest wciety string.\n"); write("Ten string\njest rozlozony na kilka linii\n\ti czesciowo" + "\nwciety.\n"); /* Wynikiem bedzie: Siemanko! To jest wciety string. Ten string jest rozlozony na kilka linii i czesciowo wciety. */ Jesli masz tablice, mapping albo zwykla zmienna dowolnego typu i chcesz wyswietlic to na ekranie, np. zeby pomoc sobie w wyszukiwaniu bledow, to sfunkcja `dump_array()' moze sie okazac bardzo pomocna. Podajesz zmienna, ktora chcesz wyswietlic jako argument i jej zawartosc zostanie ukazana na ekranie. void dump_array(mixed dane) np. string *imie = ({ "franek", "zdzichu", "bolo" }); dump_array(imie); /* Wynikiem bedzie (Array) [0] = (string) "franek" [1] = (string) "zdzichu" [2] = (string) "bolo" */ 2.2 - Drugi rzut oka na LPC =========================== Wytlumacze ci teraz pominiete w poprzednim rozdziale informacje o LPC. Ta wiedza jest ci potrzebna do tworzenia dzialajacych obiektow w srodowisku gry. 2.2.1 - Wywolania funkcji ------------------------- Sa dwa rodzaje wywolan funkcji: wewnetrzne i zewnetrzne. Jedynym rodzajem o jakim dotad mowilismy byly wywolania wewnetrzne, choc zewnetrzne tez sie znalazly w kilku miejscach. 2.2.1.1 - Tworzenie wewnatrz-obiektowych wywolan funkcji ........................................................ [call_self] Robienie wewnetrznych wywolan funkcji jest tak proste, jak pisanie nazwy funkcji i argumentow w nawiasach po niej. Lista argumentow jest albo lista wyrazen, albo wogole jej nie ma. Wywolanie funkcji jest oczywiscie wyrazeniem rowniez. (); np. zmienna = jakas_funkcja(1.0) * 4; Jest inny sposob na zrobienie tego. Jesli nazwe funkcji masz przechowana w jakiejs zmiennej i chcesz ja wywolac, to pomocna moze sie okazac funkcja `call_self()': call_self(<"nazwa funkcji">, ); np. zmienna = call_self("jakas_funkcja", 1.0) * 4; Jesli uzywajac `call_self()' podasz nazwe nieistniejacej funkcji, wyskoczy blad i wykonywanie obiektu zostanie przerwane. 2.2.1.2 - Tworzenie prostych zewnatrz-obiektowych wywolan funkcji ................................................................. [call_other] Wywolanie zewnetrzne, to wywolanie funkcji z innego obiektu. Zeby moc to zrobic, potrzebujesz odnosnika do obiektu w ktorym chcesz ja wywolywac. Nie mowilismy jeszcze o tym, jak zdobyc odnosnik do obiektu, ale zalozmy ze juz go masz. mixed ->(); mixed call_other(, "", ); np. /* * Zalozmy, ze chce wywolac funckje 'oblicz_cos' w obiekcie * "/d/Mojadomena/wiz/obiekt" i ze mam jakis wlasciwy wskaznik * obiektu przechowany w zmiennej 'obiekcik' */ zmienna = obiekcik->oblicz_cos(1.0); zmienna = "/d/Mojadomena/wiz/obiekt"->oblicz_cos(1.0); zmienna = call_other(obiekcik, "oblicz_cos", 1.0); zmienna = call_other("/d/Mojadomena/wiz/obiekt", "oblicz_cos", 1.0); /* * Te cztery linijki robia dokladnie to samo */ Jak widzisz, efunkcja `call_other()' dziala analogicznie do `call_self()'. Kiedy zewnetrznie wywolujesz funkcje poslugujac sie sciezka do obiektu, tak zwany "master object" zostaja wywolany. Jesli obiekt, w ktorym wywolujesz nie zostal jeszcze zaladowany do pamieci, to zostanie. Gdy sprobujesz wywolac nieistniejaca funkcje, zostanie zwrocone 0 bez zadnych komunikatow bledu. Jesli wywolasz funkcje z obiektu, ktory ma jakies bledy, to *zostanie* wyswietlony komunikat bledu i dzialanie obiektu ktory wykonal wywolanie zostanie przerwane. Po co wiec ten `call_self()' w takim razie? Przeciez moznaby uzywac zamiast niego `call_other()' przez caly czas z takim samym skutkiem? Nie do konca tak. `call_other()' miesza jeszcze sporo w czyms co sie nazywa dostepem do funkcji w obiekcie - o tym jeszcze nie mowilem. Roznica polega na tym, ze `call_self' rzeczywiscie dziala jak jakiekolwiek wywolanie wewnetrzne (ma dostep do wszystkich funkcji w danym obiekcie), podczas gdy `call_other()' jest typowym wywolaniem zewnetrznym. Pamietaj o tym, kiedy bede omawial roznice pomiedzy dostepem w wywolaniach wewnetrznych i zewnetrznych. 2.2.1.3 - Tworzenie wielokrotnych zewnatrz-obiektowych wywolan funkcji ..................................................................... Mozesz wywolywac wiele obiektow na raz, tak samo prosto jakbys to robil z jednym. Jesli masz tablice stringow ze sciezkami, albo wskaznikow do obiektow lub mapping, w ktorym wartosci sa sciezkami do plikow lub wskaznikami do obiektow, to mozesz w jednej instrukcji wywolac je wszystkie. Rezultatem bedzie albo tablica z wynikami, jesli wywolywales za pomoca tablicy, albo mapping z takimi samymi indeksami z jakimi wywolywales, jesli korzystales z mappinga. (tablica/mapping) ->(); np. /* * Potrzebuje mappingu, gdzie indeksami beda imiona graczy, a * wartosciami ich hitpointy. */ object *ludzie; mapping hp_map; string *imiona; // Przypisuje liste wszystkich graczy. ludzie = users(); // Bierze ich imiona. imiona = ludzie->query_real_name(); // Tworzy mapping, przy pomocy ktorego beda robione wywolania. // Element = imie:wskaznik hp_map = mkmapping(imiona, ludzie) // Zastepuje wskazniki wartosciami hitpoint. hp_map = hp_map->query_hp(); // A wszystko to mogloby byc zrobione prosciej w ten sposob: hp_map = mkmapping(users()->query_real_name(), users()->query_hp()); 2.2.2 - Dziedziczenie obiektow ------------------------------ Zalozmy, ze chcesz zakodowac jakas rzecz, drzwi na przyklad. Musisz wiec zaprogramowac mozliwosc otwierania i zamykania przejscia pomiedzy dwoma pokojami. Byc moze chcesz rowniez moc otwierac i zamykac zamek w drzwiach. O wszystko to musisz zadbac w swoim kodzie. Na dodatek, bedziesz musial kopiowac ten sam kod za *kazdym razem*, kiedy dodasz nowe drzwi i bedziesz chcial zeby roznily sie tylko opisem. Po jakims czasie wkurzysz sie na to wszystko, czesciowo dlatego, ze odkryjesz ze inni wizardzi napisali wlasne drzwi, ktore dzialaja prawie - lecz nie do konca - tak jak twoje, przez co twoje swietne obiekty beda bezuzyteczne poza twoja domena. Myslenie zorientowane obiektowe polega na tym, ze zamiast powtarzac takie pisanie w kolko, tworzy sie podstawowy obiekt drzwi, ktory moze robic wszystko to, co uwazasz, ze typowe drzwi powinny moc. Wtedy dziedziczy sie go w naszych konkretnych drzwiach, konfiguruje sie *je* (nie obiekt dziedziczony), korzystajac z mozliwosci drzwi "rodzica". Jest nawet mozliwe dziedziczenie kilku obiektow, choc "wielokrotne dziedziczenie" niesie ze soba kilka trudnych problemow do rozwiazania, wiec staraj sie unikac tego. Skladnia na dziedziczenie obiektow jest bardzo prosta. Na poczatku pliku pisze sie cos takiego: inherit ""; np. inherit "/std/door"; // (door oznacza drzwi) inherit "/std/room.c"; *UWAGA!* To *NIE* jest dyrektywa preprocesora, a zwykla instrukcja, wiec stawia sie `;' na koncu i *NIE* pisze sie `#' przed nia. Jesli chcesz, mozesz wyszczegolnic w sciezce, ze chodzi o plik c, aczkolwiek to nie jest konieczne. "Dziecko" dziedziczy wszystkie funkcje i wszystkie zmienne, ktore sa zadeklarowane w sposob pozwalajacy na ich dziedziczenie. Jesli masz funkcje o takiej samej nazwie jak w obiekcie "rodzicu", to twoja "zamaskuje" funkcje "rodzica". Kiedy funkcja bedzie wolana poprzez wywolanie zewnetrzne, to wykonana zostanie twoja funkcja. Wewnetrzne wywolania w "rodzicu" beda sie jednak odnosily do jego funkcji. Nieraz potrzebujesz wywolac funkcje "rodzica" z "dziecka" - robi sie to poprzez dodanie `::' przed wewnetrznym wywolaniem funkcji. void moja_funkcja() { /* * Ta funkcja istnieje i w naszym obiekcie(dziecku) i w obiekcie * ktory dziedziczymy(rodzicu), ale potrzebujemy ja * wywolac z rodzica. */ ::moja_funkcja(); // Wywoluje moja_funkcje w rodzicu. } Nie da sie wywolac zamaskowanej funkcji w rodzicu poprzez wywolanie zewnetrzne, mozliwe jest to tylko z rodzica. Gdy obiekt dziedziczy inny, ktory z kolei dziedziczy jeszcze inny, np. C dziedziczy B, a ten dziedziczy A, to wtedy maskowana funkcja z A, jest mozliwa do wywolania tylko w B, a w C juz nie. 2.2.3 - Cienie : Maskowanie funkcji w czasie wykonywania obiektu ---------------------------------------------------------------- Jest cos takiego w LPC, co jest zwane "cieniowaniem". Purytanie wola jednak nazywac to 'obrzydlistwem' i przy kazdej okazji krzycza zeby to usunac, gdyz wystepuje to prawie przeciwko wszystkiemu, czego uczy sie o dobrym programowaniu. "Cieniowanie" jest raczej uzyteczne w grze, aczkolwiek moze spowodowac troche problemow glownie zwiazanych z bezpieczenstwem. Uzywaj tego z rozwaga! Gdy jeden obiekt cieniuje drugi, wszystkie powtarzajace sie funkcje i zmienne z cieniujacego obiektu, zamaskuja te z cieniowanego obiektu. Wywolania cieniowanych funkcji, spowoduja wykonanie zamiast nich, tych z cieni. Cien praktycznie 'stanie' sie obiektem, ktory cieniuje. Od zwyklego maskowania rozni je to, ze funkcje sa maskowane w czasie kompilacji. Obiekty-cienie zas sa dodawane do obiektow juz w czasie ich dzialania. Mozna w ten sposob dodac do obiektu jakies wlasciwosci, ktore nie sa przewidziane w kodzie. To wszystko na razie na ten temat. Sposob tworzenia i zastosowanie cieni beda szczegolowo omowione pozniej. Jak na razie wiesz wszystko, co potrzebujesz wiedziec. 2.2.4 - Identyfikacja typow ---------------------------- [intp, floatp, functionp, stringp, objectp, mappingp, pointerp] Ze wzgledu na fakt, ze wszystkie zmienne sa na poczatku ustawiane na 0 i ze wiele funkcji zwraca 0 w razie bledu, warto moc zbadac jaki typ wartosci odebralismy. Rowniez, gdy uzywa sie typu `mixed', niezbedna jest informacja jaki typ wartosci zawiera zmienna. Do tego celu istnieja specjalne funkcje kontrolne, ktore zwracaja 1 (prawde) jesli testowane wartosci sa danego typu, a 0 jesli nie. int intp(mixed) Sprawdza, czy podana wartosc jest typu integer int floatp(mixed) Sprawdza, czy podana wartosc jest typu float functionp(mixed) Sprawdza, czy podana wartosc jest wskaznikiem funkcji (typ function) int stringp(mixed) Sprawdza, czy podana wartosc jest stringiem int objectp(mixed) Sprawdza, czy podana wartosc jest wskaznikiem obiektu (typ object) int mappingp(mixed) Sprawdza, czy podana wartosc jest mappingiem int pointerp(mixed) Sprawdza, czy podana wartosc jest tablica *UWAGA!* Funkcje te sprawdzaja typ wartosci, a *NIE* sama wartosc w znaczeniu poprawnosci dzialania. Innymi slowy `intp(0)' zawsze zwroci prawde, tak jak to zrobi `mappingp(([]))'. 2.2.5 - Kwalifikatory typow --------------------------- Typy, ktore przypisujesz zmienym i funkcjom moga miec rozne kwalifikatory, zmieniajace sposob ich dzialania. Bardzo wazne jest to, by o nich caly czas pamietac i uzywac w odpowiednich miejscach. Wiekszosc z nich inaczej dziala na zmiennych, a inaczej na funkcjach, wiec male klopoty zwiazane z ich uzywaniem sa dosyc powszechne wsrod programistow. Postaraj sie przejsc przez to teraz, a nie bedziesz mial pozniej zadnych problemow. 2.2.5.1 - Kwalifikator zmiennej `static' ........................................ Jest to dosyc klopotliwy kwalifikator, gdyz dziala on inaczej nawet na rozne zmienne, w zaleznosci od tego gdzie one sa! Zmienne globalne (jak na pewno wiesz) definiowane sa na poczatku pliku, *na zewnatrz* funkcji. Sa one dostepne we wszystkich funkcjach, tzn. ich "zasieg" rozciaga sie na caly obiekt i nie jest ograniczony do jednej funkcji. Istnieje mozliwosc nagrania wszystkich zmiennych globalnych jakiegos obiektu, za pomoca pewnej efunkcji (opisana bedzie dalej). Jednakze, jesli zmienna globalna jest zdefiniowana jako `static', to *nie* bedzie nagrana. static string JakasNazwa; // Nie nagrywana zmienna globalna. 2.2.5.2 - Kwalifikator funkcji `static' ....................................... Do funkcji, ktore sa zadeklarowane z uzyciem kwalifikatora `static' nie mozna stosowac zewnetrznych wywolywan, tylko wewnetrzne. Czyni to taka funkcje 'niewidzialna' i niedostepna dla innych obiektow. Miedzy innymi tu jest wlasnie ta roznica pomiedzy wewnetrznymi a zewnetrznymi wywolaniami funkcji, o ktorej wczesniej mowilem. 2.2.5.3 - Kwalifikator funkcji/zmiennej `private' ................................................. Zmienne lub funkcje, ktore zostaly zadeklarowana jako `private' nie beda mogly byc dziedziczone przez inne obiekty. Dostep do nich ma tylko obiekt, ktory je definiuje. 2.2.5.4 - Kwalifikator funkcji/zmiennej `nomask' ................................................ Funkcje i zmienne, ktore sa zadeklarowane jako `nomask' nie moga byc maskowane w zaden sposob, ani przez cieniowanie, ani przez dziedziczenie. Jesli sprobujesz zamaskowac takowa, to otrzymasz komunikat bledu. 2.2.5.5 - Kwalifikator funkcji/zmiennej `public' ................................................ Jest to standardowy kwalifikator dla wszystkich funkcji i zmiennych. Oznacza to, ze nie narzuca on zadnych innych ograniczen ponad te, ktore sa narzucone przez jezyk. 2.2.5.6 - Kwalifikator funkcji `varargs' ........................................ Funkcje, ktore sa zdefiniowane jako `varargs' beda mogly otrzymywac rozna liczbe argumentow. W takim przypadku nie ma potrzeby podawania ich wszystkich przy wywolywaniu, zeby bylo ono prawidlowe. Moga byc ustalone standardowe wartosci dla argumentow, ktore nie otrzymaly zadnych wartosci. varargs void mojafun(int a, string str = "pelle", float c = 3.0); { } Jak widzisz, kilka argumentow dostalo standardowe wartosci, ktore beda uzyte w przypadku gdy dany argument nie zostanie podany. Jesli nie ustalisz standardowej wartosci, to wartosc nie podanego argumentu zostanie ustawiona na 0. 2.2.6 - Drugi rzut oka na typy danych ------------------------------------- Jeden typ danych byl dotad mniej lub bardziej ignorowany. Jest nim typ `function'. Jako, ze obiekty maja swoj wlasny typ, funkcje maja taki rowniez. Dzieki niemu mozesz miec zmienne, poprzez ktore bedziesz mogl wywolywac funkcje. Przewaznie jest on jest uzywany w polaczeniu z innymi funkcjami, ktore uzywaja go jako parametru. Zmienna tego typu definiuje sie tak jak kazda inna: , , ..., ; np. function moja_fun, *tablica_fun; Przypisywanie odnosnikow funkcji do nich nie jest jednak takie proste. Mozesz przypisac dowolny typ funkcji do zmiennej tego typu: czy efunkcja, czy sfunkcja, czy lfunkcja nie czyni zadnej roznicy. Istnieje nawet mozliwosc przypisania odwolania do funkcji zewnetrznej. Przypisanie wskaznika do funkcji wymaga tego, by dana funkcja byla zdefiniowana, albo poprzez prototyp, albo poprzez deklaracje. Zalozmy, ze jak na razie interesuja cie tylko proste wskazniki do funkcji. = ; = &(); np. moja_fun = allocate; moja_fun = &allocate(); Wskaznika do funkcji uzywa sie tak *samo* jak zwyklego wywolania funkcji. int *i_tabl; i_tabl = allocate(5); // Korzystamy z powyzszego i_tabl = moja_fun(5); // ... przypisania wskaznika funkcji. Wystarczy tego na teraz. Pozniej wyjasnie jak tworzyc bardziej skomplikowane struktury funkcji. 2.2.7 - Drugi rzut oka na instrukcje switch/case ------------------------------------------------ Instrukcja switch jest bardzo sprytna i mozna w niej stosowac testowac, czy jakas wartosc typu integer miesci sie w podanym zakresie: public void kolo_fortuny() { int i; i = random(10); // Przypisuje losowa liczbe z zakresu 0 - 9 switch (i) { case 0..4: write("Sprobuj jeszcze raz, frajerze!\n"); break; case 5..6: write("Ekstra, masz trzecie miejsce!\n"); break; case 7..8: write("Tak! Drugie miejsce!\n"); break; case 9: write("OOPS! Udalo ci sie!\n"); break; default: write("Ktos grzebal przy kole... Wykrec 997!\n"); break; } } 2.2.8 - catch/throw: Obsluga bledow w czasie dzialania programu --------------------------------------------------------------- Czasami zachodzi potrzeba wywolania funkcji, o ktorej wiadomo, ze moze zwrocic blad runtime (czyli taki, ktory wynikl po kompilacji, juz w czasie dzialania programu). Na przyklad w swoim programie mozesz chciec sklonowac jakis obiekt (opisane pozniej) albo zaladowac plik. Jesli nie ma takiego pliku, lub gdy masz niewystarczajace przywileje, to wyskoczy blad runtime i wykonanie programu zostanie zatrzymane. W tych okolicznosciach przydalaby sie mozliwosc przechwycenia bladu i wyswietlenia wlasnego komunikatu o bledzie, lub tez wykonania innej czynnosci. Jest to mozliwe dzieki funkcji `catch()'. Jako argument podaje sie w niej zmienna typu 'function'. Jesli `catch()' zwroci 1 (prawde), to bedzie to oznaczalo, ze wystapil blad w czasie wykonywania podanej funkcji. Jesli wszystko z podana funkcja bedzie ok, to zwroci 0. int catch(function) np. if (catch(tail("/d/Relikwa/fatty/tajna_mapa_do_ukrytych_orzeszkow"))) { write("Przykro mi, nie ma mozliwosci przeczytania tego "+ "pliku.\n"); return; } Jest takze mozliwe wywolanie bledu i co za tym idze przerwanie programu. Jest to uzyteczne, gdy chcesz powiadomic uzytkownika o nieplanowanym zdazeniu, ktore wystapilo w czasie dzialania programu. Zazwyczaj sie to umieszcza w czesci `default' komendy switch, o ile (oczywiscie) nie uzywasz tej czesci jako pewnego rodzaju lapacza wszelkich pozostalych mozliwosci. throw(mixed info) np. if (test < 5) throw("BLAD: Zmienna 'test' ma wartosc mniejsza niz 5.\n"); 2.2.9 - Wskazniki do tablic i Mappingow --------------------------------------- W terminologii informatycznej tablice i mappingi sa uzywane jako "odniesienie poprzez wskaznik", podczas gdy inne zmienne jako "odniesienie poprzez wartosc". Oznacza to, ze tablice i mappingi, w przeciwienstwie do innnych rodzajow zmiennych w czasie kopiowania nie sa dublowane. Zamiast nich, dublowany jest tylko wskaznik do oryginalnej tablicy czy mappinga. Co to wszystko oznacza? Hmm... Po prostu to: object *tab, *tab_2; tab = ({ 1, 2, 3, 4 }); // Jakas tablica. tab_2 = tab; // Zalozmy (zle), ze 'tab_2' // staje sie kopia tablicy 'tab'. // Zmieniamy pierwszy element (1) na 5. tab_2[0] = 5; ... I teraz ... logicznie rzecz biorac wydawaloby sie, ze wartosc pierwszego elementu tab_2 rowna jest 5, podczas gdy ten sam element w 'tab' wynosi 1. Nie jest tak jednak, gdyz to co zostalo skopiowane do zmiennej tab_2 nie bylo tablica, tylko wskaznikiem do tej samej tablicy co `tab'. Oznacza to, ze nasze dzialanie zmienilo pierwszy element w oryginalnej tablicy, do ktorej obie zmienne maja wskazniki. Wydawalo by sie, ze `tab_2' i `tab' obie sie zmienily, podczas gdy w rzeczywistosci zmianie ulegla tylko tablica, do ktorej obie zmienne sie odwolywaly. Dokladnie to samo sie stanie w przypadku mappingow, gdyz pod tym wzgledem dzialaja one tak samo. Wiec.. jak w takim razie obejsc to? Czasem na prawde zachodzi potrzeba pracy na kopii, a nie na oryginale tablicy czy mappingu. Rozwiazanie jest bardzo proste. Trzeba sie tylko upewnic, ze kopia zostala stworzona z innej tablicy lub mappingu. _ To jest po prostu pusta tablica. / tab_2 = ({ }) + tab; \_ A to jest ta szczegolna, o ktora nam chodzi. W tym przykladzie `tab_2' staje sie suma tablicy pustej i tablicy `tab', stworzona jako zupelnie nowa tablica. Pozostawi to w przyszlosci oryginal bez zmian, dokladnie tak jak chcielismy. Mozesz uczynic dokladnie to samo z mappingami. Nie gra roli to, czy pusta tablice lub mapping dodajesz z przodu, czy z tylu, o ile wogole dodajesz. 2.3 - Interfejs LPC/Mudliba =========================== Jest sporo rzeczy, ktore mozesz zechciec zrobic, ktorych nie da sie wyrazic tylko za pomoca elementow jezyka LPC. Jest to na przyklad obsluga lancuchow znakowych czy tez zapisywanie do plikow. Rzeczy te sa bowiem czescia 'standardowego zestawu funkcji', ktory zawiera wiekszosc jezykow programowania. Ten rozdzial ma nauczyc cie podstaw robienia wszystkich tego typu rzeczy, potrzebnych do tworzenia obiektow LPC. Istnieje okreslony zestaw globalnych wlasciwosci, co do ktorych mozesz miec pewnosc, ze beda dostepne zawsze w dowolnym obiekcie. Oto one: *creator Kazdy obiekt jest tworzony przez kogos. Tozsamosc autora zalezy od lokacji zrodla (pliku zrodlowego, czyli takiego, z ktorego obiekt jest wykonywany) w systemie plikow. Jesli obiekt znajduje sie w katalogu zwyklego czarodzieja, wtedy jako jego autora podaje sie imie tego czarodzieja. W innym przypadku podawana jest nazwa domeny. Dla obiektow mudliba, autorem jest `root' w przypadku obiektow o prawach administratora, albo `backbone' w przypadku reszty. *uid/euid "uid" (User ID - czyli identyfikator uzytkownika) obiektu okresla najwyzszy mozliwy poziom przywilejow dla danego obiektu. Samo uid jest uzywane tylko do ograniczania na "euid" (Effective User ID - czyli rzeczywisty identyfikator uztykownika) tego samego lub innego obiektu. Euid jest uzywane w sytuacjach, gdy przywileje obiektu musza byc sprawdzone, np. dostep do pliku (czytanie/zapisywanie/usuwanie), badz tworzenie obiektu. *living Zeby obiekt mogl przyjmowac badz wydawac komendy, musi byc living'iem. 2.3.1 - Definicja obiektow standardowych i bibliotecznych --------------------------------------------------------- Jak juz poprzednio mowilem, gamedriver prawie nic nie wie o grze. A conajwyzej tak malo, jak to mozliwe. To mudlib musi sie tym wszystkim zaopiekowac. Zajmiemy sie teraz podstawowymi sprawami z nim zwiazanymi, np. poruszaniem sie, poziomami oswietlenia, tym jak obiekty powinny sie kontaktowac z graczami, itp. Po tym stworzymy obiekty, ktore beda wykorzystywaly omowione funkcje. Zwykly czarodziej nie musi przesiadywac dlugich godzin zastanawiajac sie jak napisac obiekt, bedacy w prawidlowej relacji z innymi i rozpatrujac jeszcze tysiace innych, klopotliwych spraw. Zamiast tego dziedziczy odpowiadajacy mu, jeden ze standardowych obiektow. Pozniej tylko dorzuca don kilka ustawien, czyniac go unikalnym wzgledem innych obiektow tego typu. Konsekwencja tego jest sytuacja w ktorej wszystkie obiekty w grze w 100% polegaja na fakcie, ze okreslone typy obiektow (pokoj, potwor, bron) maja okreslone zestawy powszechnych cech i mozliwosci. Po prostu *musza* miec, by mogly wspolpracowac w ustalony sposob. Jesliby nie mogly, jesliby kazdy czlowiek mial inne rozwiazanie tego samego problemu, obiekty dzialalyby tylko na terenie danego czarodzieja i nigdzie poza nim. Nie byloby mozliwe uzywanie miecza w calej grze. Ba! Nawet nie moznaby go nigdzie przemiescic. Oczywiscie oznacza to, ze musimy wymuszac jednomyslnosc, gdyz *niemozliwe* jest stworzenie (i uzytkowanie) obiektow, ktore nie dziedzicza tych specjalnych obiektow. Jak sam pozniej zobaczysz mozliwym byloby napisanie miecza, ktory nie bedzie dziedziczyc standardowego obiektu broni, tyle ze wtedy dzierzenie go staloby sie bariera nie do przebycia... Istnieja rozne standardowe obiekty uzywane do roznych celow, ale najwazniejszym z nich jest `/std/object.c'. 2.3.1.1 - Podstawowy obiekt: /std/object.c .......................................... Ten obiekt jest wielozadaniowy. WSZYSTKIE obiekty majace 'fizyczna' postac dziedzicza go. Dziedziczac jeden ze standardowych obiektow mozesz byc pewien, ze posrednio dziedziczysz takze `object.c', gdyz jest on dziedziczony przez przewazna wiekszosc standardowych obiektow. Standardowy obiekt definiuje nastepujace rzeczy: *zawartosc (inventory) Obiekt moze "zawierac" inne obiekty. W rzeczywistosci jest to zwykla lista obiektow, ktore sa trzymane wewnatrz obiektu posiadajacego te liste. Jednakze bardzo proste jest uwidocznienie tego, jako ekwipunku gracza, wnetrza torby, pokoju, pudelka itp. *srodowisko (environment) Obiekt "otaczajacy" inny obiekt, odwrotnosc zawartosci. Jesli obiekt A jest w 'ekwipunku' obiektu B, to obiekt B otacza obiekt A. Obiekt moze miec wiele innych obiektow w swowim 'ekwipunku', lecz tylko jeden obiekt-srodowisko. Wszystkie nowo utworzone obiekty nie maja zadnego srodowiska. *lista komend Lista polecen powiazanych z funkcjami. Obiekt udostepnia polecenia wszystkim "zyjacym" obiektom, ktore sie znajduja zarowno w jego ekwipunku jak i w srodowisku. Zyjace obiekty moga wydac takie polecenie i obiekt udostepniajacy je wywola powiazana z poleceniem funkcje. *wlasciwosci (properties) Wlasciwosci sa czysto umowna sprawa. Jest to najzwyklejszy w swiecie mapping, ktorego indeksy sa jakimis zarezerwowanymi nazwami, a wartosci zmiennymi, ktore wplywaja na jakies ogolnie dostepne stany. Typowymi wlasciwosciami sa waga, cena, poziom swiatla i nieco bardziej abstrakcyjne pojecia takie jak mozliwosc upuszczenia, wziecia czy sprzedania obiektu. Mozliwy do uzycia zestaw wlasciwosci zalezy juz od konkretnych obiektow. *poziom swiatla Obiekt ma okreslony poziom swiatla. Zazwyczaj nie wplywa on na otoczenie, choc mozliwe sa do utworzenia zarowno zrodla swiatla, jak i zrodla ciemnosci. Poziom swiatla jest jedna z wlasciwosci. *waga/objetosc Wartosci te okreslaja, jak duzo obiekt wazy i ile miejsca zajmuje. W przypadku 'pojemnikow' takich jak torby definiuje to takze ich pojemnosc. *widzialnosc Niektore obiekty moga byc latwiejsze do znalezenia niz inne. *nazwy i opisy Jak obiekt sie nazywa i jakim widzi go gracz. 2.3.1.2 - Standardowe klasy obiektow .................................... Istnieje spora liczba standardowych obiektow, ktorych sie uzywa (stosuje sie termin 'dziedziczy') w roznych sytuacjach. By dowiedziec sie nieco wiecej o kazdym z nich, bedziesz potrzebowal osobnej dokumentacji. Ten spis dostepnych obiektow standardowych ma za zadanie przynajmniej ukierunkowac cie w poszukiwaniach. Jak juz wczesniej mowilem, wiekszosci z nich nie wykorzystuje sie nacodzien. Z kilkoma jednak wkrotce staniesz sie za pan brat, gdyz opisywanych przez nie typow obiektow nie da sie napisac nie dziedziczac ich. `/std/armour.c' Dowolne zbroje `/std/board.c' Tablice ogloszeniowe `/std/book.c' Ksiazka ktora mozesz otwierac i zamykac, ze stronami, ktore mozesz przewracac i czytac. `/std/coins.c' Podstawa wszystkich rodzajow pieniedzy `/std/container.c' Dowolny obiekt, ktory moze zawierac w sobie inne (pojemnik) `/std/corpse.c' Cialo martwych potworow/graczy/npc'ow (npc - Non Player Charater, czyli wszystkie zyjace istoty, ktore nie sa graczami) `/std/creature.c' Proste zyjace istoty `/std/domain_link.c' Dziedziczone przez obiekty uczestniczace w twz. 'preloadingu'. (po kazdej Apokalipsie, przed dopuszczeniem graczy do gry, wazniejsze obiekty sa od razu wczytywane do pamieci, by zredukowac laga) `/std/door.c' Drzwi, ktore lacza dwa pokoje `/std/food.c' Jedzenie roznego rodzaj `/std/guild (katalog)' Obiekty zwiazane z gildiami (gildie i shadowy) `/std/heap.c' Obiekty dowolnego rodzaju, ktore moga byc umieszczone w stosach `/std/herb.c' Ziola `/std/key.c' Klucze do drzwi `/std/leftover.c' Pozostalosci po rozlozonych cialach `/std/living.c' Zyjace obiekty `/std/mobile.c' Przemieszczajace sie zyjace obiekty `/std/monster.c' Dowolne potwory `/std/npc.c' Sprytniejsze istoty humanoidalne `/std/object.c' Podstawowy obiekt `/std/poison_effect.c' Obsluguje dzialanie roznych trucizn `/std/potion.c' Mikstury `/std/receptacle.c' Dowolnego rodzaju pojemniki, ktore mozna otwierac/zamykac `/std/resistance.c' Obsluguje odpornosci na rozne rzeczy `/std/room.c' Dowolny rodzaj lokacji `/std/scroll.c' Pergaminy, kartki itp `/std/shadow.c' Uzywane jako podstawa w tworzeniu obiektow typu 'shadow' `/std/spells.c' Obiekty czarow, tomow itp `/std/torch.c' Pochodnie/lampy itp. `/std/weapon.c' Bronie wszelakiej masci 2.3.1.3 - Standardowe obiekty biblioteczne .......................................... To sa pomocnicze pliki. Uzywa sie ich w polaczeniu z obiektami z katalogu '/std/'. Na przyklad chcac zakodowac karczme, dziedziczymy '/std/room.c' oraz '/lib/pub.c'. `/lib/bank.c' Dowolny rodzaj banku `/lib/cache.c' 'Cache' uzywany w celu przyspieszenia regularnie wykorzystywanych obiektow `/lib/guild_support.c' Dodatkowe funkcje dla gildii `/lib/herb_support.c' Dodatkowe funkcje do ziol `/lib/more.c' Mozliwosc 'more' (przegladania plikow) `/lib/pub.c' Funkcje przydatne przy pisaniu karczm `/lib/shop.c' Sklepy dowolnego rodzaju `/lib/skill_raise.c' Trenowanie umiejetnosci `/lib/store_support.c' Pomocne funkcje w magazynach (niezbedny dodatek do sklepow) `/lib/time.c' Funkcje obslugujace czas `/lib/trade.c' Pomocne we wszystkim co jest zwiazane z handlem 2.3.2 - Jak zdobyc odnosniki do obiektow ---------------------------------------- Obiekty, jak juz wczesniej mowilem, wystepuja w dwoch rodzajach - twz. "master object" i "klon". W wiekszosci przypadkow operuje sie na sklonowanych obiektach. Sa to miedzy innymi takie, ktore mozesz przesunac, dotknac, obejrzec itp, lub dowolne inne, ktore wystepuja w wiecej niz jednej kopii. Operowanie na "master objectach" ogranicza sie zazwyczaj do pokoi i souli (soule to obiekty wykorzystywane przez obiekty graczy, ktore dodaja im rozne komendy). *Kazdy* zaladowany obiekt ma swoj "master object", ktory jest reprezentacja tego obiektu w pamieci, a zarazem pierwszym klonem. Zawiera on informacje o mechanizmie dzialania obiektu. Klony skladaja sie jedynie z zestawu zmiennych i odnosnika do swojego "master objectu". Ow zestaw zmiennych odroznia poszczegolne klony od siebie (np. dwa miecze tego samego typu moga sie roznic stopniem stepienia). Jesli zniszczysz "master object", jego klony istniejace juz w grze pozostana (informacja o mechanizmie dzialania obiektu gdzies pozostanie w pamieci). Zwiazany jest z tym blad w mysleniu wielu poczatkujacych czarodzieji: Uwazaja, ze przeladowujac "master object" do nowej postaci (po zmianach ktore wprowadzili w kodzie), zmienia sie wszystkie juz istniejace klony. Nic bardziej mylnego - po przeladowaniu (updatowaniu) "master objectu" dopiero nowe klony beda zawieraly wprowadzone zmiany. Ladowanie "master objectu" do pamieci odbywa sie poprzez wywolanie na sciezce do jego pliku dowolnej, nieistniejacej funkcji (sciezka do pliku stanowi jeden ze sposobow na odniesienie sie do "master objectu"). Przeladowywanie obiektu (zwane rowniez jego uaktualnianiem lub updatowaniem) polega usunieciu z pamieci starego "master objectu" i zaladowaniu nowego. Jak w takim razie zdobyc odnosnik do obiektu? To zalezy. Odnosnikiem do obiektu moze byc zarowno wskaznik do obiektu, jak i sciezka do zrodla obiektu w systemie plikow. Metody ich uzyskiwania sa rozne w roznych sytuacjach. Wytlumacze teraz je wszystkie. 2.3.2.1 - Odnosniki do aktualnego obiektu ......................................... [this_object, previous_object, calling_object] Kazdy obiekt zawsze moze zdobyc odnosnik do samego siebie. Sluzy do tego efunkcja `this_object()': object this_object() np. object ja; ja = this_object(); Zeby zbadac ktory obiekt wywolal aktualnie uruchomiona funkcje za pomoca wywolania zewnetrznego, mozna uzyc efunkcji `previous_object()': object previous_object(void|int glebokosc) np. object p_ob, pp_ob; p_ob = previous_object(); // Obiekt, ktory wywolal te funkcje pp_ob = previous_object(-2); // Obiekt, ktory wywolal inny obiekt, // ktory wywolal te funkcje Jesli nie podasz zadnego argumentu, albo podasz -1, funkcja zwroci obiekt, ktory ja wywolal. Zmniejszanie argumentu spowoduje podanie dalszych poprzednikow, np. `previous_object(-4)' zwroci obiekt, ktory wywolal obiekt, ktory wywolal obiekt, ktory wywolal twoj obiekt. O ile lancuch wywolan byl na tyle dlugi. Jesli glebokoscia przekroczy sie dlugosc lancucha wywolan, to funkcja zwroci 0. Mam nadzieje, ze zauwazyles, ze funkcja ta sprawdza tylko zewnetrzne wywolania. Istnieje inna efunkcja, ktora dziala tak samo, tyle ze dla wszystkich rodzajow wywolan (wewnetrzne lub zewnetrzne): object calling_object(void|int glebokosc) Uzywa sie jej tak samo jak poprzedniej A wiec... skad mozesz wiedziec czy ktory odnosnik wlasnie dostales jest wlasciwy czy nie (tzn. ze to nie jest np. zero albo cos innego)? Jesli jeszcze pamietasz, istnieje pewna efunkcja zwana `objectp()'? Doskonale by sie do tego celu nadawala - zwroci 1, jesli podany argument bedzie prawidlowym wskaznikiem do obiektu. int objectp(mixed ob) np. if (objectp(calling_object(-2))) write("Tak, ob ktory wywolal ob, ktory nas wywolal " + "istnieje!\n"); else write("Nie ma takiego.\n"); 2.3.2.2 - Tworzenie obiektow ............................ [setuid, getuid, seteuid, geteuid, creator, set_auth, query_auth, clone_object] Wpierw musisz sie upewnic, ze obiekt, ktory probuje stworzyc inny ma do tego wystarczajace przywileje. Reguly sa bardzo proste: Obiekt, ktory ma wlasciwe euid moze klonowac dowolny inny obiekt. Wlasciwe euid, to dowolne, rozne od 0. Uid i Euid jest standardowo ustawiane na 0 we wszystkich nowych obiektach i oznacza, ze obiekt wogole nie ma przywilejow. Zazwyczaj masz ograniczony wybor euid, ktory mozesz ustawic obiektom. Jesli jestes zwyklym wizardem ogranicza sie on tylko do twojego imienia. Lord moze jako euid wstawic swoje imie, lub dowolnego innego wizarda w swojej domenie (wyjatkiem sa tylko Archowie). I oczywiscie obiekty z uid 'root' moga ustawic dowolne euid jakie chca. A wiec... uid jest nadrzedne w stosunku do euid. Uid ogranicza euid jakie mozesz ustawic. Stanowi maksymalny poziom, jaki mozna ustawic euid. Standardowa wartosc uid ustawia sie poprzez wywolanie sfunkcji setuid(): void setuid() np. setuid(); Proste, nie? Wykonanie tej komendy spowoduje przypisanie uid uzyskanego z pozycji zrodla pliku w systemie plikow. Dziala to na takich samych zasadach jak w pobieraniu wartosci zwracanej przez sfunkcje creator(), co zostalo opisane wczesniej. string creator(mixed odnosnik) e.g. string moj_tworca; moj_tworca = creator(this_object()); Aby poznac wartosc aktualnego uid, uzywa sie sfunkcji `getuid()'. string getuid() np. string aktualny_uid; aktualny_uid = getuid(); Wiec... uid ma juz stawiony maksymalny priorytet, taki jaki ma klonujacy. Euid jednakze jest caly czas rowne 0. Poniewaz euid ustala rzczywiste przywileje, bedzie to oznaczalo, ze obiekt nie ma jeszcze *zadnych* praw. Do nadawania wartosci euid uzywa sie sfunkcji `seteuid()', gdzie jako argument podaje sie nowa wartosc euid, o ile ma sie prawa, zeby tak ustawic (jest to sprawdzane). Funkcja zwraca 0 w razie niepowodzenia. Nie podanie zadnego argumentu spowoduje wyzerowanie euid. int seteuid(void|string przyw) np. if (seteuid("lewy")) write("Wlasnie TAK! Jestem panem WSZECHSWIATA!\n"); else write("Ehhhhh...\n"); Jak zwykle jest podobna sfunkcja, ktora zwraca aktualne euid: string geteuid() np. write("Aktualnym euid jest " + geteuid() + .\n"); Wszystkie sfunkcje `setuid()', `getuid()', `seteuid()' i `geteuid()' korzystaja z efunkcji `set_auth()' i `get_auth()'. Uzywa sie ich do zmieniania specjalnej zmiennej z prawami w danym obiekcie. W razie proby uzycia `set_auth()' gamedriver wywoluje specjalna funkcje kontrolna w master object'cie, ktora sprawdza czy ma sie do tego odpowiednie prawa. Jest tak, gdyz tej zmiennej mozna przypisac dowolny string, a sposob w jaki jej uzywamy jest sztucznie ustalony, tak jak uznalismy, ze najlepiej bedzie rozwiazac to w systemie bezpieczenstwa. Kiedy probujesz wykonac operacje, ktora wymaga odpowiednich przywilejow, taka jak zapisywanie do pliku, czy klonowanie obiektu, to gamedriver wywoluje inne specjalne funkcje w master object'cie, zeby sie upewnic czy masz odpowiednie prawa. Wszystko to zalezy od tego, czy informacje zawarte w zmiennej z prawami maja scisle okreslona, wymagana postac. W zwiazku z tym nie jestes w stanie zdzialac nic wiecej za pomoca `set_auth()', niz bys mogl z `setuid()' i `seteuid()'. `query_auth()' nie jest zabezpieczone przed wywolywaniem, lecz nie znajdziesz tam zadnych ciekawych informacji. Tak prawde mowiac, informacja zawarta w zmiennej z prawami, ktorej to wartosc zwraca `query_auth()' ma po prostu postac uid i euid oddzielonych dwukropkiem. Skoro juz wiemy jak nadawac przywileje innym obiektom, sprobujmy korzystajac z nich cos sklonowac. Efunkcja do tego celu sluzaca nazywa sie `clone_object()'. Laduje i tworzy obiekt z pliku zrodlowego. Gdy operacja klonowania sie nie uda, na przyklad ze wzgledu na jakies bledy w kodzie pliku zrodlowego, zostanie wyswietlony komunikat bledu zas wykonanie obiektu przerwane. object clone_object(string sciezka) np. object magiczny_pierscien; // Tak sie ustawia prawa obiektu do klonowania setuid(); // pobiera uid na podstawie katalogu seteuid(getuid()); // jako euid podstawia pobrane uid // No i samo klonowanie magiczny_pierscien = clone_object("/d/Domena/wiz/mag_pierscien"); Oczywiscie wystarczy, ze ustawisz uid/euid obiektu *RAZ* - pozniej juz nie musisz tego robic przed wykonaniem kazdej operacji, ktora wymaga odpowiednich praw. Najbardziej rozpowszechniona metoda jest umieszczenie wywolan funkcji ustawiajacych uid/euid w funkcji, ktora jest wywolywana w momencie tworzenia obiektu - ale o tym pozniej. A teraz... tablice i mappingi istnieja tak dlugo, jak dlugo korzystaja z nich jakies zmienne. Gdy ostatnia zmienna odnoszaca sie do nich zostaje wyzerowana, to dane zawarte w nich sa rowniez czyszczone. Czy jest tak samo z obiektami? *NIE!* Obiekt pozostanie w grze tak dlugo, jak gamedriver jest uruchomiony lub dopoki doraznie go nie zniszczysz. 2.3.2.3 - Odnajdywanie odnosnikow do innych obiektow .................................................... [file_name, find_object, object_clones, find_living, set_living_name, MASTER_OB, IS_CLONE] Jak juz mowilem odnosniki do obiektow moga byc zarowno stringami jak i wskaznikami do obiektow. Zamienianie odnosnika ze wskaznika w lancuch znakowy robi sie przy pomocy efunkcji `file_name()': string file_name(object ob) np. write("To jest obiekt: " + file_name(this_object()) + ".\n"); String, zwracany przez `file_name()' jest tekstowa reprezentacja wskaznika do obiektu. Ma postac `#', na przyklad `"/d/Domena/wiz/mag_mikstura#2321"'. Ten lancuch jest wlasciwym odnosnikiem do danego obiektu. Aby zamienic tekstowy odnosnik do pliku we wskaznik, uzywa sie efunkcji `find_object()'. object find_object(string odn_ob) np. object obiekt; // Master object obiekt = find_object("/d/Domena/wiz/mag_mikstura"); // Konkretny klon obiekt = find_object("/d/Domena/wiz/mag_mikstura#2321"); Jesli funkcja nie znajdzie obiektu (moze byc podana zla sciezka, dany klon moze nie istniec lub obiekt moze byc nie zaladowany), to zwraca 0. Czasem przydaje sie miec odnosniki do wszystkich klonow danego obiektu. Do znalezienia ich sluzy efunkcja `object_clones()'. Zwroci ona tablice zawierajaca wskazniki wszystkich klonow master objectu, na ktory wskazuje odnosnik podany jako argument. Oznacza to, ze mozesz podac zarowno wskaznik do master objectu, jak i do jednego z jego klonow - nie robi to roznicy. Jesli funkcja nie bedzie w stanie znalezc zadnych klonow, to zwroci pusta tablice. object *object_clones(object odn_ob) np. object *lista_klonow; lista_klonow = object_clones(find_object("/d/Domena/wiz/mag_mikstura")); Niektore obiekty nazywa sie "zyjacymi" (living). W grze objawia sie to miedzy innymi tym, ze moga one zostac zaatakowane i (byc moze) zabite. Zyjace obiekty rejestruja sie na specjalnej liscie w gamedriverze. Robia to po to, by mozna je latwiej znalezc. Specjalna efunkcja `find_living()' szuka w tej liscie podanej nazwy zyjacego obiektu. object *find_living(string imie, void|int 1) np. object balrog_ob, *balrogowie; // Szuka potwora 'balrog' w grze. balrog_ob = find_living("balrog"); Jesli podasz '1' jako drugi argument, efunkcja zwroci liste wszystkich obiektow z ta nazwa. balrogowie = find_living("balrog", 1); Jesli efunkcja nie bedzie mogla znalezc obiektu z podanym imieniem, zwroci 0. Zeby obiekt znalazl sie na liscie imion, musi w sobie wywolac efunkcje `set_living_name()'. void set_living_name(string imie) np. // Jest to czesc funkcji create() w kodzie balroga. set_living_name("balrog"); Pamietaj o tym, ze jesli masz wiele obiektow z ta sama nazwa, to `find_living()' zwroci losowo jeden z nich. Zeby zdobyc odnosnik do master_objectu obiektu do ktorego masz wskaznik, musisz zamienic go na stringa i wtedy oderwac od niego numer klonu. Istnieje juz jednak makro znajdujace sie w standardowym pakiecie `/sts/macros.h' robiace to za ciebie. Po prostu dodaj na poczatku pliku linijke `#include ' i uzyj makra `MASTER_OB'. string MASTER_OB(object ob) np. string master; // Zalozmy ze /sys/macros.h juz zostalo przylaczone do tego pliku. master = MASTER_OB(find_living("balrog")); Makro zwraca odnosnik do master objectu balroga w postaci tekstowej, wiec jesli chcesz miec wskaznik do master objectu musisz posluzyc sie funkcja `find_object()', jako argument podajac swiezo uzyskany string. Klon najlatwiej odroznic od master object'u poprzez porownywanie tekstowego odnosnika. W tym celu mozna uzyc makra IS_CLONE. Jest ono rowniez dostepne w `/sys/macros.h'. Korzysta ono z `this_object()' i nie wymaga zadnego argumentu. int IS_CLONE np. if (IS_CLONE) write("Jestem klonem!\n"); 2.3.2.4 - Odnosniki do interaktywnych obiektow .................................................. [find_player, this_interactive, this_player] Jesli szukasz jakiegos konkretnego gracza, to do cego celu moglbys uzyc `find_living()', a potem upewnic sie, ze zwrocona wartosc jest interaktywnym obiektem. Jednakze o wiele szybciej jest uzyc efunkcji `find_player()', ktora robi dokladnie to samo, z jednym wyjatkiem - moze byc tylko jeden gracz o podanym imieniu w grze. Jesli bedzie on aktualnie zalogowany, to otrzymasz do niego odnosnik. object *find_player(string imie) np. object lewy; lewy = find_player("lewy"); if (pointerp(lewy)) lewy->catch_msg("Siemanko, bracie!\n"); else write("Eh.. chyba go nie ma.\n"); Bardzo czesto potrzeba wiedziec kto wydal komende, ktora zapoczatkowala wykonanie jakiejs okreslonej funkcji. Efunkcja `this_interactive()' zwroci do niego wskaznik. Jesli lancuch komend zostal zapoczatkowany przez jakis niezalezny, nieinterakcyjny obiekt, to zwroci ona 0. Czesciej jednak nie interesuje cie kto zapoczatkowal lancuch, lecz raczej na kogo dany obiekt kieruje swoja uwage. Ten wlasnie obiekt zwroci efunkcja `this_player()'. Innymi slowy, podczas gdy obiekt powinien zwracac uwage (listy komend, komunikaty, itp) na obiekt podany przez this_player(), inny gracz zwrocony przez `this_interactive()' mogl stac sie przyczyna wykonania lancucha komend w aktualnym obiekcie. Wartosc `this_interactive()' nigdy nie moze byc zmieniana, w przeciwienstwie do wartosci `this_player()'. Wiecej o tym bedzie pozniej. object this_player(); object this_interactive(); np. object tp, ti; tp = this_player(); ti = this_interactive(); if (objectp(ti)) { if (ti != tp) { tp->catch_msg("Zapppp!\n"); // Ale genialne tlumaczenie, ti->catch_msg("Zapnales go!\n"); // nie? :-) } else ti->catch_msg("Fzzzzz...\n"); } 2.3.2.5 - Niszczenie obiektow ............................. [destruct, remove_object] Predzej czy pozniej bedziesz chcial sie pozbyc jakiegos obiektu. Sluzy do tego efunkcja `destruct()'. Jednakze gamedriver pozwoli *tylko* na zniszczenie samego siebie, tzn. na wywolanie destruct() tylko z obiektu ktory ma byc zniszczony - zadne wywolanie zewnetrzne nie wchodzi w rachube. To oznacza, ze kazdy obiekt potrzebuje funkcji, ktora wywoluje destruct(), ktora moglbys wywolac z innego obiektu, zeby moc zniszczyc go z zewnatrz. Jesli jednak obiekt nie zawiera funkcji korzystajacej z `destruct()', to nie bedziesz w stanie go ruszyc w ten sposob. Istnieje wyjscie z tego, ktore umozliwia ci zniszczenie dowolnego obiektu, ale jest nim komenda, ktora musisz wydac wlasnorecznie. Nie mozesz uzyc jej z wewnatrz programu. Standardowy obiekt, ktory pozniej bedzie bardziej szczegolowo omowiony, definiuje funkcje `remove_object()', ktora mozesz wykonac, aby zniszczyc obiekt. Poniewaz wszystkie obiekty w grze *MUSZA* dziedziczyc go, mozesz miec pewnosc, ze w kazdym znajdziesz te funckje. Mozliwe jest zamaskowanie jej i co za tym idzie zablokowanie. Robienie tego, bedzie jednak zwyklym sabotazem, wiec nawet nie mysl o robieniu czegos takiego. Pozwolilismy na jej maskowanie, dlatego zebys mogl dodac tam jakis kod, ktory w razie niszczenia obiektu zrobi kilka innych rzeczy, a nie po to bys mogl pisac niezniszczalne obiekty. void remove_object() np. void usun_balroga(string imie_bal) { object bal; bal = find_living(imie_bal); if (objectp(bal)) bal->remove_object(); } Gdy bezposrednio uzywasz `destruct()' albo wywolujesz w tym samym obiekcie `remove_object()', *DWA* razy sie upewnij, ze zaden kod nie jest po tym wykonywany. Widzisz, wykonywanie nie jest przerywane od razu po komendzie do zniszczenia - obiekt jest tylko oznaczany jako "juz zniszczony", a sama destrukcja dzieje sie po skonczeniu wykonywania go. Oznacza to, ze wywolania funkcji lub komendy wydane po komendzie destrukcji moga spowodowac bledy runtime w innych obiektach. void destruct() np. void zniszcz_mnie() { write("Zegnaj, okrutny swiecie!\n"); destruct(); } Gdy obiekt jest niszczony, *WSZYSTKIE* wskazniki do niego (nie odnosniki tekstowe) w grze sa ustawiane na 0. Ze wzgledu na ten fakt, warto sie upewnic, czy stary wskaznik do obiektu jest wciaz wlasciwy, przed robieniem z nim czegokolwiek. Nigdy nie masz pewnosci - moze zostal usuniety w miedzyczasie. Jesli niszczony obiekt zawiera jakies inne obiekty, to w czasie destrukcji one sa rowniez usuwane. Wyjatkiem sa obiekty interakcyjne, gracze. Kiedy uaktualniasz (update) pokoj, skutecznie go niszczysz. Jesli sa w nim jacys gracze, to zostana oni przemieszczeni do startowych lokacji. Jesli dany pokoj *jest* startowa lokacja lub jest jakis problem z przemieszczaniem ich tam (bledny kod lokacji, albo niemoznosc przemieszczenia), to ci gracze zostana rowniez usunieci. 2.3.3 - Obsluga polecen przez obiekty ------------------------------------- [init, add_action, enable_commands, disable_commands, living, command, commands, get_localcmd, query_verb, notify_fail, update_actions] Jak na razie wiesz na pewno, ze nic w tej grze nie jest proste. Aby pogmatwac wydawanie polecen, mamy ich dwa rodzaje. Jednym z nich jest taki, o ktorym juz wczesniej mowilismy, czyli polecenia definiowane przez 'fizyczne' obiekty. Drugim rodzajem sa komendy twz. soul (duszowe?). Sa one wymyslem czysto mudlibowym. Bedzie o nich mowa w trzecim rozdziale. Polecenia dodane przez fizyczne obiekty dzialaja w ten sposob: Po wejsciu takiego obiektu w kontakt z innym obiektem, np. graczem (czyli: albo 1:obiekt wchodzi w ekwipunek ("zawartosc" obiektu) gracza, albo 2:gracz i obiekt znajduja sie we wspolnym srodowisku np. w tym samym pokoju, albo 3:gracz wchodzi w "zawartosc" obiektu np do pokoju), w obiekcie ktory wszedl wolana jest lfunkcja `init()'. Jest ona zwykla funkcja, taka jak kazda inna, lecz *przeznaczona* jest do dodawania polecen poprzez efunkcje `add_action()'. Innymi slowy, gdy wchodzisz do jakiegos obiektu, do listy twoich polecen zostaja dodane polecenia z nowego srodowiska, z twojego ekwipunku oraz polecenia innych obiektow bedacych w tym samym srodowisku co ty. Funkcja `add_action()' przypisuje polecenie jakiejs funkcji. Po wpisaniu slowa-polecenia, zostaje wykonana polaczona funkcja, z argumentem stanowiacym reszte wpisanego polecenia (np. gdyby cale polecenie brzmialo 'zabij zielonego smoka', to slowem-poleceniem byloby 'zabij', a argumentem 'zielonego smoka'). Oprocz poprzednich dwoch argumentow(tj. funkcja oraz slowo-polecenie), add_action() moze przyjac rowniez trzeci. Jesli rowna sie on 1, to przypisana funkcja bedzie wykonana, nawet gdy komenda bedzie stanowila tylko czesc zdefiniowanego w add_action() polecenia. Na przyklad, jesli zdefiniowane polecenie brzmialoby 'zabij", to wywolanie przypisanej funkcji mogloby byc dokonane juz przez komende "za" albo "zab". add_action(function funkcja, string slowo_polecenie, void|int 1) np. init() { /* * Funkcje 'zrob_uklon()' i 'wyjdz_z_gry()' sa zdefiniowane * gdzies w tym obiekcie. Jednakze, jesli ich deklaracje * znajduja sie za ta funkcja, to w naglowku programu musza * byc ich prototypy. */ add_action(zrob_uklon, "uklon"); // Przyzwyczaj sie lepiej add_action(&wyjdz_z_gry(), "quit"); // do roznych rodzajow // deklaracji odnosnikow // do funkcji. } Czy jest to prawda dla kazdego rodzaju obiektu? Czy kazdy obiekt otrzyma ten zestaw komend? Ano nie. Tylko "zyjace" obiekty. Obiekt moze zostac uczyniony "zyjacym" poprzez efunkcje `enable_commands()', a martwym badz bezwladnym poprzez efunkcje `disable_commands()'. Zauwaz, ze "zyjacy" obiekt dla gamedrivera oznacza "taki, ktory moze przyjmowac i wydawac polecenia" - dla mudliba znaczy to troche wiecej. Mozesz uzywac tych efunkcji za kazdym razem, gdy zechcesz wlaczyc lub wylaczyc zdolnosc to obslugiwania polecen w obiekcie. Pamietaj tylko o tym, ze gdy "ozywisz" obiekt, to nie przybedzie mu komend z racji tego, ze znajduje sie np w pokoju ktory dodaje polecenia. Sa one dodawane tylko przy wchodzeniu do srodowiska pokoju. Zeby je uzyskal bedziesz musial wiec przeniesc go z, a potem spowrotem do pomieszczenia. Mozesz sprawdzic, czy obiekt jest "zyjacy" przy pomocy efunkcji `living()'. enable_commands() disable_commands() int living(object ob) np. public void zmien_stan_ozywienia() { if (living(this_object())) disable_commands(); else enable_commands(); } Polecenia moga byc dodawane i obslugiwane tylko przez obiekty, ktore sie znajduja w srodowisku lub zawartosci danego obiektu. Kiedy jest on usuwany z zasiegu obiektow dodajacych polecenia, mozliwosc ich wykonania jest rowniez usuwana. Jak widzisz, gamedriver oczekuje, ze funkcja `init()' jest zdefiniowana w kazdym obiekcie, ktory chce dodac polecenia zyjacym obiektom. Badz jednak ostrozny w czasie uzywania tej funkcji. Jesli na przyklad uzywasz `init()' do sprawdzania, czy wchodzacy obiekt ma prawo sie tam znalezc i przenosi go gdzies jesli nie ma prawa, to najprawdopodobniej bedziesz mial klopoty. Bedzie tak dlatego, ze gdy sprobujesz dodac polecenia juz po przesunieciu, to w rzeczywistosci bedziesz to robil na nieobecnym obiekcie. Gamedriver spostrzeze to i skonczy sie to bledem. Chcialbym ci doradzic, bys *nie* uzywal funkcji `init()' do zadnych innych celow, niz do dodawania polecen. Mozesz ewentualnie sprawdzac czy obiekt, ktory wywolal `init()' powinien otrzymac polecenia, czy nie (jesli chcesz ograniczyc dostep do niektorych polecen), ale nie wrzucaj tam niczego innego. W wiekszosci obiektow, ktore dziedzicza posrednio lub bezposrednio podstawowy obiekt, musisz wywolac takze rodzica `init()', gdyz bez tego twoj init() moze stracic kilka bardzo waznych rzeczy. Po prostu umieszczaj instrukcje `::init();' na poczatku kazdego swojego init'a, jeszcze przed instrukcjami `add_action()', a wszystko bedzie w porzadku. Aby wykonac jakies polecenie w zyjacym obiekcie, uzyj efunkcji `command()'. int command(string komenda) np. command("kichnij"); command("zaloz helm"); Aby otrzymac liste dostepnych polecen, uzywa sie efukcji `commands()' lub `get_localcmd()' w zaleznosci od tego, jakiego typu informacji sie potrzebuje. `commands()' zwraca tablice tablic, zawierajacych wszystkie komendy dostepne dla okreslonego obiektu plus te, ktore obiekt sam definiuje wraz z funkcjami, ktore sa w razie ich wykonania wywolywane. `get_localcmd()' jest prostsza i zwraca tylko tablice z slowami-poleceniami. Gdy zaden obiekt nie jest wyszczegolniony, uzywa sie jako standardowej wartosci `this_object()'. Obejrzyj manuala dla `commands()' by dowiedziec sie jaki format ma zwracana tablica. mixed commands(void|object ob) string *get_localcmd(void|object ob) Gdy uzywasz jednej funkcji do wielu slow-polecen, zachodzi potrzeba zbadania, ktore konkretnie zostalo uzyte. Do tego uzywa sie efunkcji `query_verb()'. string query_verb() np. init() { ::init(); add_action(&moja_funkc(), "apa"); // wszystkie trzy add_action(moja_funkc, "bepa"); // polecenia wywoluja add_action(&moja_funkc(), "cepa"); // te same funkcje... } public int moja_funkc() { switch (query_verb()) // ...ale kazde polecenie jest w niej { // rozpatrywane oddzielnie case "apa": < kod > break; case "bepa": < kod > break; case "cepa": < kod > break; } return 1; } Funkcje obslugujace polecenia powinny zwracac 1, gdy zostaly poprawnie uzyte, tzn kiedy zostala wywolana wlasciwa funkcja, z wlasciwym argumentem. Kiedy zwrocisz 0, gamedriver bedzie szukal innych funkcji przypisanych do identycznego slowa-polecenia, do czasu az ktoras z nich zwroci 1, albo nie bedzie juz zadnych innych. Jest specjalna efunkcja zwana `notify_fail()', ktorej mozesz uzyc do przechowania komunikatu bledu, ktory zostanie wyswietlony gdy zadna z funkcji nie 'przygarnie' polecenia. Zamiast standardowego, bezsensownego komunikatu bledu `What?', mozesz dac graczowi wydajacemu polecenie jakas lepsza informacje. W przypadku gdy wszystkie funkcje powiazane z identycznym slowem poleceniem zwroca 0, to *ostatnio* zdefiniowany `notify_fail()' zostanie uzyty do wyswietlenia komunikatu bledu. notify_fail(string komunikat) np. public void init() { ::init(); add_action(&zrob_uklon(), "uklon"); } public int zrob_uklon(string komu) { if (!find_player(komu)) { notify_fail("Nie tu nikogo takiego!\n"); return 0; } < kod uklonu > return 1; } Jesli jestes absolutnie pewien, ze polecenie odnosilo sie do twojego obiektu i chcesz przerwac wykonanie nawet jesli twoj obiekt znalazl w nim blad (zle argumenty, kontekst, lub cokolwiek innego), to mozesz zwrocic 1. Pamietaj jednak, ze wtedy musisz uzyc innej metody wyswietlenia komunikatu bledu na ekranie niz `notify_fail()', gdyz ta funkcja ma szanse byc uzyta tylko gdy zwrocisz 0. Gdy twoj obiekt zmienia dostepne polecenia w czasie swojego dzialania i chcesz, by okoliczne zyjace obiekty uaktualnily swoj zestaw komend, musisz wywolac efunkcje `update_actions()' na swoim obiekcie. Jesli nie sprecyzujesz w argumencie zadnego obiektu, to domyslnie zostanie przyjety `this_object()'. Wszystkie okoliczne zyjace obiekty uniewaznia stary zestaw komend od twojego obiektu i wywolaja sobie jeszcze raz `init()' z niego, zeby pobrac nowy zestaw. update_actions(object ob) 2.3.4 - Alarmy: Niesynchroniczne wywolania funkcji -------------------------------------------------- [set_alarm, remove_alarm, get_alarm, get_all_alarms] Gamedriver oblicza tak zwany "evaluation cost", albo prosciej "eval cost" (koszt wykonania). Jest to zwykly sposob mierzenia, jak bardzo obiekt obciaza CPU. Kazdy obiekt ma nadany limit kosztu wykonania. Kiedy jest on wyczerpany, wykonanie obiektu zostaje przerwane. Ograniczenie to zostalo narzucane, by gra nie byla za dlugo przeciazana. Istnienie tego pociaga za soba jednak pewne konsekwencje. Jakies duze obliczenia moga sie po prostu nie zmiescic w limicie eval cost'u, wiec w takiej sytuacji potrzeba rozbic program na mniejsze czesci. Czasem przydaje sie opoznic wykonanie jakiejs funkcji, a czasem warto, by funkcja byla wykonywana cyklicznie. Pozwala na to wykorzystanie alarmow. Ustawia sie je efunkcja `set_alarm()'. int set_alarm(float opoznienie, float cykl, function funkcja_alarm) remove_alarm(int alarm_id) mixed get_alarm(int alarm_id) mixed get_all_alarms() Funkcja zwraca unikalny dla tego obiektu numer alarmu, ktory mozesz pozniej wykorzystywac do manipulowania nim. Informacje o danym alarmie uzyskuje sie wywolujac funkcje `get_alarm()', jako argument podajac numer alarmu. Do usuwania ich sluzy efunkcja `remove_alarm()', a do zdobycia informacji o wszystkich alarmach sluzy efunkcja `get_all_alarms()'. Ta ostatnia jest przewaznie wykorzystywana w sytuacjach, gdy zapomni sie przechowac gdzies identyfikator alarmu, albo chce sie wyswietlic informacje o obiekcie. Efunkcja `set_alarm()' pozwala na ustalenie opoznienia, po jakim podana funkcja zostanie wykonana po raz pierwszy, oraz opoznienia pomiedzy kolejnymi cyklicznymi wywolaniami. Kazde wywolanie alarmu zaczyna sie z eval cost'em rownym 0. Poniewaz funkcje sa wywolywanie przez alarm niesynchronicznie wzgledem uzytkownika obiektu, `this_player()' i `this_interactive()' zwroca 0. Pamietaj o tym, gdyz niektore efunkcje korzystaja z wartosci zwracanej przez `this_player()'. *UWAGA! PRZECZYTAJ TO UWAZNIE!* Bardzo latwo wpasc w nawyk dzielenia programu na wiele 'alarmowych' wywolan w malych odstepach czasowych. Jednakze *NIE* do tego sluza alarmy. Smiertelnym grzechem jest robienie funkcji alarmowej, ktora tworzy powtarzajace sie alarmy wewnatrz powtarzajacych sie alarmow. Ilosc alarmow wtedy gwaltownie wzrasta i *CALA GRA* momentalnie staje. Jest to tak nieprawdopodobnie kretynskie, ze grozi natychmiastowym demotem, wiec upewnij sie, ze wszytko jest *W PORZADKU* za pierwszym razem. Ogolnie rzecz biorac, odstepy pomiedzy cyklicznymi alarmami, powinny byc dluzsze niz jedna dwie sekundy, tak samo jak odstepy przed pojedynczymi wywolaniami. Funkcje alarmowe beda szerzej opisane i zademonstrowane w trzecim rozdziale. 2.3.5 - Srodowisko i zawartosc ------------------------------ [move_object, move, enter_inv, enter_env, leave_inv, leave_env, environment, all_inventory, deep_inventory, id, present] Jak juz wczesniej mowielem, kazdy obiekt ma zarowno "wnetrze", jak i "otoczenie". Otoczenie, lub inaczej mowiac "srodowisko" moze byc tylko jednym obiektem, podczas gdy we wnetrzu, w "zawartosci" moze sie znajdowac wiele obiektow. Swiezo sklonowany obiekt znajduje sie w swego rodzaju pustce, gdyz nie ma zadnego srodowiska. Zeby mogl znalezc sie w fizycznym swiecie gry, musi byc tam przesuniety. Jednakze, nie wszystkie obiekty moga byc przemieszczane. Zeby *DOWOLNY* obiekt, ktory chce byc gdzies umieszczony, albo chce samemu zawierac jakies inne obiekty dzialal, *MUSI* dziedziczyc `/std/object.c' gdzies w swoim lancuchu dziedziczen. Po co to ograniczenie? Dlatego, ze standardowy obiekt definiuje spora liczbe pomocnych funkcji i inne obiekty polegaja na tym, ze beda one w twoim obiekcie. Oto najwazniejsze sposrod nich: `move()' Przemieszcza obiekt do innego, obslugujac rachunek wagi/objetosci. Zwraca, czy przesuniecie powiodlo sie. Jest odpowiedzialna za wywolywanie nastepujacych funkcji: `enter_inv()' Wywolywana jest w obiekcie, gdy inny obiekt wchodzi w jego zawartosc. `leave_inv()' Wywolywana w obiekcie, gdy inny obiekt wchodzi w jego zawartosc. `enter_env()' (!!!) `leave_env()' (!!!) *UWAGA!* Powyzsze funkcje beda wywolane *TYLKO* wtedy gdy to wlasnie `move()' zostanie uzyty do przesuniecia. Dlatego tak wazne jest to, zebys przemieszczal w ten sposob, a nie poprzez efunkcje, ktora robi to bezposrednio. Lfunkcja `move()' korzysta z efunkcji `move_object()'. *ALE* pamietaj, gdy odwolasz sie bezposrednio do tej drugiej, to stany obiektu takie jak oswietlenie, waga, czy objetosc nie zostana uaktualnione. Jak juz poprzednio mowilem, fiaskiem skonczy sie proba przeniesienia obiektu do innego przy pomocy `move_object()', gdy ktorys z nich nie dziedziczy `/std/object.c'. Dodatkowo, efunkcja moze byc wywolana tylko z obiektu, ktory chce byc przesuniety. To samo dotyczy oczywiscie lfunkcji `move()'. W celu uzyskania odnosnika do obiektu srodowiska, uzywa sie efunkcji `environment()'. Jak juz wczesniej powiedzialem, zaden obiekt nie ma zdefiniowanego srodowiska zaraz po utworzeniu - otrzymuje je dopiero, gdy jest gdzies przenoszony. Kiedy obiekt chociaz raz opusci 'pustke', w ktorej sie znajduje na poczatku, to juz nigdy nie moze tam wrocic, tzn. nie mozna przeniesc obiektu do '0'. Obiektami, po ktorych mozesz sie spodziewac, ze nie beda mialy zadnego srodowiska sa pokoje, dusze, cienie i obiekty daemon. Masz do wyboru dwie funkcje, sposrod ktorych mozesz wybierac, gdy chcesz zdobyc sklad obiektu. Efunkcja `all_inventory()' zwraca tablice ze wszystkimi obiektami bedacymi zawartoscia podanego obiektu. Efunkcja `deep_inventory()' zwraca tablice, zawierajaca nie tylko to, co `all_inventry()', ale rowniez obiekty, ktore sa we wnetrzu obiektow, ktore sa we wnetrzu podanego obiektu, itd. object *all_inventory(object ob) object *deep_inventory(object ob) np. /* * Funkcja wyswietla ekwpipunek Fatty na ekranie. Od argumentu bedzie * zalezalo, czy bedzie to tylko ekwipunek widoczny na pierwszy rzut * oka, czy caly. */ void fatty_powiedz_aaaaa(int wszystko) { object fatty_ob, *listaob; if (!objectp((fatty_ob = find_player("fatty")))) { write("Przykro mi, Fatty nie ma dzis w grze.\n"); return 0; } listaob = wszystko ? deep_inventory(fatty_ob) : all_inventory(fatty_ob); write("Oto " + (wszystko ? "cala " : "") + " zawartosc wielkiego brzucha Fatty:\n"); dump_array(listaob); } Co powiesz na sprawdzenie, czy dany obiekt jest obecny w zawartosci innego? Bazowy obiekt `/std/object.c' definiuje zarowno nazwe jak i opis w obiektach. Rowniez w nim znajduje sie lfunkcja `id()', ktora sprawdza czy podany argument jest jedna z nazw danego obiektu. Jesli jest to zwraca 1(prawde). Efunkcja `present()' przeszukuje zawartosci obiektow, sprawdzajac czy jest tam obiekt o podanym odnosniku lub nazwie. Gdy w argumencie wpiszesz to drugie, to `present()' skorzysta z poprzednio omowionej funkcji `id()' do sprawdzenia, czy w przeszukiwanym aktualnie obiekcie istnieje takowa nazwa. Wykonanie funkcji skonczy sie tak szybko, jak szybko znajdzie ona pierwszy pasujacy do opisu obiekt. Oznacza to, ze jesli jest wiecej takich obiektow, to funkcja zwroci ci tylko jeden z nich. object present(object ob|string odnob, object *listaob|object ob|void) np. /* * Szuka orzeszkow u Fatty */ void znajdz_orzeszek() { object fatty_ob; fatty_ob = find_player("fatty"); // Nie mozna znalezc Fatty! if (!objectp(fatty_ob)) { write("Fatty chwilowo nie ma, sprobuj pozniej.\n"); return; } if (present("orzeszek", fatty_ob)) write("Tak, Fatty wydaje sie byc bardzo zadowolony z zycia.\n"); else write("Na twoim miejscu trzymalbym sie z dala od " + "Fatty, dopoki nie zaspokoi glodu.\n"); } Jesli nie podasz drugiego argumentu w `present()', to funkcja poszuka obiektu w zawartosci `this_object()', czyli tego obiektu, z ktorego zostala wywolana. Gdy w drugim argumencie podasz tablice, to funkcja przeszuka wszystkie obiekty z listy. Jesli nie znajdzie niczego, to zwroci 0. 2.3.6 - Funkcje obslugujace lancuchy znakowe -------------------------------------------- [break_string, capitalize, lower_case, sprintf, strlen, wildmatch] W srodowisku gry opartym na tekscie, naturalne jest to, ze tworcy zadali sobie troche trudu w stworzeniu latwych i wszechstronnych funkcji obslugujacych lancuchy znakowe. Jak juz wiesz, stringi mozna sumowac przy pomocy operatora `+', a nawet laczyc je z liczbami calkowitymi bez zadnych klopotow. Floaty i wskazniki do obiektow musza juz jednak byc konwertowane. Te pierwsze przy pomocy efunkcji `ftoa()' (opisanej pozniej), a te drugie poprzez juz opisana efunkcje `file_name()'. Jedna z najczesciej sprawdzana rzecza w stringach, poza tym co zawieraja, jest ich dlugosc. Uzyskuje sie ja przy pomocy efunkcji `strlen()'. Jako argument mozna podac rowniez liczby calkowite (zwroci wtedy 0), dzieki czemu mozna ja wykorzystywac takze do sprawdzania czy zmienna typu string zostala juz zainicjalizowana. int strlen(string str) np. string str = "Fatty jest spasionym, zatwardzialym szowinista"; write("Dlugosc stringa '" + str + "' wynosi " + strlen(str) + " znakow.\n"); Nieraz zachodzi potrzeba takiego przeformatowania stringa, by zaczynal sie z duzej litery. Sluzy do tego efunkcja `capitalize()'. Oprocz tego istnieje efunkcja `lower_case()', ktora zamienia wszystkie litery w podanym stringu na male. string capitalize(string str) string lower_case(string str) np. void // Wyswietli podane imie, odpowiednio sformatowane. wyswietl_ladne_imie(string imie) { string nowe_imie; // Zalozmy, ze imie = "fAttY" nowe_imie = lower_case(imie); // Teraz imie = "fatty" nowe_imie = captialize(imie); write("Imie brzmi: " + imie + "\n"); /* Efektem jest: Imie brzmi: Fatty */ } Czasem przydaloby sie polamac stringa na mniejsze kawalki(np dlugosci linijki ekranu), by ladniej wygladal i nadawal sie do wyswietlenia. Sluzy do tego efunkcja `break_string()'. Dzieki niej mozesz nawet dodac spacje na poczatku polamanych lancuchow znakowych. Jej dzialanie polega na wstawianiu znaku nowej linii po odpowiedniej ilosci slow, tak by jedna czesc zmiescila sie w podanym limicie znakow. Trzeci argument mowiacy ile ma byc spacji wciecia, albo podajacy string wstawiany na poczatku kazdego fragmentu jest opcjonalny. string break_string(string str, int dlug_lam, int dlugosc_wciecia|string wstawiany_string|void) np. string str = "To jest string, ktory chce przedstawic na rozne " + "sposoby."; write(break_string(str, 26) + "\n"); write(break_string(str, 26, 5) + "\n"); write(break_string(str, 26, "Fatty mowi: ") + "\n"); /* Efektem bedzie: To jest string, ktory chce przedstawic na rozne sposoby. To jest string, ktory chce przedstawic na rozne sposoby. Fatty mowi: To jest string, ktory chce Fatty mowi: przedstawic na rozne Fatty mowi: sposoby. */ Bardzo czesto bedziesz chcial przedstawic zawartosc zmiennej na ekranie. Jak juz pokazalem, mozesz to zrobic poprzez zamienienie wartosci zmiennej w stringa i wyswietlenie jej. Integerow nawet nie trzeba konwertowac - wystarczy, ze dodasz go przy pomocy operatora `+'. Otrzymasz jednak cos, co nie bedzie sformatowane i byc moze bedzie wymagalo jakis przerobek. Czasem mozesz chciec wyswietlic tresc zmiennych w postaci tabelki i bedziesz wtedy musial bawic sie w rozne duperele takie jak uzaleznianie ilosci spacji od dlugosci zmiennej itp. Zamiast tego, mozesz skorzystac z efunkcji `sprintf()'. `sprintf()' pobiera dwa stringi. Pierwszy to jest ten, ktory chcesz przeformatowac. Drugi zas zawiera wskazowki, wedlug ktorych formatowanie ma sie odbywac. Wynikiem jest gotowy lancuch znakow, ktory mozesz wyswietlic np. przy pomocy `write()'. Wszystkie znaki z drugiego stringa, poza specjalnymi zaczynajacymi sie od `%' beda przekopiowane do wynikowego stringa. Znaki kontrolne maja postac: "%". Szerokosc jest liczba calkowita, oznaczajaca dlugosc "okienka" w ktorym dane beda wyswietlane oraz czy dana ta ma byc rownana do lewej czy do prawej strony. Dodatni numer oznacza, ze do prawej, zas ujemny, ze do lewej. Jesli nie podasz szerokosci, to zmienna zostanie wlozona w okienko o dlugosci rownej dlugosci zmiennej. Wyznacznik typu sklada sie z jednej lub wiecej liter, okreslajacych jaki typ zmiennej bedzie tu uzyty. A oto lista wyznacznikow typu: `d' `i' Argument jest liczba calkowita. string str; int a; a = 7; str = sprintf("test: >%-3d%i<", 1, a); write(str + "\n"); // Efektem jest: // test: >1 7< `s' Argument jest lancuchem znakow. `c' Podany argument jest numerem ASCII, znaku ktory ma byc wyswietlony. `o' Liczba ma byc przedstawiona w systemie osemkowym. `x' Liczba ma byc przedstawiona w systemie szesnastkowym. `X' Liczba ma byc przedstawiona w systemie szesnastkowym (duzymi literami). `O' Argument jest typem danych LPC. Jest to swietna rzecz do wyszukiwania bledow (odpluskwiania), gdyz mozesz wyswietlic dzieki niej zawartosc DOWOLNEJ zmiennej. np. write(sprintf("1:%d 2:%s 3:%c 4:%o\n5:%x 6:%X 7:%O\n", 5, "hupp happ", 85, 584, 32434, 85852, strlen)); // Efektem bedzie: // 1:5 2:hupp happ 3:U 4:1110 // 5:7eb2 6:14F5C 7:<> Specyfikator ten jest jak na razie jedynym, w ktorym da sie wyswietlic floaty. To byla lista wszystkich wyznacznikow typow. Do wyznacznikow szerokosci mozna jeszcze dodac te elementy: `' '' Liczbowy argument zostanie poprzedzony jedna spacja, o ile jest dodatni. Pozwala to na robienie fajnych tabel bez zawracania sobie glowy tym, ze liczby z minusem zajmuja o jeden znak wiecej. `+' Dodatnie argumenty liczbowe zostana poprzedzone plusem. `'X'' Znak(i) w apostrofach bedzie poprzedzal argument. `|' Argument zostanie wycentrowany w okienku. write((sprintf(">%19|s<\n", "Fatty grubas"))); // Efektem bedzie: // > Fatty grubas < `#' Oznacza to tryb tablicowy. Efektem bedzie lista slow oddzielonych `\n' w tabeli o szerokosci okienka. Oczywiscie mozna uzyc tego tylko do stringow. `=' Ten wyznacznik jest poprawny tylko dla stringow. Wyswietla rezultat w kolumnach, o ile argument jest szerszy od okienka. `*' Argument obok gwiazdki jest rozmiarem okienka. Jesli polaczysz to z trybem tablicowym, to otrzymasz fajne tabelki. `@' Argumentem jest tablica. Oczywiscie musisz polaczyc to z wyznacznikiem typu, zaznaczajac typ elementow. Bardzo czesto zachodzi potrzeba sprawdzenia, czy dany string jest czescia jakiego innego. Jesli nie jestes zainteresowany informacja gdzie on dokladnie wystapil, a tylko czy wogole, to efunkcja `wildmatch()' bedzie czyms w sam raz dla ciebie. Po prostu zwraca 1, jesli podany string wystapil gdzies, w jakims innym, wiekszym stringu. Mniejszy string moze skladac sie tez z prostych symboli-masek. * Porownuje dowolna liczbe dowolnych znakow ? Porownuje jeden dowolny znak [xyz] Porownuje dowolne znaki sposrod tych w nawiasach kwadratowych [^xyz] Porownuje dowolne znaki nie bedace pomiedzy nawiasami kwadratowymi \c Porownuje c, nawet jesli jest to znak specjalny int wildmatch(string matryca, string str); np. // Cokolwiek, co sie konczy na .foo wildmatch("*.foo", "bar.foo") == 1 // Cokolwiek zaczynajacego sie od a, b lub c i zawierajacego // conajmniej jeden znak wiecej wildmatch("[abc]?*", "axy") == 1 wildmatch("[abc]?*", "dxy") == 0 wildmatch("[abc]?*", "a") == 0 2.3.7 - Funkcje obslugujace znaczniki bitowe -------------------------------------------- [clear_bit, set_bit, test_bit] Nieraz zachodzi potrzeba przechowania sporej liczby informacji typu 'tak/nie'. Bardzo prostym i przy okazji niezbyt dobrym sposobem byloby stworzenie sporej liczby integerow, po jednym na kazda informacje i wstawianie w nie 0 albo 1, zeby przedstawic jakis stan. Daje to latwy dostep i jest zrozumiale, ale jak przychodzi do przechowania wiekszej ilosci informacji, to sie zaczynaja problemy z straszna pamieciozernoscia tej metody. Zamiast tego, mozna uzywac stringow, gdzie kazdy bit w znaku (jest ich 8 na znak) moze przechowywac informacje typu tak/nie. Maksymalna liczba bitow w lancuchu wynosi okolo 1200 = dlugosc stringa okolo 150 znakow. Choc raczej watpie, ze wykorzystasz je wszystkie. Poszczegolne bity ustawia sie przy pomocy efunkcji `set_bit()', ktora wymaga dwoch argumentow. Pierwszym jest zmienna typu string, w ktorej bit ma byc ustawiony, drugim zas numer bitu, ktory chcesz wlaczyc. `clear_bit()' dziala analogicznie do `set_bit()', tylko ze zeruje(wylacza) podany bit. Jesli chcesz sprawdzic jaka wartosc zawiera dany bit, to powinienes uzyc efunkcji `test_bit()'. Nie musisz inicjalizowac stringow, ktore chcesz wykorzystac do przechowywania bitow. Zarowno `set_bit()' jak i `clear_bit()' zwracaja zmodyfikowany string, a w przypadku gdy nie jest on wystarczajaco szeroki to zostanie rozszerzony przez `set_bit()'. `clear_bit()' jednakze nie skroci stringa. string set_bit(string bitstr, int numer_bitu) string clear_bit(string bitstr, int numer_bitu) int test_bit(string bitstr, int numer_bitu) np. // Wlacza 23 bit string bf; bf = ""; bf = set_bit(bf, 22); // Zeruje 93 bit bf = clear_bit(bf, 92); // Sprawdza 3 bit if (test_bit(bf, 2)) write("Wlaczony!\n"); else write("Wyzerowany!\n"); 2.3.8 - Funkcje obslugujace czas -------------------------------- [time, ctime, file_time, last_reference_time, object_time] Z jakiejs nieznanej przyczyny wszystkie pomiary czasu w UNIXe, a co za tym idzie w mudzie, zaczynaja sie od 1 stycznia 1970 roku. Byc moze tworcy tego systemu wymyslili sobie, ze z komputerowego punktu widzenia nie ma powodu, by chciec ustawic jakas wczesniejsza date. W kazdym razie tak jest i nic na to nie mozna poradzic. Mierniki czasu sa integerami i zliczaja czas od wyzej wymienionej daty w sekundkach. Efunkcja `time()' zwraca aktualny czas. Mozesz wykorzystac ja w tej postaci, albo przekonwertowac zwracana wartosc w jakis zrozumialy string przy pomocy efunkcji `ctime()'. W celu otrzymania czasu, w ktorym plik zostal utworzony, uzywa sie efunkcji `file_time()'. Istnieje analogiczna efunkcja odnoszaca sie do obiektow - `object_time()'. Warto czasem wiedziec kiedy ostatni raz obiekt byl uzywany, tzn. kiedy ostatni raz byla wywolana w nim jakas funkcja. Jesli jako pierwsza instrukcje wywolasz `last_reference_time()' to otrzymasz ten czas. Pamietaj jednakze, ze po wykonaniu tej funkcji, czas ostatniego wywolania przyjmie wartosc czasu biezacego. int time() string ctime(int tm) int file_time(string obref) int object_time(object ob) np. // last_reference_time() wywolujemy jako pierwsze write("Ten obiekt ostatni raz byl uzyty " + ctime(last_reference_time()) + "\n"); write("Aktualny czas: " + ctime(time()) + ".\n"); write("Ten obiekt istnieje juz " + (time() - object_time(this_object())) + " sekund.\n"); 2.3.9 - Funkcje obslugujace konwersje tablice-stringi ----------------------------------------------------- [explode, implode] Istnieje mozliwosc podzielenia stringa na mniejsze kawalki na podstawie jakiegos innego lancucha, albo sklejenia roznych stringow umieszczonych w tablicy w jeden. Do tego celu sluza efunkcje `explode()' i `implode()'. Efunkcja `explode()' wymaga dwoch argumentow: pierwszym jest string, ktory chce sie podzielic, drugim zas jakis inny string, ktorego `explode()' szuka w wiekszym jako znacznika, gdzie go podzielic (np. explode(jakis_tekst, " ") zwroci jakis_tekst podzielony na slowa; znacznikiem dzielacym jest tutaj spacja). Efunckja zwraca tablice skladajaca sie z podzielonych stringow. `implode()' jako argumentow wymaga tablicy i stringa, a zwraca string skladajacy sie z posklejanych elementow tablicy, polaczonych stringiem z drugiego argumentu. string *explode(string str, string matryca) string implode(string *lista_str, string str_laczacy) np. string owoce = "jablko i banan i ananas " + "i pomarancz i fatty ktory je to wszystko"; string *lista_owocow; lista_owocow = explode(owoce, " i "); dump_array(lista_owocow); /* Efektem bedzie: (Array) [0] = (string) "jablko" [1] = (string) "banan" [2] = (string) "ananas" [3] = (string) "pomarancz" [4] = (string) "fatty ktory je to wszystko" */ owoce = implode(lista_owocow, ", "); write(owoce + "\n"); // Efektem bedzie: // jablko, banan, ananas, pomarancz, fatty ktory je to wszystko 2.3.10 - Funkcje oblsugujace tablice ------------------------------------ [allocate, member_array, sizeof, pointerp] Zaczne od malej powtorki z tablic. Moga one zawierac dowolne typy danych, wlaczajac w to inne tablice. Pamietaj o tym, ze tablice w przeciwienstwie do typow danych (poza mappingami) sa kopiowane poprzez odnosnik, a nie poprzez wartosc. Oznacza to, ze gdy przypisujesz tablice zmiennej, *nie* kopiujesz jej, a jedynie przechowujesz odnosnik, wskaznik do tablicy w zmiennej. np. string *arr1, *arr2; arr1 = ({ 1, 2, 3 }); arr2 = arr1; arr2[1] = 5; dump_array(arr1); /* * Efektem jest: * * (Array) * [0] = (int) 1 * [1] = (int) 5 * [2] = (int) 3 */ A wiec jak widzisz, zmiana zawartosci tablicy `arr2' zmienia rowniez zawartosc tablicy `arr1'. Zeby uczynic ja unikalna, musisz wpierw wykonac kopie `arr1', na przyklad poprzez dodanie do niej pustej tablicy `({ })'. Jak juz wiesz, tablice beda automatycznie zaalokowane poprzez zwykle wpisanie czegos w nie badz poprzez dodanie elementu lub innej tablicy. Jesli jednak chcesz natychmiast zaalokowac tablice do odpowiedniego rozmiaru to mozesz uzyc efunkcji `allocate()'. Rozmiar podaje sie jako jedyny argument. Funkcja zainicjalizuje podana liczbe elementow i ustawi je wszystkie na 0, niezaleznie od typu tablicy. mixed *allocate(int rozmiar) np. string *str_tabl; str_tabl = allocate(3); str_tabl[1] = "Fatty jest sflaczalym szowinista"; dump_array(str_tabl); /* Efektem jest: (Array) [0] = (int) 0 [1] = (string) "Fatty jest sflaczalym szowinista" [2] = (int) 0 */ Jesli chcesz sprawdzic, czy dane wyrazenie jest elementem tablicy i jesli tak, to jaki jest indeks tego elementu, to mozesz skorzystac z efunkcji `member_array()', podajac jako argumenty tablice i szukany element. Funkcja zwroci numer indeksu, jesli znajdzie element, albo -1 gdy poszukiwania zakoncza sie niepowodzeniem. Jesli w tablicy bedzie wiecej odpowiadajacych elementow, to `member_array()' zwroci indeks pierwszego z nich. int member_array(mixed tablica, mixed element) np. int *tab = ({ 1, 55443, 123, -3, 5, 828, 120398, 5, 12 }); int indeks; // Wszystkie '5' zostana zastapione przez '33' while ((indeks = member_array(tab, 5)) >= 0) tab[indeks] = 33; Bardzo wazna efunkcja dotyczaca tablic jest `sizeof()'. Zwraca ona rozmiar podanej tabliy, tzn. liczbe elementow znajdujacych sie w niej. Czesto zachodzi potrzeba napisania petli oblatujacej wszystkie elementy tablicy, albo po prostu znalezienia indeksu ostatniego elementu i wtedy ta efunkcja sie bardzo przydaje. *UWAGA!* Indeks ostatniego elementu wynosi rozmiar_tablicy - 1 : (sizeof(tablica) - 1), gdyz numeracja indeksow zaczyna sie od 0. int sizeof(mixed tab) np. string *tab = ({ "Fatty", "szownita" }); write(implode(tab, " ") + " jest zle.\n"); tab[sizeof(tab) - 1] = "szowinista"; write(implode(tab, " ") + " jest poprawnie.\n"); Efunkcja `pointerp()' moze byc zastosowana do sprawdzenia, czy zmienna zawiera tablice (dowolnego typu), czy nie. Jest bardzo przydata, jesli masz doczynienia z funkcjami, ktore moga zwrocic 0 (wartosc NULL), gdy cos pojdzie nie tak jak powinno. int pointerp(mixed tab) np. string *tab; if (pointerp((tab = find_player("zdzichu")->pobierz_gildie()))) write("Zdzichu nalezy do: " + implode(tab, ", ") + ".\n"); else write("Zdzichu nie nalezy do zadnej gildii!.\n"); 2.3.11 - Funkcje obslugujace Mappingi ------------------------------------- [mkmapping, mappingp, m_sizeof, m_delete, m_indices, m_values, m_restore_object, m_save_object] Tak jak to wczesniej mowilem, mappingi sa listami indeksow powiazanych z wartoscami. Podajac indeks, otrzymujesz przypisana do niego wartosc. Zawartosc mappingow jest ulozona w specjalnie posortowany sposob, dzieki czemu dostep do nich jest bardzo szybki. Jednakze maja pewna wade - sa strasznie pamieciozerne i zuzywaja bardzo duzo miejsca w porownaniu do tablic. Jak sie alokuje mappingi? Jest to bardzo proste. Wystarczy, ze zadeklarujesz go, a potem przypiszesz jedna wartosc do indeksu. Jesli indeks bedzie juz istnial, wartosc przy nim zostanie zastapiona nowa. A jesli nie, to zostanie dodana nowa para. Do tworzenia mappingow mozna tez uzyc efunkcji `mkmapping()' i jako argumenty podac dwie tablice, jedna z samymi indeksami, a druga z wartosciami. Pamietaj tylko, ze *musza* miec one taki sam rozmiar. mapping mkmapping(mixed tab_ind, mixed tab_war) np. string *ind_tab, *wart_tab; mapping mp; mp["lewy"] = "wielki"; mp["alvin"] = "straszny"; mp["fatty"] = "grubas"; // ... jest tym samym co ... ind_tab = ({ "lewy", "alvin", "fatty" }); wart_tab = ({ "wielki", "unikalny", "grubas" }); mp = mkmapping(ind_tab, wart_tab); // Mozesz oczywiscie podac te tablice bezposrednio, // bez poslugiwania sie zmiennymi Tak jak w tablicach, tu tez jest funkcja sprawdzajaca czy dana zmienna zawiera mapping, czy nie. Jest nia `mappingp()'. Uzywaj jej do tego samego celu, tzn. gdy jakas funkcja moze, ale nie musi zwracac mappingu, a ty chcesz miec pewnosc zanim zaczniesz indeksowac zwracana wartosc. Do znajdywania rozmiaru mappingu moze ci posluzyc efunkcja `m_sizeof()'. Dziala dokladnie tak samo, jak odpowiednik u tablic, zwracajac liczbe elementow (par) w mappingu. W mappingach usuwanie elementow juz nie jest takie proste, jak w tablicach. Sluzy do tego efunkcja `m_delete()'. Jej dzialanie nie polega na bezposrednim usunieciu elementu, tylko na stworzeniu nowego mappingu i przekopiowaniu tam zawartosci starego bez podanego elementu. mapping m_delete(mapping map, mixed elem) np. mapping mp, m_nowy; mp["lewy"] = "wielki"; mp["alvin"] = "straszny"; mp["fatty"] = "grubas"; m_nowy = m_delete(mp, "fatty"); dump_array(m_nowy); /* Wynik: * * (Mapping) ([ * "lewy":"wielki" * "alvin":"straszny" * ]) */ A jak zdobyc wszystkie elementy mappingu? Na przyklad chcielibysmy jakiejs odwrotnosci `mkmapping()'. Do tego sluza dwie funkcje: `m_indices()' oraz `m_values()', ktore zwracaja (kolejno) wszystkie indeksy oraz wszystkie wartosci danego mappingu. Doszlismy do dosyc niestabilnej kwestii - kolejnosci elementow w mappingach. Jak wczesniej mowilem, mappingi nie maja jej na stale zdefiniowanej. Tzn. maja, ale nie jest to temat ktorym powinienes sobie zawracac glowe. Zmienia sie ona gdy dodajesz, albo usuwasz jakas pare. W kazdym razie wazne jest to, ze gdy pobierzesz wszystkie indeksy i wartosci z jakiegos mappinga (za pomoca m_indices() i m_values()), to elementy otrzymanych tablic *beda* sobie odpowiadaly *o ile* pomiedzy pobieraniami nie wykonywales zadnych operacji na tym mappingu. mixed m_indices(mapping mapp); mixed m_values(mapping mapp); np. // Funkcja wyswietla mapping i jego zawartosc void dump_mapping(mapping mp) { int i, sz; mixed ind, war; ind = m_indices(mp); // Pomiedzy tymi dwoma intrukcjami, nie war = m_values(mp); // powinno byc zadnych dzialan na mappingu. sz = sizeof(ind); for (i = 0 ; i < sz ; i++) write(sprintf("%O", ind[i]) + " przypisane do " + sprintf("%O", war[i]) + "\n"); } /* Na przyklad uruchamiamy: dump_mapping(([ "fatty" : "grubas", * "lewy" : "wielki", * "alvin" : "straszny" * ])); * Otrzymujemy: * * "alvin" przypisane do "straszny" * "fatty" przypisane do "grubas" * "lewy" przypisane do "wielki" */ Sa, albo beda dwie funkcje, ktore zapisuja i odtwarzaja dane obiektu. Niestety, jak na razie maja one jeszcze bledy i nie dzialaja dokladnie tak, jakbysmy sobie tego zyczyli. Powinny one funkcjonowac w ten sposob: `m_save_object()' stworzy mapping, zawierajacy wszystkie globalne, nie-statyczne zmienne, ktorych nazwy beda indeksami. Bedziesz mogl wtedy zgrac go bezposrednio do pliku, albo przekazac go dalej, jako argument jakiejs funkcji. Odwrotnoscia tego bedzie funkcja `m_restore_object()'. Bedzie ona przyjmowala mapping jako argument, rozkladala jego elementy ustawiajac zmiennym globalnym odpowiadajace wartosci. 2.3.12 - Konwersja typow ------------------------ [atoi, atof, ftoa, itof, ftoi, str2val, val2str, sscanf] Wiekszosc rzeczy wpisywanych przez gracza to stringi; wpisujesz cos i gra powinna odpowiednio na to zareagowac. Wymaga to istnienia funkcji, ktore zanalizuja twoje komendy i przetlumacza to na wartosci, ktorych mozesz uzyc w swoich programach. Analiza skladni komend, jest lekko mowiac *bardzo* skomplikowana i zostawiam to na trzeci rozdzial. Teraz skupmy sie tylko na konwertowaniu samych wartosci. Jak mowilem, to co gracze wpisuja jest w formie stringow, przez co zamienianie stringow w integery oraz floaty i vice versa moze byc dosyc pozyteczna umiejetnoscia. Zacznijmy od integerow. Zalozmy, ze otrzymales string zawierajacy jakas wartosc numeryczna i chcesz jej uzyc do jakis obliczen. Musisz wiec przeksztalcic stringa w integera. Sluzy do tego efunkcja `atoi()'; podajesz string jako argument i otrzymujesz integer - bardzo proste. Lancuch nie moze jednak zawierac zadnych znakow innych niz cyfry. W przeciwnym wypadku funkcja zwroci 0. int atoi(string str) np. int war; war = atoi("23"); write("23 + 3 = " + (war + 3) + "\n"); Liczby zmiennopozycyjne (floaty) maja analogiczna efunkcje, `atof()', ktora przeksztalca stringa we floata. Jak juz wiesz, floaty nie moga byc przekonwertowane w stringi w ten sam sposob, jak integery, czyli poprzez dodanie ich do innego stringa. Funkcja `ftoa()' zamienia floata w stringa. Tak samo jak w przypadku `atoi()', jesli w `atof()' podasz jakis string, w ktorym beda znaki nie-numeryczne, to zwroci 0 (wyjatkiem beda specjalne znaki, takie jak np. `.'. Do zamiany pomiedzy integerem a floatem sluza efunkcje `itof()' oraz `ftoi()'. Pamietaj tylko, ze gdy zamieniasz floata w integera, to czesc dziesietna nie jest zaokraglana, tylko obcinana. Jest wiele momentow, w ktorych mozesz chciec przechowac wartosc w stringu, a potem przemienic ja spowrotem. Sluza do tego efunkcje `val2str()' oraz `str2val()'. Da sie wyswietlic zawartosc `val2str()', ale nie do tego zostala ona stworzona. Mozesz przechowac dowolna wartosc zmiennej, korzystajac z tych funkcji Najczesciej jednak uzywanym konwerterem danych jest efunkcja `sscanf()'. Mozesz sprecyzowac, gdzie ma ona szukac wartosci (podobnie jak w `sprintf()'), w celu pobrania ich i przechowania w jakiejs zmiennej. Jest ona charakterystyczna, gdyz ustawia wartosci zmiennych podanych w argumencie, wiec niemozliwe jest pobranie jej adresu. Poza tym, dziala ona bardzo prosto. Podaje sie matryce, string do przeszukania i zmienne, w ktorych dane powiny byc przechowane. Funkcja zwraca liczbe znalezionych wartosci. String, ktory podasz jako matryce, jest interpretowany doslownie, z wyjatkiem ponizszych lancuchow kontrolnych. `%d' porownuje liczbe calkowita. `%s' porownuje lancuch znakow. `%f' porownuje liczbe zmiennopozycyjna. `%%' porownuje znak `%'. int sscanf(string str, string matryca, ...); np. int szer; float waga; string orgstr; string typ_szer, typ_wagi; /* * Zalozmy, ze zadawane jest pytanie : * "Jak ci sie wydaje, jak ciezki i szeroki jest Fatty?". * Oprocz tego zalozmy, ze odpowiedz jest dana w formie * ' i ', na przyklad: * '4 metry i 3.2 tony'. Przyjmijmy, ze pierwsza wartosc moze byc * tylko integerem, a trzecia tylko floatem. * * Zalozmy jeszcze, ze odpowiedz jest podana w zmiennej `orgstr' */ if (sscanf(orgstr, "%d %s i %f %s", szer, typ_szer, waga, typ_wagi) != 4) { write("Podaj pelna odpowiedz!\n"); return; } write("Aha, uwazasz, ze Fatty jest szeroki na " + szer + " " + typ_szer + " i wazy " + ftoa(waga) + " " + typ_wagi ".\n"); 2.3.13 - Funkcje matematyczne ----------------------------- [random, rnd, sin, cos, tan, asin, acos, atan, atan2, exp, log, pow, sinh, cosh, tanh, asinh, acosh, atanh, abs, fact, sqrt] Efunkcja `random()' zwraca dowolna liczbe calkowita z zakresu od 0, do podanej w argumencie liczby minus jeden. Na przyklad `random(8)' zwroci losowa liczbe z zakresu 0 - 7. Reszta funkcji matematycznych pobiera jako argumenty i zwraca floaty. Funkcje trygonometryczne uzywaja radianow, nie stopni. Pamietaj o tym. Oto pelna lista funkcji matematycznych: `float rnd()' Zwraca losowa liczbe z zakresu od 0 do 1. `float sin(float)' Oblicza sinus podanego kata. `float cos(float)' Oblicza cosinus podanego kata. `float tan(float)' Oblicza tangens podanego kata. `float asin(float)' Oblicza arcus sinus z zakresu od -pi/2 do pi/2. `float acos(float)' Oblicza arcus cosinus z zakresu od 0 do pi. `float atan(float)' Oblicza arcus tangens z zakresu od -pi/2 do pi/2. `float atan2(float x, float y)' Compute the argument (phase) of a rectangular coordinate in the range -p to p. (!!!) - Hmm, ktos wie co to moze znaczyc? `float exp(float)' Wylicza funkcje w postaci e^x. `float log(float)' Oblicza logarytm naturalny. `float sinh(float)' Oblicza wartosc sinusa hiperbolicznego. `float cosh(float)' Oblicza wartosc cosinusa hiperbolicznego. `float tanh(float)' Oblicza wartosc tangensa hiperbolicznego. `float asinh(float)' Oblicza wartosc arcusa sinusa hiperbolicznego. `float acosh(float)' Oblicza wartosc arcus cosinusa hiperbolicznego. `float atanh(float)' Oblicza wartosc arcus tangensa hiperbolicznego. `float abs(float)' Oblicza wartosc absolutna argumentu (modul). `float fact(float)' Compute the factorial (gamma function) of the given argument. Oblicza silnie (funkcja gamma) podanego argumentu. `float sqrt(float)' Oblicza pierwiastek kwadratowy podanego argumentu. 2.3.14 - Obsluga plikow ----------------------- [save_object, restore_object, save_map, restore_map, write_bytes, read_bytes, write_file, read_file, file_size, file_time, rename, rm, ed] Przechowywania informacji w plikach jest bardzo wazne. Zaraz pokaze ci kilka funkcji, ktore ci w tym pomoga. Pozwol mi jednak strzelic male kazanie, na temat zuzycia CPU: Odczyt i zapis plikow jest sprawa bardzo obciazajaca CPU. Moze nie w porownaniu do innych procesow, ale podczas operacji dyskowej CPU nie moze robic nic innego. Innymi slowy, wczytywanie i zapisywanie duzych porcji danych znacznie spowalnia gre. Zeby ograniczyc zbyt duze obciazanie pamieci, dysku i CPU, narzucono limit nie pozwalajacy obluzyc wiecej niz 50 kb danych na raz. Pliki moga byc wieksze, ale ty nie mozesz zapisywac ni wczytywac fragmentow wiekszych niz ten limit. Oznacza to, ze musisz podzielic prace nad wiekszymi plikami na kawalki wykonywane sekwencyjnie, z ewentualna przerwa pomiedzy nimi, zeby reszta gry tez miala czas na zrobienie czegos. Wiec, prosze pamietaj o tym, ze to ograniczenie nie zostalo stworzone, by ci utrudnic zycie i zeby bylo obchodzone roznymi kretymi sposobami, tylko aby ci przypominac, ze obciazasz zasoby muda i ze powinienes innym tez dac zyc. Amen. Zacznijmy od bardzo prostej sprawy: nagrywania i odtwarzania obiektow. Zazwyczaj, chce sie przechowac zmienne globalne obiektu w pliku, po to by je pozniej odtworzyc. Do tego celu sluza efunkcje `save_object()' oraz `restore_object()'. W obu musisz podac sciezke wraz z nazwa pliku jako argument i oczywiscie miec prawa do zapisu lub do odczytu. Wynikiem `save_object()' bedzie plik, ktorego nazwa bedzie miala koncowke '.o'. Przy odtwarzaniu musisz pamietac o tym, by ja podac. Przy nagrywaniu to nie jest konieczne - funkcja sama doda rozszezenie '.o', gdy ty o tym zapomnisz. `restore_object()' zwraca 1, gdy odtwarzanie zakonczylo sie sukcesem, zas w przeciwnym wypadku zwraca 0. Zawartoscia nagranego pliku jest lista wszystkich zmiennych globalnych z ich wartosciami, oddzielonymi spacjami. Format zapisu przechowanej zmiennej jest taki sam, jak w przypadku juz omowionej funkcji `val2str()'. Mappingi sa najbardziej wygodnym typem do przechowywania zmiennych. Wystarczy umiescic dane w mappingu, gdzie indeksami sa nazwy zmiennych, a potem nagrac mapping przy pomocy efunkcji `save_map()', by pozniej odtworzyc go poprzez `restore_map()'. Przewaga tej metody nad `save/restore_object()' jest to, ze nie jestes ograniczony wylacznie do nie-statycznych zmiennych i mozesz przechowac co ci sie zywnie podoba. Minusem jest to, ze odtwarzanie danych z mappingu jest juz ciut bardziej skomplikowane. void save_object(string sciezka_nagr); int restore_object(string sciezka_odcz); void save_map(mapping mapp, string sciezka_nagr); mapping restore_map(string sciezka_odcz); np. /* * Zalozmy, ze mamy takie definicje zmiennych globalnych: * * string imie, *opis; * int flip; * mapping dane_map, smap; * * Zalozmy, ze interesuje nas przechowanie zmiennych imie, opis, * flip i dane_map */ // Ustawienie dziedzicznych przywilejow poprzez nadanie obiektowi // euid tworcy pliku setuid(); seteuid(getuid()); // Metoda nagrywania 1 save_object("mojplik"); // Metoda odtwarzania 1 if (restore_object("mojplik")) write("Tak!\n"); else write("Nieeee..\n"); // Metoda nagrywania 2 smap = ([ "imie" : imie, "opis" : opis, "flip" : flip, "dmap" : dane_map ]); save_map(smap, "mojplik"); // Metoda odtwarzania 2 smap = restore_map("mojplik"); if (m_sizeof(smap)) { imie = smap["imie"]; // Odtwarza imie opis = smap["opis"]; // Odtwarza opis flip = smap["flip"]; // Odtwarza flip dane_map = smap["dmap"]; // Odtwarza dane_map write("Tak!\n"); } else write("Nieeee..\n"); Musisz zapamietac, ze format zapisu uzywany przez `save_object()' i przez `save_map()' jest taki sam, dzieki czemu mozna odtworzyc plik zapisany przez `save_object()' przy pomocy `restore_map()' i wybrac z otrzymanego mappinga, tylko te elementy, na ktorych nam zalezy. Zalozmy, ze bys chcial odtworzyc tylko zmienna 'opis' w powyzszym przykladzie. Nie martwil bys sie wtedy innymi zmiennymi i uzylbys wtedy okrojonej drugiej metody odtwarzania. Uwazaj na odczytywanie danych, ktore zostaly nagrane przez `save_map()' instrukcja `restore_object()'. Zeby wszystko bylo poprawnie, indeksy nagranego mappingu musialy by miec takie same nazwy, jak zadeklarowane w obiekcie zmienne globalne. W powyzszym przykladzie sa roznice pomiedzy nimi, wiec odtworzenie metoda 1, danych zgranych metoda 2 skonczyloby sie bledem. W odwrotnej sytuacji, czyli przy wczytywaniu metoda 2 danych przechowanych metoda 1 nie wyskoczy zaden blad, ale nie uda sie odtworzyc zmiennej `dane_map'. To byly wszystkie metody przechowywania zmiennych w plikach. Nieraz moze zajsc potrzeba nagrania danych w wolnej formie, a nie tylko w zmiennych. Do tego celu sluza efunkcje `write_bytes()' i `read_bytes()', albo `write_file()' i `read_file()'. W zasadzie obie pary funkcji dzialaja bardzo podobnie, tzn wczytuja i nagrywana stringi o okreslonej dlugosci. Jedyna roznica jest to, ze `write_bytes()' moze byc uzyte do nagrywania na stare dane w pliku (overwriting), podczas gdy `write_file()' jest w stanie zapisywac tylko na koncu. No i `read_bytes()' operuje na bajtach, a `read_file()' na pelnych liniach. Obie zapisujace funkcje zwracaja 1 w przypadku powodzenia, zas 0 w gdy cos sie nie uda. Obie wczytujace funkcje zwracaja string, zawierajacy odczytana porcje danych w przypadku sukcesu, a 0 w przypadku niepowodzenia, wiec sprawdzaj rezultat przy pomocy `stringp()', zeby sie upewnic, ze wszystko bylo ok. int write_bytes(string sciezka, int poz, string tekst); string read_bytes(string sciezka, void|int poz, void|int num); int write_file(string sciezka, string tekst); string read_file(string sciezka, void|int poz, void|int num); O pliku rowniez da sie uzyskac informacje. W poznaniu rozmiaru pliku moze dopomoc efunkcja `file_size()'. Mozna ja tez wykorzystac do sprawdzenia obecnosci pliku. Jesli liczba zwrocona przez funkcje jest dodatnia, to oznacza to, ze jest to rozmiar. Gdy nie ma pliku, funkcja zwraca -1, a gdy plik jest katalogiem, -2. Aby poznac czas ostatniej modyfikacji mozna uzyc efunkcji `file_time()'. int file_size(string sciezka); np. void plik_info(string sciezka) { int typ, tm; rozm = file_size(sciezka); tm = file_time(sciezka); write("Plik '" + sciezka + "' "); switch (rozm) { case -1: write("nie istnieje.\n"); break; case -2: write("jest katalogiem, ostatni raz zmodyfikowanym " + ctime(tm) + ".\n"); break; default: write("ma rozmiar " + rozmiar + " bajtow, ostatni raz " + "byl zmodyfikowany " + ctime(tm) + ".\n"); break; } } Jesli chcesz zmienic nazwe albo przemiescic plik, to efunkcja `rename()' jest w sam raz dla ciebie. Ale uwazaj, gdyz jej dzialanie polega na skopiowaniu pliku, a potem skasowaniu starej wersji. Moze byc rowniez uzyta do przesuniecia katalogow. Jesli zyczysz sobie usunac caly plik, to mozesz skorzystac z efunkcji `rm()'. Zwraca ona 1 w przypadku sukcesu, a 0 w przypadku niepowodzenia. Uwazaj, bo `rename()' dziala dokladnie odwrotnie, gdyz zwraca 1 w przypadku *niepowodzenia*, a 0 jak wszystko jest ok. int rename(string stara_sciezka, string nowa_sciezka); int rm(string sciezka); np. if (rm("mojplik")) write("Ok, skasowane.\n"); else write("Sorki, cos nie idzie.\n"); if (rename("plik_stary", "plik_nowy")) write("Niestety, wciaz po staremu...\n"); else write("Ok!\n"); Wewnetrzny edytor 'ed' jest efunkcja, ktora operuje na plikach. Mozesz go uzywac do czegokolwiek, lecz pamietaj, ze wiekszosc ludzi nie wie jak z niego korzystac. Pamietaj takze, ze efunkcja `ed()' moze byc uzyta do stworzenia nowego pliku, albo edycji starego, na podstawie praw obiektu, ktory ja wywolal. Jesli podasz wskaznik do funkcji, to przy wyjsciu z `ed()' zostanie ona wywolana. Jesli nie podasz sciezki, uzytkownik bedzie musial sam wpisac sciezke i nazwe pliku wewnatrz edytora. void ed(void|string sciezka, void|function fun_wyjsciowa); 2.3.15 - Obsluga katalogow -------------------------- [mkdir, rename, rmdir, get_dir] Tworzenie, zmienianie nazwy i usuwanie katalogow robi sie przy uzyciu efunkcji (kolejno) `mkdir()', `rename()', `rmdir()'. Aby je wykonac musisz oczywiscie miec prawa do zapisu w katalogu, w ktorym to robisz. `mkdir()' i `rmdir()' zwracaja 1 w przypadku sukcesu, a 0 w razie niepowodzenia. `rename()' jak juz mowilem dziala odwrotnie i zwraca 1 w przypadku niepowodzenia. `rmdir()' kasuje tylko puste katalogi, tzn takie, ktore nie zawieraja zadnych plikow ani katalogow. int mkdir(string sciezka); int rename(string stara_sciezka, string nowa_sciezka); int rmdir(string sciezka); W celu pobrania zawartosci katalogu uzywa sie efunkcji `get_dir()'. Zwraca ona tablice albo zawierajaca wszystkie pliki w podanym katalogu w przypadku sukcesu, albo pusta w przypadku niepowodzenia. string *get_dir(string sciezka); np. string *zawartosc_kat; int i, sz; zawartosc_kat = get_dir("/d/Domena/fatty"); for (i = 0, sz = sizeof(zawartosc_kat) ; i < sz ; i++) { // Obejrzyj kod funkcji plik_info w poprzednim przykladzie file_info(zawartosc_kat[i]); } 2.3.16 - Wejscie/wyjscie konsoli -------------------------------- [write, write_socket, cat, tail, input_to] Juz zapewne jestes za pan brat z efunkcja `write()', ktora wyswietla dane na ekranie osoby zdefiniowanej jako sluchacz w danym obiekcie. Moze nim byc gracz, ale moze rowniez byc nim jakis inny obiekt. Zazwyczaj funkcja wystarcza, gdy masz pelna kontrole nad tym co i do kogo piszesz. Jednakze istnieje pewna podobna funkcja, ktora czasem staje sie potrzebna. Nazywa sie ona `write_socket()'. Dziala ona prawie tak jak `write()', tylko ze wypisuje teksty *wylacznie* na ekranie interakcyjnego uzytkownika. Jesli nie ma takowego, to zamiast do niego, pisze w glownym logu bledow. W czasie kodowania zwyklych obiektow nie bedziesz musial jej uzywac. Przewaznie, lub nawet zawsze jest wykorzystywana w konkretnych obiektach mudliba, czesciowo tych, ktore maja doczynienia z logujacymi sie graczami. Wypisywanie `write'm jest niezle, ale czasem mozesz miec chrapke na blyskawiczne wyswietlenie plikow, lub przynajmniej ich czesci. Do tego sluzy efunkcja `cat()'. Ukaze ona na ekranie okreslona porcje podanego pliku na ekranie. Istnieje tez inna efunkcja zwana `tail()', ktora wyswietli ostatnie 1080 bajtow pliku. `cat()' zwraca liczbe ukazanych linii, zas `tail()' zwraca 1 w przypadku sukcesu, a 0 w przypadku niepowodzenia. int cat(string sciezka, int start, int dlug); int tail(string sciezka); np. // Wyswietla PLIKTEST, od 20 do 100 linii cat("PLIKTEST", 20, 80) // Wyswietla koncowke tego samego pliku tail("PLIKTEST"); Wiekszosc rzeczy wpisywanych w grze przez graczy przychodzi w formie argumentow do funkcji. Czasem jednak potrzeba zapytac gracza o cos, a on musi odpowiedziec. Pojawia sie tu problem, gdyz wykonanie obiektow w gamedriverze jest sekwencyjne; jesli sie zatrzymasz z wykonywaniem czekajac na odpowiedz, cala gra zatrzyma sie wraz z toba, do czasu az leniwy gracz sie namysli (o ile kiedykolwiek) i odpowie. Z pewnoscia taka sytuacja nie jest dobrym rozwiazaniem. Zamiast tego, mozna wywolac efunkcje `input_to()', ktora pozwala ci ustalic ktora funkcja zostanie wywolana majac za argument jakakolwiek odpowiedz gracza, po ukonczeniu wykonywania aktualnej funkcji. Brzmi to troche skomplikowanie, ale na prawde wcale takie nie jest. Spojrz tylko na przyklad: void input_to(string funkcja, void|int bezecha, void|mixed arg); np. string imie, plec, zajecie; pytajaca_fun() { write("Wpisz swoje imie: "); input_to("fun_2"); } fun_2(string wpis) { imie = wpis; write("\nWpisz swoja plec (Facet/Kobitka): "); input_to("fun_3"); } fun_3(string wpis) { plec = wpis; write("\nCzym sie zajmujesz?: "); input_to("fun_4"); } fun_4(string wpis) { zajecie = wpis; write("\n\nDzieki za wspolprace!\n"); } Jesli jako drugi argument do `input_to' podasz 1, to cokolwiek gracz wpisze, nie zostanie wyswietlone na ekranie. Uzywa sie tego przy haslach i innch waznych informacjach. Trzeci, opcjonalny argument jest przekazywany do funkcji, ktora podales jako odbiorce. 3 - Zaawansowany LPC i Mudlib ***************************** W rozdziale tym zajmiemy sie nieco bardziej zaawansowanymi sprawami dotyczacymi LPC. Musisz *na prawde* zrozumiec podstawy LPC, ktore wylozylem w poprzednich rozdzialach, zeby moc przyswoic sobie to, co tu opisuje. Nie miej zadnych oporow przed przegladaniem poprzednich rozdzialow w razie potrzeby, czytajac ten tekst. N.1 - Pisanie efektywnego kodu ============================== Ten temat jest blisko powiazany z tym co powiedzialem wczesniej, ze *nie* bede wyjasniac jak programowac. Wycofam swe slowa - troszeczke - i opowiem o kilku sprawach co robic, albo nawet co wazniejsze, czego *nie* robic. N.1.1 - Efektywne petle ----------------------- Ten temat moze sie wydawac raczej trywialny.. W koncu na ile sposobow mozna zapisac petle? Raczej na niewiele. Zacznijmy od najczestszego bledu. Zalozmy, ze mamy jakas duza tablice, nazwijmy ja 'wielka_tab' i zalozmy, ze chcemy 'przeleciec' petla przez wszystkie jej elementy. Co robisz w takiej sytuacji? "Proste!" wyjasniasz, "Oczywiscie, ze zwykla petla 'for'!". Pewnie... Ale jak to zapiszesz? Najczesciej ludzie robia to w ten sposob: int i; for (i = 0 ; i < sizeof(wielka_tab) ; i++) { // Tu robimy cos z tablica } No tak... a wiec co jest zle? Jesli przejrzysz rozdzial mowiacy o dzialaniu instrukcji `for', to zobaczysz, ze wykonywane sa trzy czesci w okraglych nawiasach oddzielone srednikami. Pierwsza tylko na poczatku, druga (srodkowa) za *kazdym* cyklem petli i trzecia rowniez za kazdym razem na koncu kazdego cyklu. Oznacza to, ze funkcja `sizeof()' zostaje wykonana za kazdym cyklem petli. Jest to raczej marnotrastwo, biorac pod uwage fakt, ze tablice rzadko zmieniaja rozmiar. Jesli by robily to czesto, bylaby to inna para kaloszy, ale jako ze nie robia... Nie. Napisz to w ten sposob: int i, sz; for (i = 0, sz = sizeof(wielka_tab) ; i < sz ; i++) { // Tu robimy cos z tablica. } Widzisz? Zmienne 'i' oraz 'sz' tylko na poczatku dzialania petli maja przypisywane wartosci. Licznik 'i' zostaje ustawiony na 0, a zmiennej 'sz' zostaje przypisany rozmiar tablicy. Przez caly czas dzialania petli porownywane sa same zmienne, zamiast ciaglego obliczania nie zmieniajacego sie rozmiaru tablicy. Mozesz mi wierzyc albo nie, ale to jest bardzo czesty blad, prawie kazdy go robi. Oszczednosci w moim sposobie moga sie nie wydawac tak wielkie... ale... pomnoz to razy wszystkie petle w mudzie i przez liczbe razy kiedy sa wykonywane, a otrzymasz calkiem niezla liczbe. Kosztem drugiego sposobu w porownaniu do pierwszego jest dodanie jednej zmiennej lokalnej i to jest wystarczajaco mala cena. Pamietaj o tym problemie nie tylko w przypadku tablic, ale rowniez w mappingach lub innych ogolnych pojemnikach, zawierajacych rzeczy, przez ktore chcesz 'przeleciec' petla. Rozwiazanie, z wyjatkiem malych roznic w rozpoznawaniu rozmiaru pojemnika jest zawsze takie same. --- nie dokonczone... Mrpr nie napisal dalszego ciagu jeszcze :/) ---