SnakeASCII, czyli kultowy "wąż" w specyficznej tekstowej formie

1

Z racji tej, że zarwałem kilka wcześniejszych nocek, co przyczyniło się do kompletnego rozregulowania mojego zegara biologicznego, ostatnią noc spędziłem przy komputerze; A że pora nie sprzyjała kończeniu pracy nad moim projektem formatu TreeStructInfo, postanowiłem przysiąść nad czymś przyjemnym, ale nie wymagającym wilekiego skupienia i główkowania;

Zawsze ciągło mnie do napisania kultowej gry Snake w trybie tekstowym, ale w nieco inny sposób, niż wyświetlanie węża zbudowanego ze znaków np. X; Miałem w końcu czas na zapisanie czegoś na kształ odwiecznych ciągotek;

Tak narodził się pomysł stworzenia symulatora gry Snake w trybie tekstowym, który miałby służyć do przetestowania algorytmów zarówno sterowania wężem, jak i jego wyświetlania na ekranie; Ale głównym założeniem było wyświetlanie węża, złożonego ze specjalnych znaków ASCII, służących do budowania rozmaitych ramek okien itd., tak aby wąż był wyświetlany jako jedna, łamana, podwójna linia, gdzie "zakrętami" węża były by znaki rogów ów ramek okien; Wąż także miałby być wyświetlany w różnych kolorach dla różnych jego modułów, a także umożliwiać zjadanie wyświetlanych na ekranie "kropek" (ja nazwałem je "złotem", choć to niezbyt trafne określenie);

I tak udało się stworzyć taki symulator, w całości zaimplementowany w Lazarusie; Nie jest to cudo algorytmiki - kod pisałem po prostu "na pałę", bez wcześniejszego zaprojektowania aplikacji; Nie jest to jednak jeden wielki WTF - zarówno wąż, jak i właściwa gra posiadają osobne klasy, udostępniające określone metody, potrzebne do prowadzenia symulacji; Całość pisałem z rozwagą i przemyśleniem, a kod jest zakomentowany, aby mniej więcej było wiadomo do czego służą dane jego elementy;

Jego przybliżona funkcjonalność:

  • sterowanie wężem za pomocą klawiszy strzałek,
  • resetowanie stanu symulacji, przez skrócenie długości węża do długości początkowej oraz jego ustawienie w domyślnych koordynatach,
  • możliwość przerwania symulacji i zamknięcia programu,
  • możliwość poruszania się po określonym obszarze planszy,
    *wykrywanie kolizji głowy węża z jego ciałem oraz barierkami planszy i blokowanie w takim przypadku ruchu,
  • wyświetlanie "złota" w randomowych miejscach planszy, z wykluczeniem pojawienia się złota poza planszą, na barierce planszy lub na ciele węża,
  • zjadanie "złota" i wydłużanie ciała węża,
  • malowanie węża w dwóch kolorach - głowa i czubek ogona na żółto, a ciało węża na zielono;
    Z głównych funkcji to chyba tyle, choć mogłem o czymś zapomnieć;

Klawiszologia:

klawisz funkcja opis
strzałka w górę MoveUp ruch węża w górę lub skręcenie w górę
Down MoveDown ruch węża w dół lub skręcenie w dół
Left MoveLeft ruch węża w lewo lub skręcenie w lewo
Right MoveRight ruch węża w prawo lub skręcenie w prawo
R Reset przywrócenie długości i pozycji węża do wartości domyślnych
E Exit zakończenie symulacji i wyjście z programu
W najbliższym czasie, jeśli tylko znów znajdę chwilę czasu, to z symulatora poruszania się węża zrobię pełną grę, z możliwością:
  • automatycznego poruszania się węża,
  • możliwość szybszego poruszania się w danym kierunku, przez przytrzymanie klawisza strzłaki,
  • obsługi różnych plansz, nie tylko pustych w środku, ale także "labiryntów",
  • zmianę koloru modułu węża, na którym znajdowało się zjedzone "złotko",
  • liczniki zjedzonych "złotek",
  • prowadzenie rankingu najlepszych wyników + możliwość podania nazwy gracza,
  • plansze wczytywane ze specjalnych plików (pewnie Ini, bo fajnie będzie je można tworzyć ręcznie),
  • różne inne funkcje, dzięki którym gra będzie ciekawa i jakoś może będzie przyciągać;
    Póki co uddaję do dyspozycji sam symulator, który posiada wcześniej opisaną funkcjonalność; Poniżej zrzut ekranu konsoli z uruchomioną symulacją:

snake_simulation.png

Pełne źródła symulatora oraz osobny plik wykonywalny oczywiście udostępniam, dlatego że nie ma sensu ukrywania go przed światem - raczej na nim nie zarobię; A nóż widelec komuś się przyda, lub zainspiruje do programowania; Źródła dołączone do posta w załączniku, w formie archiwum zip;

Zachęcam do przetestowania symulatora węża, a jeśli ma ktoś jakieś sugestie lub propozycje na przyszłość - walcie śmiało; Wszelka krytyka oczywiście także mile widziana; Miejcie jednak na uwadze fakt, iż to dopiero początek - żeby z symulatora zrobić ciekawą grę - jeszcze sporo pracy i modyfikacji istnijącego kodu.

2

Trochę offtop, ale:

Kto pamięta edytor tekstu TAG? (Bodajże polskie dzieło)
DOS-owy edytor tekstowy, z własnym formatem plików, bodajże siedmioma czcionkami - ale bardzo fajny edytorek.

Miał on tryb rysowania ramek w dokumencie. Polegało to na tym, że momentu uaktywnienia tej funkcji - w dokumencie rysowały się ramki w miejscu, gdzie podążyliśmy strzałkami. Gdy do jakiegoś punktu docieraliśmy drugi raz (albo we wszystkich kierunkach) to właśnie używało tych takich przecinających się znaków. Można było używać podwójnych i pojedynczych linii.

Zawsze mnie to fascynowało i smuciło, że żaden inny edytor tego nie miał.

U Ciebie @furious programming nie ma przecinania, ani dołączania trzech kierunków - szkoda :P

BTW: Strona kodowa 852 (jedyna słuszna dla polskich znaków pod DOS?) nie posiadała znaku , bo im brakło 255 znaków :P TAG chyba działał ponad ASCII, więc to śmigało (i chyba miał export do txt gdzie ten znaczek zmieniał się na ý)

0

Tak, TAG był polskim dziełem. Jeszcze był do tego MiniTAG, z tego co pamiętam, jakaś tańsza, uboższa wersja.

0

Nie znam edytora TAG, ale coś sobie poczytam :]

@dzek69 - uwierz mi, że nad przecinaniem także się zastanawiałem; Ba, nawet na początku tworzenia metody TSnake.CanMove nie było jeszcze zaimplementowanej metody TSnake.IsFreeModulePosition, stąd wąż elegancko przechodził "po sobie"; Dodanie rysowania odpowiedniego znaku krzyżyka w miejscu łączenia było by wtedy bardzo proste, ale w takim przypadku nie było by praktycznie możliwości zablokowania węża, stąd i wcześniejszego zakończenia gry; Gra musiałaby trwać aż do zajęcia przez węża wszystkich pól planszy, a to raczej nie o to chodzi;


No dobra, widzę że póki co Wam nie dogodziłem, więc będę musiał wprowadzić jeszcze sporo ficzerów :D

Co jakiś czas będę wrzucał nowe źródła jako małe commity, zawierające kolejne małe poprawki; W każdej bardzo wolnej chwili będę rozwijał Snakea, jako malutki projekt, nie wymagający wielkiego skupienia; Nie wiem kiedy uznam, że czas zokończyć nad nim pracę, ale na pewno nie nastąpi to prędko;

W takim razie pierwszy commit:

  • rysowanie na jasnozielono modułu węża, który znajduje się na pozycji wchłoniętego złota - nie dotyczy modułów głowy i ogonka węża;

Zrzut:

snake.png

Nie wiem jaka funkcja będzie następna, ale będzie ich jeszcze mnóstwo; A jeśli ktoś ma jakieś propozycje, to piszcie śmiało :]

1

Kolejny update - wprowadziłem sporo zmian, jednak nadal jest to symulator, jeszcze nie można pograć;

Nowości:

  • stworzenie klasy pojedynczej planszy, zawierająca podstawowe informacje;
  • możliwość ładowania planszy z pliku - obecnie z pliku Ini;
  • możliwość tworzenia plansz o dowolnych kształtach,
  • określenie w pliku planszy takich informacji, jak nazwa i poziom trudności, pozycja startowa i kierunek węża, mapa planszy jako macierz znaków (łatwa do modyfikacji czy tworzenia), sekcje "dziur", w których nie będą pojawiać się złotka;
  • rysowanie barierek plansz znakami pojedynczej linii;
  • możliwość tworzenia ozdobnych kępek trawy, poza powierzchnią roboczą planszy;
  • "animowane" rysowanie zawartości planszy;
  • poprawione sterowanie węża;
  • ustawienie ikonki aplikacji i informacji o wersji programu;
    I jeszcze kilka pomniejszych, mniej istotnych poprawek; Przykładowy zrzut aplikacji:

screen.png

Coraz więcej nowych rzeczy, ale do końca jeszcze daleko; Teraz przynajmniej można tworzyć ręcznie pliki plansz, nadając im dowolne kształty i upiększając kępkami trawy; Poprawne zapisanie współrzędnych "dziur" w pliku planszy uniemożliwia pojawienie się złotka w miejscu, do którego wąż nie dojdzie; Opis tworzenia pliku planszy zawarty jest w pliku levels\Levels Creating.txt;

Plansze póki co trzeba tworzyć ręcznie, ale na pewno dorobię edytorek, oczywiście w trybie tekstowym, aby dużo łatwiej i szybciej móc tworzyć nowe plansze; Na razie to początki, jak tylko skończę funkcjonalność węża oraz plansz, to zrobię w końcu automatyczne poruszanie się wężem (czyli normalną pętlę obsługującą FPSy), tak że będzie można już coś pograć

Jeśli chodzi o sterowanie wężem - planuję wprowadzić tryb turbo, dzięki któremu proste odcinki będzie można pokonać dużo szybciej;

Następny update wkrótce, a w załącznikach źródło projektu oraz sam plik wykonywalny.

0

uuu , a u mnie:

An unhandled exception occurred at $00419ABE :
EAccessViolation : Access violation
  $00419ABE  READLEVELINFO,  line 255 of Level.pp
  $0041943C  TLEVEL__LOADFROMFILE,  line 331 of Level.pp
  $00419142  TLEVEL__CREATE,  line 175 of Level.pp
  $0040F382  TGAME__CREATE,  line 63 of Game.pp
  $0040166A  main,  line 14 of SnakeASCII.lpr
 
0

@szalonyfacet - zobacz do pliku levels\0.lvl (otwórz notatnikiem, to zwykły Ini) jak wygląda sekcja INFO; Powinna wyglądać tak:

[INFO]
Name=Space   ; tutaj nie ma znaczenia
Difficulty=A ; możliwa literka od A do D

Rozumiem, że u Ciebie wywala się na linii:

FDifficulty := LetterToLevelDiff(strDifficulty[1]);

U mnie wszystko gra - "konwersja" jest poprawna; Żeby coś móc poradzić, potrzebowałbym więcej informacji.

1

hmm dziwnie prawisz o jakims pliku w folderze, jak ja mam tylko exeka bez folderow i dodatkowych plikow. Poobralem plik z zalacznika SnakeASCII_Exe.zip, otworzylem i odpalilem w konsoli.

I ten blad wyrzuca mi w konsoli.

Notabene nie tylko u mnie na kompie.

0

Ha, @szalonyfacet - racja, przecież do archiwum z plikiem wykonywalnym nie dołączyłem katalogu z levelami... :D

Oczywiście exe nie ma prawa działać poprawnie, bo brakuje mu pliku levels\0.lvl; Już poprawiam - dołączę ten katalog do archiwum w poprzednim poście;


@dzek69 - dzięki za czujność; A tak poza tym, wcześniej wspomniałeś o edytorze TAG, który pozwalał na rysowanie tabel w dokumentach; Nie używałem tego edytora, więc nie sprawdzę jak tę funkcję się używało, w każdym razie w edytorze poziomów na pewno zaimplementuję taką funkcję - do rysowania barierek poziomów, kępek trawy oraz regionów "dziur", w których nie będą się pojawiać kropki do zbierania;

Dzięki Panowie za informacje - mały fail nigdy nie zaszkodzi :]

1

A gdzie generator losowych mapek? ;>

0

Myślałem o nim, ale najpierw muszę dokończyć rysowanie różnych rzeczy, stworzyć menu, różne liczniki (ilości złapanych kropek, czas rozgrywki, ilość żyć itd.) no i samą pętlę, dzięki której będzie można grać;

Generator losowych plansz być może zrobię, ale w edytorze plansz, aby zamiast tworzyć całość ręcznie - móc sobie taką wygenerować i potem użyć w głównej aplikacji gry :]

0

zrób obsługę kodu seedującego random :D żeby jak w Wormsach grać zawsze na ulubionym-ale-losowym kształcie mapy, który kryje się pod stringiem "furiousprogrammingniemamuzgu" ;)

0

@furious programming: dodaj licencję i wrzuć na githuba (lub coś w tym rodzaju), ja mniejsze rzeczy wrzucałem.
Szkoda żeby się coś takiego marnowało i leżało odłogiem.

1

Dzięki za wskazówkę @vpiotr;

Wrzucę na GitHuba, ale jak po pierwsze doprowadzę kod do porządku, a po drugie - jak już będzie można pograć; Bo póki co kod w fazie testów, jeszcze nie wszystko z dotychczasowej funkcjonalności działa poprawnie, kod nie jest rozłożony na klasy tak, jak to powinno wyglądać;

Dorobiłem taką małą animację, że po kolizji (czyli zderzeniu się z głowy z ciałem węża lub barierką) wąż "szarzeje" od głowy do ogona - tak jakby zamarzał; Tyle że gdzieś leży problem z jego kolorowaniem, bo elementy jego ciała, w których znajdował się punkcik do zbierania nie są kolorowane na jasnoszary; Tzn. kolorowanie działa, ale wygląda to tak, jakby współrzędne nie pasowały i zamiast pokolorowania odpowiedniego modułu - kolorowany jest kolejny, i tak we wszystkich miejscach; Znalazłem buga - kolorowanie "śmierci" węża poprawione i działa prawidłowo;

Trochę jeszcze bałagan w tym kodzie, ale jak znajdę trochę czasu to lepiej zorganizuję kod i przede wszystkim dorobię w końcu sensowną pętlę i obsługę klawiatury, aby można było pograć; Dopiero wtedy wrzucę kod na GitHuba - jeśli już coś będzie można z tym zrobić, niż tylko testować :]

0

U mnie wygląda to tak:

72c9542c41.png

Coś mim się wydaje, że Twój program w żaden sposób nie kontroluje kodowania znaków, i jak jest jakieś niestandardowe, to wyświetla kaszę. Nie używasz Unicode?

0

@Krolik - owszem, w ostatniej wersji jaką dołączyłem w tym wątku (i którą Ty testowałeś), nie ma jeszcze ustalonej strony kodowej, więc program korzysta z domyślnej, co nie wszędzie będzie działać poprawnie; Ale jest to ustalone już, a oprócz tego kod dość mocno poprawiłem i rozwinąłem, tak że jak skończę implementować główną pętlę gry (czyli wąż będzie się już sam poruszać) to wrzucę aktualne źródła.

0

Po długiej przerwie przyszedł czas na kolejną aktualizację; Wprowadziłem do kodu mnóstwo zmian, a także zmusiłem węża do samodzielnego poruszania się; Poniżej wypunktowane zmiany;


1. Podział kodu

Kod został podzielony na 10 modułów, dzięki czemu całość stała się bardziej czytelna, a same moduły są krótsze; Opis poszczególnych modułów:

moduł opis
SnakeASCII.lpr główny moduł projektu, tworzy, wywołuje i niszczy klasę gry
Game.pp zawiera główną klasę gry
Controller.pp zawiera klasę do obsługi klawiatury
Painter.pp zawiera klasę, służącą do malowania interfejsu (barierek planszy, kępek trawy, węża i złotka)
Converter.pp zawiera klasę, posiadającą statyczne funkcje konwertujące, przydatne w kilku klasach
Timer.pp zawiera klasę stopera, potrzebną do obsługi klawiatury oraz automatycznego poruszania się węża (w późniejszym czasie także do liczenia czasu rozgrywki)
Level.pp zawiera klasę pojedynczej planszy, z możliwością ładowania jej danych z plików konfiguracyjnych
Snake.pp zawiera klasę węża (w późniejszym czasie przyda się także do rysowania węża w tutorialach)
Gold.pp zawiera klasę z informacjami o złotku, losowanym na planszy
Common.pp zawiera zbiór wspólnych stałych i typów, wykorzystywanych w całym projekcie

2. Ruchy węża

Skorzystałem z sugestii @dzek69'a i oprogramowałem węża tak, że możliwe jest przechodzenie przez swoje ciało; Ale to przechodzenie nie jest takie dowolne - zbyt łatwo by było;

Wąż może przejść jedynie w miejscu, w którym było złotko; Taki moduł węża jest tak samo jak wcześniej, rysowany innym-jaśniejszym kolorem; Kolejnym ograniczeniem jest możliwość przejścia przez moduł węża, który nie znajduje się na jego zakręcie - w takim przypadku, nawet jeśli w tym module znajdowało się złotko - zostanie wykryta kolizja i wąż "umrze";

Jeszcze jednym ograniczeniem jest to, że przez moduł ze złotkiem można przejść tylko raz; Jeśli przez bramkę przejdzie ogon węża - jest ona zamykana; To nie jest celowe, tylko logiczne - bramka zostaje utworzona w miejscu, gdzie znajdował się poprzedni moduł, nie ogon; A skoro ogon przechodzi przez bramkę, to właśnie ogon posiada informację o złotku; Ogon przechodzi przez bramkę, więc i informacja o złapanym w tym miejscu złotku przechodzi do kolejnego modułu, a że ogon jest ostatni, to ta informacja zostaje tracona;


3. Klawiszologia

Niestety obsługa klawiszy strzałek jako znaków AnsiChar okazała się bardzo problematyczna, więc klawisze sterowania zostały zmienione; Poniżej lista obsługiwanych klawiszy z ich przeznaczeniem:

klawisz podstawowy nazwa wewnętrzna opis
W INPUT_KEY_UP skręcenie węża w górę
S INPUT_KEY_DOWN skręcenie węża w dół
A INPUT_KEY_LEFT skręcenie węża w lewo
D INPUT_KEY_RIGHT skręcenie węża w prawo
spacja INPUT_KEY_TURBO maksymalne przyspieszenie węża
klawisz specjalny nazwa wewnętrzna opis
---------------- ---------------- ----------------
P INPUT_KEY_PAUSE zatrzymanie gry
R INPUT_KEY_RESET zresetowanie gry
Q INPUT_KEY_QUIT wyjście z gry
Znaczenia klawiszy specjalnych chyba nie muszę tłumaczyć; W każdym razie póki co, zresetować grę można jeśli nie jest włączona pauza; A jeśli zabijecie węża, aby całkowicie wyjść z gry trzeba najpierw ją zresetować;

Standardowo wąż wykonuje ruch co kilka klatek, sprawdzając stan tajmera; Ilość klatek oczekiwania na ruch węża zapisana jest w pliku planszy; W przypadku najmniejszej prędkości węża, ruch wykonywany jest co 6 klatek, w przypadku największej prędkości - co 2 klatki; Każda klatka gry trwa ~40ms, więc dla najwolniejszej prędkości ruch wykonywany jest co ~240ms, a przy największej - co ~80ms; Istnieje pięć stopni prędkości - ich opis w tabelce:

typ ruchu nazwa wewnętrzna ilość klatek ilość milisekund
najwolniejszy lossLow 6 240ms
średni lossMedium 5 200ms
szybki lossHigh 4 160ms
ekstremalny lossExtreme 3 120ms
niemożliwy lossImposible 2 80ms
Jak widać w tabelce, możliwe jest skorzystanie z trybu turbo; Charakteryzuje się on tym, że ruch węża wykonywany jest zawsze w każdej klatce, czyli co ~40ms; Tryb ten włącza się wciskając i trzymają albo klawisz spacji, albo klawisz ruchu; Aby go wyłączyć, trzeba puścić trzymany klawisz; Tryb ten możliwy jest tylko w przypadku ruchu w jednym kierunku - przy wciśnięciu klawisza skrętu węża jest on automatycznie wyłączany;

Obsługa ruchów węża napisana jest tak, że w jednej klatce wąż może skręcić tylko raz, nawet jak pomiędzy ruchami węża wciśnięto kilka razy klawisze sterowania (nie dotyczy trybu turbo); Klasa gry współpracuje z klasą kontrolera, który zapamiętuje tylko jeden wciśnięty klawisz, a kolejne znajdujące się w buforze inputu kasuje; Jeśli wciśnięto klawisz skrętu węża, gra oczekuje na moment, aż tajmer odliczy tyle cykli, ile klatek trzeba czekać na ruch (to zapisane jest w pliku planszy); Po obsłudze skrętu węża, zapamiętany klawisz jest kasowany; W przypadku klawisza trybu turbo (czyli spacji lub przytrzymanego W, S, A lub D) lub klawisza specjalnego, jest on obsługiwany w każdej klatce;


4. Znaki żywego węża

Poprzednio malowanie węża było uboższe, dlatego że nie obsługiwał on przechodzenia przez moduły ze złapanym złotkiem, a samo złotko było malowane na każdym module, łącznie z zakrętami, pomijając moduły głowy i ogonka; Teraz malowanie węża jest dużo bardziej skomplikowane;

Głowa węża zawsze posiada ten sam znak - znak prostokąta; Zakrętny węża posiadają te same znaki, co wcześniej; Natomiast zmienione zostało malowanie "karku" węża oraz ogona;

Jeśli moduł karku znajduje się w otwartej bramce (czyli głowa węża przeszła przez moduł ze złapanym złotkiem), rysowany jest zawsze krzyżyk, który łączy ciało węża; W przypadku ogona, sprawa jest jeszcze bardziej skomplikowana; Jeśli moduł ogona nie znajduje się w miejscu otwartej braki - rysowany jest aktualny znak, czyli albo podwójna prosta linia, albo znak zakrętu; Jeśli natomiast ogon znajduje się w miejcu otwartej bramki - rysowany jest znak T, odwrócony w odpowiednią stronę, który także ładnie zamyka strukturę jego ciała; Zaobserwować to można lepiej przy najniższej prędkości węża;


5. Kolory żywego węża

Tutaj nowość - wprowadzone zostały schematy kolorów dla węża; Określone są w nich kolory modułów ciała węża, bez kolorów głowy oraz ogona, które zawsze są takie same - żółte; Każdy moduł ciała węża może być malowany na dwa kolory - ciemniejszy dla podstawowego modułu, w którym nie znajduje się złotko ani bramka, oraz jaśniejszy, w którym znajduje się złapane złotko lub otwarta bramka; Obsługiwane jest 6 schematów kolorów;

schemat stałe koloru typ modułu
zielony Green podstawowy moduł ciała
LightGreen moduł ze złapanym złotkiem lub otwartą bramką
zielono-niebieski Cyan podstawowy moduł ciała
LightCyan moduł ze złapanym złotkiem lub otwartą bramką
niebieski Blue podstawowy moduł ciała
LightBlue moduł ze złapanym złotkiem lub otwartą bramką
fioletowy Magenta podstawowy moduł ciała
LightMagenta moduł ze złapanym złotkiem lub otwartą bramką
czerwony Red podstawowy moduł ciała
LightRed moduł ze złapanym złotkiem lub otwartą bramką
złoty Brown podstawowy moduł ciała
Yellow moduł ze złapanym złotkiem lub otwartą bramką
Schematów jest tyle, dlatego że tyle jest par kolorów - podstawowy + Light* (wyjątkiem jest złoty); Może ich być więcej - trzeba tylko powiększyć tablicę ze stałymi kolorów;

Schematy te zostaną wykorzystane w późniejszym czasie, już po stworzeniu trybu "podróży", czyli przechodzenia plansza po planszy; Docelowo ma być 6 różnych "światów", po 10 plansz w każdej; Dla każdego świata, wąż będzie posiadał inny schemat kolorów;

Poniżej zrzuty wszystkich obsługiwanych na dzień dzisiejszy schematów kolorów węża:

colors-schemas.png

Schemat kolorów węża także zapisany jest w pliku konfiguracyjnym danej planszy;

Samo malowanie węża jest bardzo szybkie, dzięki odmalowywaniu zawsze tylko i wyłącznie 4 modułów, bez względu na długość węża: modułów głowy, karku, ogona i modułu, który ogon opuścił; Barierki planszy oraz kępki trawy nigdy nie są odmalowywane, więc co klatka, to malowanie czterech modułów;


6. Śmierć węża

Kolejnym ficzerem jest malowanie animowanej śmierci węża; Charakteryzuje się ona tym, że w sposób animowany, kolejne moduły węża kolorowane są na szaro lub biało, a odcień szarego zależy od typu modułu - ciemniejszy dla podstawoweych modułów ciała, a jaśniejszy dla modułów ze złapanym złotkiem lub dla bramki;

Poniżej na zrzucie namalowany martwy wąż:

dead-snake-colors-schema.png

Szybkość malowania animowanej śmierci węża zależy od jego długości; Im dłuższy wąż, tym mniej wynosi czas odstępu pomiędzy malowaniem modułów;


7. Kolory planszy

W odróżnieniu od poprzedniej wersji, barierki planszy oraz kępki trawy mogą być malowane na wszystkie dostępne w konsoli standardowe kolory; Nie dotyczy to koloru czarnego, który zarezerwowany jest dla tła planszy;

Barierki planszy zawsze malowane są na jeden kolor, zapisany w pliku planszy; Nie ma możliwości namalowania kawałka barierki na inny kolor; Natomiast w przypadku kępek trawy, każda z nich może być malowana na inny kolor, ale - jedna kępka, jeden kolor;

Zarówno barierki planszy, jak i kępki trawy malowane są w sposób pseudo-animowany, malując wiersze z góry do dołu; W miarę fajny efekt wyszedł;


8. Pliki planszy

W dalszym ciągu korzystam z plików Ini do przechowywania informacji o danej planszy; Część informacji zawartych w pliku nie jest jeszcze obsługiwanych, jak nazwa planszy czy poziom jej trudności, i może zostać w późniejszym czasie albo wykluczona, albo zamieniona na inną;

Pliki te zostały zmodyfikowane - dorobiłem więcej sekcji oraz kluczy, stąd więcej rzeczy można ustawić; Zawartość przykładowego pliku planszy (ta plansza jest także w załącznikach oraz końcowym zrzucie ekranu) tutaj;

Dozwolone wartości dla poszczególnych kluczy wymienione są w pliku Readme.txt, zawartym w katalogu levels; Część katalogów jest póki co pusta, ale będą także używane w późniejszym czasie;

Zrzut planszy, którą opisuje powyższy plik konfiguracyjny:

SnakeASCII_screen.png


9. podsumowanie

Gra zajmuje niewiele w pamięci i niewiele mocy procesora potrzebuje; Powinna działać szybko na nieobciążonym kompuerze; Jeśli komputer zostanie obciążony, zachowanie płynności gry będzie dokładnie takie same, jak obciążenie gry na Pegasusie - całość spowolni; I takiego efektu się spodziewałem, dlatego że chciałem tę grę napisać tak, jak dawno temu pisano gry na Pegasusa; Żadnych wątków, synchronizacji logiki i widoku itd., stąd też timer liczący klatki gry i oszczędność malowania konsoli; Wyszło jak dla mnie idealnie;

To chyba wszystko, co nowego udało się wprowadzić; Zgodnie ze sugestią @vpiotra, dodałem informacje o licencji; Wybrałem licencję GNU Lesser GPL 3 na wypadek, jakby ktoś chciał na tym zarobić :]

Co do problemu z kodowaniem znaków, o którym pisał @Krolik - Lazarus nie wspiera możliwości kompilowania jednego kodu na różne platformy; Podany w załącznikach plik wykonywalny będzie poprawnie wyświetlał znaki jedynie na Windowsach, gdzie ustalone ma kodowanie CP_UTF8; Jeśli kod ma działać na uniksach - musi być pod uniksem skompilowany; Dodana jest obsługa menedżera łańcuchów dla uniksów, więc znaki powinny być poprawnie wyświetlane; Ja niestety nie mam możliwości skompilować kodu na którymś z Linuksów czy Mac OS X, więc to zadanie pozostawiam linuksiarzom;

To tyle - życzę miłego analizowania kodu (połowa jego objętości to komentarze) oraz grania - bo już można; W załączniku dodaję źródła gry oraz skompresowany UPXem plik wykonywalny w trybie release, skompilowany na WinXP, więc tylko dla windowsowców;

A co do GitHuba - chyba jeszcze nie czas, bo mało funkcjonalna jest ta "gra" :]

0
babubabu napisał(a)

A czemu zamiast plików INI nie wykorzystasz TreeStructInfo? :]

Z prostej przyczyny - TreeStructInfo jeszcze nie jest opublikowany, więc nie ma dostępnych materiałów, które wyjaśniały by po pierwsze strukturę plików, po drugie - jak korzystać z oficjalnego API dla FPC; Ale z tych plików mam zamiar skorzystać już od dłuższego czasu, tylko czekam na publikację projektu w sieci;

Jeśli wszystko pójdzie po mojej myśli, strona projektu TreeStructInfo zostanie dziś wrzucona na serwer, bo wczoraj ukończyłem specyfikację formatu, czym zamknąłem prace nad tym projektem :]

babubabu napisał(a)

Zauważyłem, że zamiast ramki to mam znaki równa się ale nie pamiętam czy ten problem występuje przy poruszaniu w lewo czy w prawo czy w obu kierunkach.

Za mało informacji podajesz, abym cokolwiek mógł zrobić;

Jeśli testujesz program na Linuksie lub Mac OS X, to musisz mi napisać czy program uruchomiłeś z gotowego exe z załącznika SnakeASCII_0.102_for_Win.zip, czy kod skompilować i uruchomiłeś; Ja niestety nie mam możliwości sprawdzić działania programu pod innymi platformami niż Windows, więc kod dla innych systemów pisze "na czuja", po wstępnej lekturze dokumentacji Free Pascala;

Jeżeli natomiast uruchamiasz program z exeka lub kompilujesz kod pod Windowsem, to tutaj jest mała nieścisłość; W Windows XP znaki podwójnej ramki stykają się ze sobą, tworząc jednolite linie, jak widać to na zrzutach z poprzedniego posta; Pod Windows 7 znaki ramki nie łączą się ze sobą - pomiędzy nimi jest malutka, jednopikselowa przerwa; A że zarówno wąż, jak i barierki planszy są zbudowane ze znaków pojedynczej i podwójnej ramki - nie wygląda to tak dobrze, jak pod WinXP;

Samo budowanie węża z tych znaków jest dość skomplikowane - obsługiwanych jest jak dobrze liczę 12 różnych znaków ramki, więc przypadków trzeba obsłużyż kilkanaście; Poświęciłem najwięcej czasu właśnie na poprawne rysowanie węża, bo ono zmienia się dynamicznie co każdy jego ruch;

Ale co do malowania węża jestem w 100% pewny, bo z setek testów wynika, że obsłużone zostały wszystkie przypadki, a te, które mogą budzić wątpliwości są celowe; Np. malowanie głowy węża zawsze na wierzchu, ogon węża zawsze malowany na żółto, bez względu na to, czy ogon znajduje się w wolnym miejscu planszy, czy w module bramki, itd.; Nigdzie nie wykorzystuję znaków =, więc ten znak po prostu nie może być malowany;

Do czego zmierzam - w systemie Win7 i pewnie także WinVista i Win8, znaki ramki dla poziomego modułu ciała węża wyglądają bardzo podobnie do znaku =, dlatego że między nimi jest malutka przerwa; Wynika to niestety z takiego a nie innego zestawu znaków z systemowego fonta, który używany jest w konsoli; Przybliżony efekt złego malowania węża na poniższym zrzucie:

snake-types.png

Mniej więcej tak jak po prawej stronie zrzutu wygląda wąż na Windows 7, przynajmniej na komputerze, na którym mogłem grę przetestować;

Jeśli natomiast opisane przez Ciebie @babubabu znaki wyglądają inaczej niż te, które opisałem - pokaż mi zrzut ekranu z gry i zaznacz te znaki, które uważasz za nieprawidłowe, a postaram się to naprawić; Napisz też pod jakim systemem uruchamiasz grę, bo takie informacje są ważne;


BTW: Jeszcze o jednej rzeczy nie wspomniałem w poprzednim poście; Funkcja pauzowania gry została zmieniona; Nie pamietam jak wygląda w źródłach załączonych w poprzednim poście (nie pamiętam czy modyfikację wprowadziłem przed dodaniem załącznika do posta, czy po), ale wstępnie pętla oczekująca na klawisz P aby wznowić grę, zabierała ~40% mocy procesora; W obecnym kodzie dodałem instrukcję Delay:

{ wait for input key }
while not FGameController.InputKeyPressed do
begin
  { sleep the frame }
  Delay(SINGLE_FRAME_DELAY_TIME); // tutaj
  { process the input }
  FGameController.ProcessInput();
end;

dzięki czemu bufor klawiatury sprawdzany jest co 40ms, a nie bez przerwy; To pozwoliło na utrzymanie zerowego poboru mocy obliczeniowej procesora, podczas pauzy gry; Sama gra natomiast także wykorzystuje znikome zasoby procesora, utrzymując 0% w menedżerze zadań Windows; To oczywiście informacje szacunkowe, bo szczegółowej diagnozy nie przeprowadzałem;

Nowością, która na pewno nie znajduje się w źródłach w załączniku z poprzedniego posta to rysowanie trawki w inny sposób; Wcześniej było to malowanie znaków /, teraz trawka malowana jest inaczj - każda linia budowana jest randomowo z liter w, W, v i V, bo po pierwsze wygląda lepiej, a po drugie po każdorazowym zresetowaniu gry (co oznacza także przemalowaniu kępek trawy) trawka wygląda inaczej:

grass-types.png

Nowa trawka i zapewne kilka innych nowości będzie możliwych w kolejnej wersji gry, do której podam załączniki w tym wątku;


Od kilku dni sugerujecie wrzucenie źródeł gry na GitHub, co mnie wcale nie dziwi; Jednak z tym chcę jeszcze poczekać, dlatego że w pierwszej kolejności chcę ukończyć pracę nad projektem TreeStructInfo, opublikować projekt w sieci i dodać źródła biblioteki na GitHub;

Po drugie - najpierw wolałbym oprogramować grę SnakeASCII do takiego stopnia, aby prócz pojedynczej-bezcelowej rozgrywki można było zrobić coś więcej; Żeby było jakieś menu, więcej plansz, jakieś opcje, liczenie czasu i punktów, zamiana plików Ini na TreeStructInfo itd.; Żeby ta gra coś reprezentowała, a nie nosiła wciąż zmianiona zwykłych testów, jak początkowo zakładałem (tylko przetestowanie rysowania węża ze znaków ramki);

Na pewno wrzucę grę do repozytorium, ale najpierw chciałbym rozbudować ją do podstawowego stopnia, żeby faktycznie wyglądała jak gra :]

0

Coś na pewno jest nie tak:
snake.jpg

Gdy wonsz porusza się w lewo to widać przerwy ale gdy porusza się w prawo to tych przerw już nie ma.

Przyczyny mogą być dwie:

  1. Błąd w systemie/foncie/konsoli
  2. Błąd w grze.

Win 7 Home Premium

Ah zapomniałem. Grę odpalam z execa dołączonego do posta. Źródeł nie przeglądałem jeszcze.

0

Fajne :D

Jeśli komputer zostanie obciążony, zachowanie płynności gry będzie dokładnie takie same, jak obciążenie gry na Pegasusie - całość spowolni; I takiego efektu się spodziewałem, dlatego że chciałem tę grę napisać tak, jak dawno temu pisano gry na Pegasusa; Żadnych wątków, synchronizacji logiki i widoku itd., stąd też timer liczący klatki gry i oszczędność malowania konsoli; Wyszło jak dla mnie idealnie;

Czyli nie da rady "wypaść" przez ścianę jak odpowiednio zmulę komputer? :P

BTW: Można pogodzić obie metody, ale rzadko widzę takie implementacje.

0

Spodziewałbym się, że poruszać wężem będę kursorami. Niestety, po ruszeniu nie udało mi się nawet skręcić.
Dobra, przeczytałem gdzieś w połowie tego wątku o WSAD. Ale nie lubię WSAD to nie odpalam już ;]

0
babubabu napisał(a)

Gdy wonsz porusza się w lewo to widać przerwy ale gdy porusza się w prawo to tych przerw już nie ma.

Wonsz budowany jest zawsze z tych samych znaków; jeśli część jego ciała idzie w lewo lub w prawo - wykorzystywany jest znak o kodzie 205, a jeśli w górę lub w dół - znak o kodzie 186; Kody znaków wrzucone są do enumów typu TSnakeModuleBasicType:

type
  { snake single module basic type enumeration }
  TSnakeModuleBasicType = (
    smbtNone            = 0,   { none module                 }
    smbtVertical        = 186, { vertical body               }
    smbtCornerLeftDown  = 187, { corner from left to bottom  }
    smbtCornerLeftUp    = 188, { corner from left to top     }
    smbtCornerRightUp   = 200, { corner from right to top    }
    smbtCornerRightDown = 201, { corner from right to bottom }
    smbtHorzizontal     = 205, { horizontal body             }
    smbtHead            = 219  { head module character       }
  );

Natomiast znaki dodatkowe, które używane są do wyświetlania "bramek", zawarte są w enumach typu TSnakeModuleAdditionalType:

type
  { snake single module additional type enumeration }
  TSnakeModuleAdditionalType = (
    smatLeftVert   = 185, { gate from left to vertical     }
    smatTopHorz    = 202, { gate from top to horizontal    }
    smatBottomHorz = 203, { gate from bottom to horizontal }
    smatRightVert  = 204, { gate from right to vertical    }
    smatCrossGate  = 206  { cross gate                     }
  );

Więc jak widać nie korzystam w ogóle ze znaku równości =; Przyczyną musi więc być font używany w konsoli; W kazdym razie zawsze możesz zdebugować źródła i znaleźć ewentualny bug - źródła są publiczne :]


dzek69 napisał(a)

Czyli nie da rady "wypaść" przez ścianę jak odpowiednio zmulę komputer?

Nie wykonasz żadnego niedozwolonego ruchu - przykro mi :]

dzek69 napisał(a)

BTW: Można pogodzić obie metody, ale rzadko widzę takie implementacje.

Owszem, ale na celu miałem zaprogramowanie głównej pętli tak, jak miało to miejsce w grach na Pegasusa; Jeśli zamulisz komputer to całość powinna zwolnić, zarówno logika, jak i malowanie interfejsu; Wykorzystałem tak rzadko stosowaną metodę po pierwsze z ciekawości (zawsze podziwiałem gry z Pegasusa) i aby sprawdzić/przetestować, jak trudne jest napisanie takich pętli dzisiaj; A po drugie też dlatego, że nie znam technik tworzenia gier, synchronizacji logiki i widoku, dlatego też na dzień dzisiejszy nie do końca wiem jak miałoby to wyglądać; Poza tym nie chciałem implementować jakiejś zaawansowanej logiki w tak prostej grze, więc skorzystałem z prostego i ciekawego sposobu;


Marooned napisał(a)

Spodziewałbym się, że poruszać wężem będę kursorami. Niestety, po ruszeniu nie udało mi się nawet skręcić.
Dobra, przeczytałem gdzieś w połowie tego wątku o WSAD. Ale nie lubię WSAD to nie odpalam już ;]

Kurde, tu jest problem z obsługą klawiszy strzałek jako znaków AnsiChar; Nie wiem jeszcze co powoduje nierozpoznanie tych klawiszy przez kontroler, w każdym razie póki co sterowanie ustawione jest na WSAD; Jeśli odkryję problem, to na pewno zamienię z powrotem na strzałki; Ale najpierw muszę znaleźć ten bug i zależy mi na tym, czyli przedebugować kontroler;

W późniejszych wersjach dorobię na pewno ustawienia gry i możliwość zmiany klawiszy sterowania, więc jeśli nie uda mi się przywrócić poprawnej obsługi strzałek - będzie to jakieś połowiczne rozwiązanie;


Jeszcze bardzo dużo będzie w tej grze zrobione, więc obecna forma na pewno będzie modyfikowana i można spodziewać się poprawek i usprawnień :]

0

No i poszło - zmodyfikowanie kodu tak aby obsługiwał także klawisze strzałek oznaczało dodanie jednego warunku do pętli w metodzie TGameController.ProcessInput():

procedure TGameController.ProcessInput();
begin
  { check if the key was pressed }
  if KeyPressed() then
  begin
    { get the key from input }
    FInputKey := UpCase(ReadKey());

    { detect an arrow key }
    if (FInputKey = INPUT_KEY_NONE) and KeyPressed() then  // tutaj
      { get the arrow key character }
      FInputKey := UpCase(ReadKey);

    { clear input buffer }
    while KeyPressed() do
      ReadKey();

    { validate input key }
    if (FInputKey in INPUT_SNAKE_MOVE_KEYS_SET) or (FInputKey in INPUT_SPECIAL_KEYS_SET) then
      Exit;
  end;

  { if key is not supported or key is not pressed - set the input null key }
  FInputKey := INPUT_KEY_NONE;
end;

Przywróciłem więc możliwość sterowania wężem strzałkami, więc możliwe że @Marooned program z kolejnej aktualizacji już uruchomi :]


Niestety obsługa klawiszy jako znaków AnsiChar znów objawia kolejny problem; Z racji tej, że i tak znak strzałki ląduje jako któraś z liter H, K, P lub M, póki co nie ma możliwości wybrania strzałek do sterowania oraz liter H, K, P i M do innych opcji, a obecnie literka P służy do zapauzowania gry; Dlatego zmieniłem literkę P na Esc do obsługi pauzy;

Rozwiązaniem tego problemu będzie wprowadzenie translacji AnsiChar na liczbę typu UInt16 i przechowywanie kodu wciśniętego klawisza, a nie jego wartości; Przykładowo młodszy bajt będzie zawierał kod literki, a starszy np. zapalony najmłodszy bit informujący o podaniu rozszerzonego klawisza; Dzięki temu w przypadku litery H, kod klawisza to będzie 0x0048, a strzałki w górę (czyli też litery H) kodem będzie liczba 0x0148; Obojętne jest to, w jaki sposób budowane będą 16-bitowe liczby kodów znaków, ważne żeby dało się je rozróżnić;


Dziękuję za szybką poradę dotyczącą klawiszy strzałek i wyręczenia mnie z Googlowania (choć zacząłem już czytać pewien wymieniony w komentarzu wątek, ale użytkownicy mnie ubiegli) :]

0
furious programming napisał(a):

Z racji tej, że i tak znak strzałki ląduje jako któraś z liter H, K, P lub M, póki co nie ma możliwości wybrania strzałek do sterowania oraz liter H, K, P i M do innych opcji

zauważ że naciskając strzałkę, kod litery jest poprzedzony znakiem #0 (czyli dwa kody, najpierw zero, potem właściwy) co bardzo łatwo jest obsłużyć

0

@fsadsafdfdsa - teraz już wiem, dzięki @babubabu, @dzek69 i @vpiotr; Pamiętam jak jeszcze w TI pisząc jakąś grę na ocenę 6 (albo Hanoi, albo Kółko i Krzyżyk - nie pamiętam już), znalazłem w sieci informacje na temat znaków, jakie reprezentują strzałki; Wtedy wszystko obsługiwałem przez ReadKey i wszystkie znaki traktowałem jako jednobajtowe, więc utrwaliło mi się to w głowie, że klawisz strzałki to w sumie jeden znak; Ale to była błędna wiedza, którą nosiłem w sobie do wczoraj;

W kodzie metody TGameController.ProcessInput, który podałem w poprzednim poście, klawisze strzałki są rozpoznawane poprawnie; Nie obsługuję ich od razu, a zapisuję do pola klasy, czyli do FInputKey; Klawisz ten muszę zapamiętać, bo gdybym tego nie zrobił, a jeszcze nie nadszedł czas na obsługę ruchu węża (czyli zegar nie doliczył maksymalnej liczby cykli) - klawisz byłby tracony; W takim wypadku aby skręcić wężem, trzeba by klawisz sterowania wciskać zawsze w ostatniej klatce, a to oczywiście uniemożliwiło by granie;

Więc z racji tej, że wciśnięty klawisz zawsze trzeba zapamiętać, kod znaku trzeba rozszerzyć do dwóch bajtów, ustalając inne wartości dla zwykłych-jednobajtowych klawiszy, a także inne dla dwubajtowych; Rozwiązałem to tak, że przechowywany jest kod znaku jako liczba UInt16; Młodszy bajt zawiera kod znaku, a starszy albo 0x00 dla jednobajtowego klawisza, albo 0xFF dla specjalnego; Do zbudowania takiego kodu znaku wystarczy prosta alternatywa na dwóch liczbach - 16-bitowej i 8-bitewej:

$0000 or $50 = $0050  // dla podstawowego klawisza P
$FF00 or $50 = $FF50  // dla klawisza strzałki w dół, który także posiada kod litery P

Dzięki temu np. strzałka w dół oraz klawisz P mogą mieć osobne funkcje i nie będą się nakładać; Kod już zmodyfikowałem w tym celu, no i działa prawidłowo; Obecna forma wygląda w ten sposób:

procedure TController.ProcessInput();
var
  chrInput: AnsiChar;
begin
  { check if the key was pressed }
  if KeyPressed() then
  begin
    { get the key from input }
    chrInput := UpCase(ReadKey());

    { detect extended key }
    if (Ord(FInputKey) = INPUT_KEY_NONE) and KeyPressed() then
    begin
      { get the extended key character }
      chrInput := UpCase(ReadKey());
      { build the extended key code }
      FInputKey := INPUT_EXT_KEY_CODE_PREFIX or Ord(chrInput);
    end
    else
      { build the standard key code }
      FInputKey := INPUT_STD_KEY_CODE_PREFIX or Ord(chrInput);

    { clear input buffer }
    while KeyPressed() do
      ReadKey();

    { validate input key }
    if ValidateInputKey(INPUT_SNAKE_MOVE_KEYS_ARR) or ValidateInputKey(INPUT_SPECIAL_KEYS_ARR) then
      Exit;
  end;

  { if key is not supported or key is not pressed - set the input null key }
  FInputKey := INPUT_KEY_NONE;
end;

deklaracje stałych prefiksów dla kodów klawiszy i ich typu poniżej:

type
  { double-byte type for input key codes }
  TInputKey = UInt16;

const
  { input key code prefix for standard key }
  INPUT_STD_KEY_CODE_PREFIX = TInputKey($0000);
  { input key code prefix for special key }
  INPUT_EXT_KEY_CODE_PREFIX = TInputKey($FF00);

To oczywiście wygląda bardziej na bajer niż najsensowniejsze rozwiązanie, dlatego że tak jak @Marooned zauważył - można czytać klawisze od razu jako liczby 16-bitowe, wykorzystując zdefiniowane kody klawiszy ze zbioru stałych z prefiksami VK_; Żeby skorzystać np. z GetKeyState musiałbym po pierwsze sprawdzić, czy ta funkcja jest uniezależniona od platformy (w 2009 roku jeszcze nie była) oraz przystosować kod tak, aby można było obsłużyć kilka klawiszy naraz; Tutaj pojawiło by się więcej warunków, bo trzymając wciśnięty klawisz trybu turbo (domyślnie jest to klawisz spacji) oraz skrętu węża, trzeba by skręcenie węża wykonać bez względu na stan timera; Z drugiej strony, jeśli klawisz turbo nie jest wciśnięty a klawisz sterowania jest - trzeba by go zapamiętać i obsłużyć tylko w przypadku, jeśli tajmer doliczył odpowiednią ilość cykli;

W każdym razie GetKeyEvent odpada, bo blokowałby program; Z tego co wyczytałem, ta funkcja oczekuje na klawisz, jeśli nie został wciśnięty, czyli zamroziłby grę; Jej odpowiednikiem, który nie blokuje programu jest PollKeyEvent, więc tę funkcję można by wykorzystać; Zaś samo zapamiętywanie musiało by być zmienione - zamiast zapamiętywania jednego klawisza, trzeba by pamietać wszystkie; Czyli zmienić FInputKey z pojedynczej liczby 16-bitowej, np. na kolejkę, którą trzeba by odpowiednio obsługiwać;

Oczywiście wszystko można zaprogramować, tylko że z założenia tryb turbo miał być domyślne możliwy do wykorzystania tylko w poruszaniu się w linii prostej, bez możliwości używania go również na zakrętach; Czytanie klawiszy jako znaków AnsiChar po pierwsze umożliwia w prosty sposób rozróżnić klawisze podstawowe od specjalnych, a po drugie samo z siebie wyłącza automatycznie tryb turbo, nawet jeśli nie zwolni się jego klawisza :]

Tu muszę się jeszcze zastanowić, czy umożliwić wykorzystanie tego trybu cały czas, czy tylko na prostych; Jeśli cały czas - kontroler i główna pętla gry pójdą praktycznie całe do modyfikacji;


Następnym elementen do modyfikacji jest obsługa macierzy typu TSnakeModulesArr w klasie TSnake; Nie wiem czym ja myślałem (chyba tym z tył na dole), ale obecnie każdorazowo po złapaniu złotka, macierz z pola FModules jest powiększana o jedno miejsce, czyli realokowana; Co złapanie złotka - relokacja macierzy, a im dłuższy wąż, tym dłużej ta relokacja będzie trwać; Przy bardzo długim wężu, składającym się przykładowo ze 100-150 modułów, ta relokacja może spowalniać grę; Jakiś dziwne spowolnienia przy testowaniu bardzo długiego węża już zauważyłem, ale nie śledziłem co jest tego przyczyną; Podejrzewam więc stratę czasu na wydłużanie macierzy z modułami;

Rozwiązanie jest takie samo, jak np. w przypadku alokacji pamięci dla wewnętrznej macierzy w klasie TStrings; Rezerwacja pamięci dla kolejnych n * 2 modułów, czy tam np. n + 16 modułów; Można by tez zaalokować od razu dla 819 modułów, bo tyle się zmieści na planszy (gdyby wąż zapełnił całą jej przestrzeń); Jak dobrze liczę, była by to statyczna alokacja 819 * SizeOf(TSnakeModuleRec), czyli 819 * 4, co daje nam 3276 bajtów (3,2KiB); Alokowanie macierzy o stałym rozmiarze całkowicie wykluczyłoby konieczność jej relokacji, a tym samym tracenie czasu pomiędzy kolejnymi klatkami gry;

Podsumowując - kolejny znaczący krok w optymalizacji i raczej to ostatnie rozwiązanie wykorzystam, bo 3,2KiB pamięci alokowane z góry raczej nie można nazwać "zasobożernością" :]

1

Fakt, że nie zaglądałem do kodu więc mogłem od razu napisać PollKeyEvent jako opcja nieblokująca.

Z 10 lat temu gdy pisało się gierki pod DOSa, widziałem bardzo banalny ale skuteczny sposób obsługi nawet i 50 klawiszy na raz - nawet jeśli klawiatura obsługiwała 3 na raz (tyle miałem max na mojej pierwszej, nie licząc ctrl/shift/alt). Autor budował tablicę na wszystkie obsługiwane znaki - dla scan kodów wystarczyła o rozmiarze 255.
Jeśli klawisz zostaje naciśnięty, wstawiamy w tablicę o indeksie klawisza 1, jak puszczony, dajemy 0. Fragment odpowiadający za poruszanie testuje sobie dane w tej tablicy (czy mamy turbo, czy mamy strzałkę w prawo etc).

To działa dla ciągłego poruszania. Średnio nadaje się jednak jeśli chcemy kolejkować klawisze, czyli stuknięcie kursora ma skręcić bohatera nawet jeśli stuknięcie jest krótkie, a w czasie trzymania klawisza nie odpali się kod poruszający postacią.

To tak gwoli teoretyzowania.
Peace

0

Muszę przyznać, że ciekawe rozwiązanie, upraszczające obsługę klawiszy;

Jeśli klawisz zostaje naciśnięty, wstawiamy w tablicę o indeksie klawisza 1, jak puszczony, dajemy 0. [...]

Można by zamieć 0 i 1 na wartości typu Boolean, albo wręcz wykorzystać tablicę alokowaną na 256 div 8 bajtach, zapalając i gasząc pojedyncze bity jako flagi stanu wciśnięcia;

Jeśli skusiłbym się na obsługę kilku klawiszy jednocześnie, to kolejkowanie raczej odpada - nie było by możliwości w łatwy sposób sprawdzać stan kilku klawiszy, nie zdejmując ich z kolejki; Mimo wszystko jeszcze się zastanowię nad tym sterowaniem, bo może lepszym wyjściem okaże się obsługa kilku klawiszy; Dziękuję za informacje.

0

Czas na kolejną, małą aktualizację; Trochę rzeczy pozmieniałem i usprawiniłem, dzięki czemu rozgrywka na pewno stanie się ciekawsza;


Przede wszystkim zmieniłem sterowanie wężem; Przystosowałem kod pod obsługę klawiszy rozszerzonych (specjalnych), w prosty sposób odczytując je jako dwubajtowe; Dzięki temu kody znaków strzałek nie gryzą się z podstawowymi literkami;


Druga sprawa to rozszerzona funkcjonalność trybu turbo; Wcześniej było tak, że aby skorzystać z tego trybu, trzeba było wcisnąć i trzymać albo klawisz sterowania, albo klawisz dedykowany (czyli klawisz spacji); Teraz jest inaczej; Przytrzymanie klawisza sterowania (strzałki) działa tak jak poprzednio - dopóki trzymamy klawisz, wąż będzie się poruszał z maksymalną prędkością; Natomiast wprowadzona została funkcja trwałego turbo, obsługiwanego klawiszem spacji; Wciśnięcie i puszczenie spacji aktywuje tryb trwałego przyspieszenia, a ponowne wciśnięcie i puszcze wyłącza; Dzięki temu klawiszami sterowania możemy wykorzystywać turbo na krótkich odcinkach, a trwałe turbo cały czas; Bez względu na to czy wąż idzie w linii prostej, czy chcemy nim skręcać, tryb turbo się w takim przypadku nie wyłączy sam;

W przypadku używania trwałego trybu turbo (za pomocą klawisza Spacji) trzeba uważać, bo wąż naprawdę zapierdziela i trudno go okiełznać; Granie stało się trudniejsze, ale to raczej dobrze :]


Następna sprawa to macierz przechowująca informacje o modułach węża; Zrezygnowałem całkowicie z manipulowania jej rozmiarem - rozmiar maceirzy typu TSnakeModulesArr jest zawsze statyczny; Dzięki temu nie ma w ogóle konieczności relokacji pamięci po każdym złapanym złotku, co wcześniej mogło lekko opóźniać grę; Teraz bez względu na długość węża, powiększany jest jedynie licznik jego modułów, czyli wartośc pola TSnake.FLength; Całość działa bardzo szybko;


Kolejną poprawkę naniosłem w klasie TGold; Wcześniej losowanie nowej pozycji było wykonywane tak, że po wylosowaniu nowych współrzędnych sprawdzane było, czy złotko znajdowałoby się na wężu lub w niedostępnym miejscu planszy; Jeśli oba warunki zostały spełnione, czyli złotko nie będzie znajdować się na wężu oraz znajdzie się w dostępnym miejscu planszy - losowanie było kończone;

Jednak zdażało się tak, że po złapaniu złotka, nowe było losowane zaraz obok niego - jedno pole obok; Przez przypdaek można było złapać dwa złotka w jednej linii, dzięki czemu pokazywały się na ciele węża obok siebie dwie bramki; I tutaj byłby problem, gdyby węża pokierować tak, że wchodzi w jedną bramkę, zakręca, staje na drugiej, zakręca i schodzi z drugiej bramki; Problem nie byłby ze sterowaniem, a z wyświetlaniem ogona węża; W takim przypadku nie można by wyświetlić "teownika", tylko trzeba by wyświetlić odpowiedni moduł węża;

Zamiast po raz kolejny rozbudowywać warunki w metodzie rysującej węża, przerobiłem lekko metodę losującą nową pozycję złotka; Teraz dodatkowo sprawdzane jest, czy poprzednie i nowe współrzędne wypadają koło siebie i jeśli tak - losowanie wykonywane jest powtórnie; Warunkiem zakańczającym losowanie jest odstęp co najmniej o jedno pole na osi X lub osi Y; Dzięki temu teraz nie ma możliwości, aby złotko wylosowane zostało jedno obok drugiego; To zwalnia z rozbudowy warunków dotyczących malowania ogona węża;


Z większych modyfikacji to by było tyle; Mniejsze - poprawienie komentarzy, dodanie i zmienienie nazw kilku stałych, zabezpieczenie przywrócenia ustawień konsoli oraz wyczyszczenia jej ekranu po zakończeniu gry (w głównym module programu);

Zmieniona została także klawiszologia:

klawisze strzałek | sterowanie wężem, włączenie/wyłączenie krótkiego trybu turbo
Spacja | włączenie/wyłączenie trwałego trybu turbo
P | zapauzowanie gry
R | zresetowanie gry
Esc | wyjście z gry
To by było na tyle zmian;


Kolejnych zmian będzie trochę i będą to duże zmiany, głównie w klasie TGame; Muszę przygotować kod do obsługi węża tak, aby można go było wykorzystać w wielu miejscach programu; Przyda się to do zrobienia różnych trybów gry - practice, time attack, battle i adventure;

Jeśli uda się skutecznie wydzielić kod do grania, zabiorę się za oprawę gry - czołówkę, menu, opcje ustawień, szybki tutorial o tym jak grać i co można, a czego nie można itd.; Najważniejsze już jest zrobione - wąż umie sam się poruszać, można nim sterować i obsługuje poprawnie klawisze oraz poprawnie zachowuje się na dowolnych planszach;

Co chciałbym jeszcze dodać - chciałbym sprzężyć logikę gry z nowymi funkcjami; Dorobić różne liczniki - licznik żyć, licznik punktów, licznik złapanych złotek (czyli długości węża) oraz licznik czasu rozgrywki; Jeśli mowa o licznikach, to chciałbym przed pokazaniem się planszy, pokazać ekran poprzedzający - numer/nazwę planszy, miniaturkę węża i ilość żyć (coś jak w Super Mario); Chciałbym jeszcze rozszerzyć funkcjonalność klasy rysującej, aby po złapaniu złotka wyświetlała się obok ustalona ilość punktów, która po jakimś czasie zniknie; To samo w przypadku przejść przez bramki - za każde przejście ekstra punkty; Coś jak w grach na pegasusa - po złapaniu czegoś pojawiała się ilość punktów, która po chwili znikała; To samo chciałbym zrobić w tej grze;


Aktualne źródła wersji 0.109 dostępne w załaczniku, a w drugim załączniku skompilowany plik wykonywalny, przeznaczony dla Windowsów;

Życzę miłego testowania i oczywiście jestem otwarty na wszelkie propozycje :]

1
Sopelek napisał(a)

Takie przerysowanie trawy co jedną klatkę byłoby bardzo męczące dla oczu i użytkownika.

Nie tylko dla gracza, ale także dla klasy TPainter; Jeśli już skusiłbym się na przemalowywanie trawki, to jedynym sensownym rozwiązaniem jest stworzenie osobnego tajmera i rysowanie co np. dwie sekundy, czyli co 48 klatek; Wszystko dlatego, aby nie obciążać metody rysującej i utrzymywać płynność gry;

Dzięki za sugestię - zobaczę jak by to wyglądało i co zrobić, aby wyglądało to jak najlepiej; Jeśli chcecie i umiecie to możecie zmodyfikować kod i pokazać tutaj jak według Was wyglądało by animowanie tej trawki; Źródła są w załaczniku :]

1

Źle wygląda gdy konsola ma ustawiony inny niż domyślny font (np. Luicida Console).

Można zacząć od tego:

uses 
  (...) Windows, jwawincon;

Const
 LF_FACESIZE=32;

type
 COORD = Record X,Y:Word; end;
 CONSOLE_FONT_INFOEX =
  record
   cbSize:Cardinal;
   nFont:Cardinal;
   dwFontSize:Coord;
   FontFamily:Cardinal;
   FontWeight:Cardinal;
   FaceName:Array[0..LF_FACESIZE-1] of WideChar;
  end;
 PCONSOLE_FONT_INFOEX=^CONSOLE_FONT_INFOEX;

function GetNumberOfConsoleFonts(): DWORD; stdcall; external 'KERNEL32.dll';
function GetConsoleFontInfo(hnd: Handle;bool: BOOL; uint: DWORD; fonts: PCONSOLE_FONT_INFO): BOOL; stdcall; external 'KERNEL32.dll';
function GetCurrentConsoleFontEx(hnd: Handle;bool: BOOL; var fontInfo: CONSOLE_FONT_INFOEX): BOOL; stdcall; external 'KERNEL32.dll';
function SetCurrentConsoleFontEx(hnd: Handle;bool: BOOL; fonts: PCONSOLE_FONT_INFO): BOOL; stdcall; external 'KERNEL32.dll';
function SetConsoleFont(hnd: Handle; index: DWORD): BOOL; stdcall; external 'KERNEL32.dll';

function GetFontFamilyName(FontFamily: Cardinal): string;
begin
  case FontFamily of
    FF_DECORATIVE: result := 'FF_DECORATIVE';
    FF_DONTCARE: result := 'FF_DONTCARE';
    FF_MODERN: result := 'FF_MODERN';
    FF_ROMAN: result := 'FF_ROMAN';
    FF_SCRIPT: result := 'FF_SCRIPT';
    FF_SWISS: result := 'FF_SWISS';
    else
      result := '???';
  end;
end;

// Check if current console font is "Terminal" and if not - correct it
procedure checkFont;
const
  MAX_FONTS = 40;
  TERMINAL_FACE_NAME: WideString = 'Terminal';
var
  handle: THandle;
  fontCount: DWORD;
  font: CONSOLE_FONT_INFO;
  fontex: CONSOLE_FONT_INFOEX;
  fontIdx: DWORD;
  fonts: array[0..MAX_FONTS-1] of CONSOLE_FONT_INFO;
begin
  fontCount := GetNumberOfConsoleFonts();
  if fontCount > MAX_FONTS then
    fontCount := MAX_FONTS;

  writeLn('Font count: ', fontCount);

  handle := GetStdHandle(STD_OUTPUT_HANDLE);
  if not GetCurrentConsoleFont(handle, TRUE, font) then
    raiseLastWin32Error;

  fontIdx := font.nFont;

  writeLn('Font index: ', font.nFont);
  fontEx.cbSize := sizeof(fontex);

  if not GetCurrentConsoleFontEx(handle, TRUE, fontex) then
    raiseLastWin32Error;

  writeLn('Font retrieved');

  writeLn('Font name: ', WideString(fontEx.FaceName));
  writeLn('Font family: ', GetFontFamilyName(fontex.FontFamily));

  if (fontEx.FaceName = 'Terminal') and (fontEx.FontFamily = FF_MODERN) then
    exit;

  writeLn('Font incorrect, running reconfiguration');

  fontex.FontFamily := FF_MODERN;
  move(TERMINAL_FACE_NAME, fontEx.FaceName, Length(TERMINAL_FACE_NAME)+1);

  if not SetCurrentConsoleFontEx(handle, TRUE, @fontex) then
    raiseLastWin32Error;
end;

begin
  checkFont;
end.

0 użytkowników online, w tym zalogowanych: 0, gości: 0