furious programming
2018-11-28 15:03

Pamiętacie mój dawny wpis na blogu, w którym opisałem kilka swoich przemyśleń na temat modernizacji składni Pascala? Traktował on m.in. o fuzji bloków kontrolnych, określeniu skoku iteratora pętli for oraz o deklaracji zmiennych w dowolnym miejscu ciała podprogramu, na wzór najpopularniejszych języków programowania.

No i ostatnia propozycja, czyli punkt B., właśnie wchodzi w życie – dla stałych i zmiennych, ogólnych i dla pętli. ;)

Introducing Inline Variables in the Delphi Language

The coming 10.3 version of Delphi introduces a very handy feature to the language, local inline variables with local scope and type inference.

The Delphi language in 10.3 has a fairly core change in the way it allows far more flexibility in the declaration of local variables, their scope and lifetime. This is a change that breaks a key tenet of the original Pascal language, but offers a significant number of advantages, reducing unneeded code in several cases.

Jeśli o mnie chodzi, to jest to świetna wiadomość.


Przez chwilę miałem nadzieję na to, że może jednak będę miał możliwość używania nowych konstrukcji – w końcu FPC cały czas jest w pełni kompatybilny ze składnią Delphi. Niestety moja nadzieja gaśnie z każdą chwilą, ze względu na wypowiedzi ważnych graczy w tym wątku

Wychodzi na to, że deweloperzy zmian nie doceniają, nie chcą implementować inline'owanych deklaracji zmiennych, a co gorsze (dla całego projektu, nie dla mnie), uznają tę nowość za dobry powód do zerwania kompatybilności z Delphi. Wolą zostać przy pradawnej, zardzewiałej składni oraz twierdzić, że jedyne słuszne i najlepsze rozwiązanie to to obecne. Taki beton, o który rozbija się w drobny mak większość sensownych pomysłów modernizacji składni. :/

Sam w dalszym ciągu uważam, że połączenie wewnętrznych deklaracji lokalnych zmiennych w połączeniu z inferencją typów to funkcjonalność, która powinna zostać dodana do składni już dawno temu i której mi ciągle cholernie brakuje.

#fpc #free-pascal #lazarus #delphi

karpov

Ogólnie nie jestem zwolennikiem deklaracji zmiennych gdzie się ma ochotę ale zrobienie wyjątku np dla pętli for byłoby świetnym rozwiązaniem for var i := 1 to 10 do writeln(i);

furious programming

Koniecznie z wykorzystaniem inferencji i koniecznie dla obu typów pętli – for to/downto do i for in.

for var Item in Collection do // miodzio

Już nawet tym bym się od biedy zadowolił…

furious programming
2018-09-01 01:49

No i nadszedł ten dzień – pisanie kodu projektu Deep Platformer właśnie zostało zakończone! ;)

W tym wpisie znajdziecie opis ostatnich funkcjonalności, które zaimplementowałem wczoraj i dziś, a także kilka zrzutów ekranu przedstawiających różne momenty w grze. Co prawda kodu już więcej nie będzie (chyba że drobne, ostateczne korekty), jednak jeszcze dużo pracy przed opublikowaniem platformera.


Mechanika świetlików

Sporo czasu wczoraj poświęciłem na zachowanie świetlików, a także na mechanizm umożliwiający ich zbieranie. Świetliki do tej pory potrafiły jedynie się unosić (zataczając malutkie kółeczko zgodnie z ruchem wskazówek zegara lub przeciwnie), a także mrugać swoim światełkiem. Implementując funkcjonalność ich zbierania, początkowo napisałem kod tak, aby po dotknięciu świetlika, ten po prostu znikał. Wyglądało to dość tandetnie, więc dodałem kolejny jego stan – wznoszenie się. Teraz, po dotknięciu świetlika, ten zaczyna wznosić się z coraz większą prędkością aż do zniknięcia poza obszarem kamery. Oczywiście respektują one kierunek siły przyciągania – jeśli ta działa w dół, to świetliki lecą ku górze, a jeśli przyciąga w górę, to lecą w dół. Są też wrażliwe na zmianę kierunku tej siły i jeśli zostanie ona zmieniona podczas ich wznoszenia się, świtliki stopniowo zwalniają i zaczynają popierdzielać w przeciwnym kierunku.


Liczniki

Następnie zabrałem się za stworzenie ostatniego, wizualnego elementu, mianowicie za liczniki (widoczne w lewym górnym rogu ekranu). Początkowo miał być tylko jeden licznik, pokazujący liczbę świetlików (zebranych/wszystkich). Ale coś pustawo było, więc wymyśliłem drugi – z liczbą zgonów bohatera (tak, zgonów, a nie żyć). Rozgrywka prowadzona jest na takiej samej zasadzie jak Limbo – można zabić bohatera mnóstwo razy i wciąż rozpoczynać od ostatniego checkpointu.

O ile świetlik to świetlik, tak liczbę zgonów bohatera powinien obrazować taki sprajt, aby mniej więcej wiadomo było czego ten licznik dotyczy. Bohater sam w sobie śmierci nie symbolizuje, więc trzeba było coś z tym zrobić. Przygotowałem więc trzy kolejne sprajty, które zawierają zwyczajne jego ciało z przymkniętym okiem, a w lewym górnym rogu kratkę # (nie jest ona jakoś szczególnie widoczna, jednak jasna niezbyt pasuje). Wygląda na wkurzonego, więc lepiej oddaje przeznaczenie tego licznika (tu: od bramki automatycznego zapisu).

Oczywiście bohater w liczniku jest tak samo animowany jak ten śmigający po platformach, ale jego animacja ogranicza się jedynie do mrugania. Dodatkowe sprajty można również wykorzystać do renderowania postaci gracza (jest ich tyle samo, mają ten sam rozmiar i też różnią się wyłącznie fazą mrugnięcia), czym się chwilę pobawiłem, jednak nie będę ich dla niego stosował, aby nie duplikować zawartości.

Drugi licznik ozdabia animowany świetlik, który nie porusza się po okręgu jak te istniejące na planszy, a jedynie mruga światełkiem. Aby nie mylił on gracza, stworzyłem osobne trzy sprajty specjalnie dla licznika – są takie same jak te podstawowe, tyle że z czarnym, wyróżniającym obramowaniem.

 
2.png
 

Tekst wyświetlany w liczniku zgonów to no, jeśli bohatera jeszcze nie ubito, lub np. 2x (dla dwóch zgonów). Licznik świetlików wyświetla tekst all, jeśli zebrano wszystkie, lub np. 1/3, dla jednego zebranego spośród trzech. Dla zerowej liczby zgonów i zebranych wszystkich świetlików, słowa no i all są wg mnie lepsze, niż 0x i 3/3.

Jeśli te liczniki mimo wszystko przeszkadzać będą graczowi, to istnieje możliwość ich ukrycia klawiszem Del.


Dodatkowe informacje

Liczniki były gotowe, więc skupiłem się na pozostałych tekstowych informacjach wyświetlanych na ekranie. Jednym z nich jest licznik klatek, który domyślnie wyświetlany jest w prawym górnym rogu i jest zawsze widoczny (nawet podczas zmiany jasności ekranu). Jednak w trybie debugowania (możliwym do uruchomienia również w wersji release, ale nie powiem jak), na ekranie wyświetla się też licznik obciążenia procesora1 oraz napis cheat mode. Aby wyglądało to jakoś spójnie, w trybie debugowania oba liczniki (klatkażu i obciążenia) wyświetlane są na dole ekranu, a w prawym górnym rogu renderowany jest tytuł trybu.

Licznik framerate'u pokazuje się i ukrywa klawiszem Ins. Jeśli tryb debugowania jest uruchomiony, to tym klawiszem pokazuje się lub ukrywa również licznik obciążenia i tekst cheat mode. Nie było sensu wyznaczania kolejnego klawisza do tych dodatkowych informacji. Poza tym i tak już pół klawiatury mam obsadzone funkcjami, w większości służącymi właśnie do debugowania.

 
7.png

[1] Licznik obciążenia procesora to nie do końca dobre określenie, bo gra praktycznie przez cały czas pracy pożera pełną moc jednego jądra. Licznik ten dotyczy wykorzystania czasu przydzielonego jednej klatce (w przybliżeniu 16.7ms) na obsługę logiki i wyrenderowanie obrazu bieżącej klatki. Jeśli gra pracuje w pełnych 60fps, to procentaż obciążenia zawsze będzie poniżej 100%.


Fabuła

Jak już wcześniej wspominałem, fabuła dzieli się na trzy rozdziały – tutorial, fabułę podstawową oraz dodatkową. Po tutorialu wszystkie liczniki zostają zresetowane. Natomiast po drugim rozdziale, czyli po rozgrywce podstawowej, składającej się z kilku światów, sprawdzane są statystyki gracza i jeśli sobie zasłużył, dostąpi zaszczytu zagrania na dodatkowym świecie (rozdziale trzecim).


Outro

Na podstawie omówionych wcześniej dwóch liczników, gra decyduje o tym, które outro pokazać. Kombinacji związanych z liczbą zgonów i liczbą zebranych świetlików oraz z liczbą skończonych rozdziałów jest siedem, więc tyle animacji dla outra trzeba będzie stworzyć. Ale do tego mam generator, więc nie potrwa to długo. Mam nadzieję, bo jeszcze nie wymyśliłem co owe outra mają ostatecznie zawierać. Do dyspozycji mam tekst i obrazki – coś się wymyśli.


Tryb debugowania

Deweloper może wszystko, oszukiwać też – stąd nazwa cheat mode. Bez tego trybu nie dałoby się wygodnie testować różnych mechanizmów, dlatego można go spotkać praktycznie w każdej grze, bez względu na jej wiek. W moim projekcie pojawił się jeszcze zanim skończyłem pierwszą funkcjonalność, czyli wyświetlanie samych warstw poziomów z możliwością przełączania się pomiędzy nimi. Pierwszą jego funkcją była możliwość manipulowania klatkażem, tak aby móc spowolnić odtwarzanie i dokładnie zbadać animację przełączania się pomiędzy warstwami.

W chwili obecnej, tryb ten pozwala na:

  • latanie bohaterem – grawitacja na niego nie działa, kolizje z otoczeniem nie są sprawdzane (klawisze W, A, S, D),
  • przełączenie warstwy – w dowolnym miejscu poziomu, do dowolnej warstwy (klawisze P i L),
  • zmianę kierunku przyciągania – również w dowolnym miejscu poziomu, ustawienie przyciągania w górę lub w dół (klawisze O i K),
  • zebranie lub przywrócenie wszystkich świetlików – klawisze I i J,
  • zwiększenie lub zmniejszenie liczby zgonów – klawisze U i H,
  • zadokowanie kamery w bieżącym położeniu i jej oddokowanie – klawisze Y i G,
  • pokazanie lub ukrycie warstw poziomu – dla każdej warstwy jeden klawisz, od 1 do 4, gdzie 4 to warstwa najbliższa (czarna), a 1 to warstwa najgłębsza, widoczna jedynie podczas zmiany warstwy,
  • pokazanie lub ukrycie bramek – klawisz 8,
  • pokazanie lub ukrycie świetlików – klawisz 9,
  • pokazanie lub ukrycie bohatera – klawisz 0,
  • spowolnienie lub przyspieszenie odtwarzania gry – klawisze Page Up i Page Down,
  • ustawienie minimalnego lub maksymalnego klatkażu – klawisze Home i End.

Klawiszy przeznaczonych do debugowania zawartości jest 25, a tych podstawowych raptem 11. :D


Zrzuty ekranu

To co widać na zrzutach to m.in. menu (niebieskie, z logo gry), wygląd poziomów dla tutoriala (niebieskie), a także pierwszy i drugi świat w rozdziale drugim, czyli w podstawowej fabule (odpowiednio turkusowy i zielony). Pierwszy turkusowy ma ciemniejszy obraz – tak wygląda ekran po zapauzowaniu. Wtedy to fizyka nie działa, a animowane jest jedynie mruganie bohatera oraz obiekty w licznikach (które przyciemniane nie są).

Na ostatnim zrzucie widać pokazaną bramkę z literką E (exit) – jest to bramka zakończenia poziomu, których może być wiele w każdej planszy. Są jeszcze inne bramki – do zmiany warstwy z literą L (layer), do zmiany kierunku siły przyciągania z literą G (gravity) oraz bramka automatycznego zapisu pozycji gracza z literką S (save). Gradientowe kwadraty z literkami bramek widoczne są tylko w trybie debugowania.


No nic, to tyle na dziś. Teraz pozostało stworzyć docelowe poziomy już z konkretnym wyglądem platform (bo póki co są one kwadratowe, zgodnie z hitboksami) i wygenerowanie binarek z docelowymi animacjami (intra światów, intra i outra poziomów, cutscenki itd.). Nie wiem ile mi to zajmie, ale mam nadzieję, że niezbyt długo.

Najwyższy czas już puścić projekt w świat i delektować się hejtem związanym z użytą technologią, brakiem wsparcia przyspieszenia sprzętowego i oldschoolowym podejściem do implementowania funkcjonalności samej gry – to dopiero będzie zabawa. Widzimy się na premierze – choć kto wie, może wysmaruję jeszcze jakiś wpis. ;)

#free-pascal #lazarus #platformer #retro

furious programming

W razie czego, beta-testerzy mogą się odzywać w peemkach – wymagane min. 100 pkt reputacji. ;)

furious programming
2018-08-26 03:53

W poprzednim wpisie na temat platformera pochwaliłem się nowym generatorem – do tworzenia plików binarnych z animacjami. Dzięki temu aby zmodyfikować jakąś animację, nie muszę rekompilować projektu, a jedynie wystarczy wygenerować nowy plik. Poszedłem o krok dalej i dziś skończyłem dwie rzeczy – głowną metodę iterującą po rozdziałach, światach i poziomach, a także zestaw klas tworzących drzewko fabuły, na której owa metoda się opiera.

Dziś nie będzie obrazków, bo graficznych nowości brak, za to wrzucę trochę kodu, coby wpis nieco pokolorować. ;)


Klasy fabuły

Właściwa część gry finalnie dzieli się na trzy rozdziały – tutorial, fabułę główną i dodatkową. Każdy rozdział może posiadać ekran tytułowy (opcjonalnie) oraz zawierać co najmniej jeden świat, a każdy świat co najmniej jeden poziom. Rozdziały są potrzebne, dlatego że gra musi wykonać pewne operacje po rozdziale pierwszym (tutorialu), a także po rozdziale drugim (fabule głównej).

Klasy opisujące drzewko fabuły zawierają metody do sprawdzania istnienia katalogów i plików binarnych, a także zwracają ścieżki tych plików, bazując na indeksie bieżącego poziomu. Mimo iż owe klasy opisują drzewo danych (tu: katalogów i plików), same nie tworzą stricte drzewiastej struktury. Klasa TStory posiada jedynie jeden obiekt klasy TStoryChapter (pomimo trzech rozdziałów), ten z kolei jeden obiekt klasy TStoryWorld (pomimo sześciu światów), a ten jeden obiekt klasy TStoryLevel. Aby końcowy obiekt opisujący bieżący poziom wiedział o który konkretnie chodzi, w akcesorach przekazywane są dane na temat rozdziału, świata i poziomu do klas niższego rzędu, a konkretniej wpisywane są do class varów.

Pomimo prostoty implementacji (wiem, nieco dziwnej), cztery obiekty opisują fabułę, składającą się z dowolnej liczby rozdziałów, światów i poziomów, a składnia dostępna dla obiektu Story jest dość przyjemna (co jest raczej kwestią subiektywną).


Metoda obsługi fabuły

Wyżej opisane klasy stanowią podstawę do iterowania po rozdziałach, światach i poziomach. Metoda z nich korzystająca, czyli ta obsługująca całą fabułę, zawiera raptem kilkanaście linijek wysokopoziomowego kodu:

procedure TGame.PlayStory();
begin
  if Terminated then Exit;
 
  Story.Reset();
 
  if Story.ChaptersExists then
  repeat
    Story.NextChapter();
    Story.InitChapter();
 
    if Story.Chapter.WorldsExists then
    repeat
      Story.Chapter.NextWorld();
      Story.Chapter.InitWorld();
 
      if Story.Chapter.World.IntroExists then
        PlayWorldIntro();
 
      if Story.Chapter.World.LevelsExists then
      repeat
        Story.Chapter.World.NextLevel();
        Story.Chapter.World.InitLevel();
 
        if Story.Chapter.World.Level.IntroExists then PlayLevelIntro();
        if Story.Chapter.World.Level.LevelExists then PlayLevelGame();
        if Story.Chapter.World.Level.OutroExists then PlayLevelOutro();
      until Story.Chapter.World.NoMoreLevels or Terminated;
    until Story.Chapter.NoMoreWorlds or Terminated;
  until Story.NoMoreChapters or Terminated;
end;

Trzy pętle – pierwsza śmiga po rozdziałach, druga po światach, trzecia po poziomach. Przed rozgrywką na każdym poziomie i po niej, możliwe jest obsłużenie animacji. Można to zrobić albo w celu wyświetlenia ważnych informacji (to zostanie użyte w tutorialu), albo do stworzenia przerw z pustym ekranem, dla estetyki (w postaci filmiku z jedną, krótką animacją, zawierającą jedynie czarne tło). Gra sprawdza czy odpowiednie pliki z animacjami istnieją na dysku i jesli tak, odtwarza je.


Sceny

Kod metody obsługi fabuły skończony, więc zabrałem się za kod konkretnych scen. W jednym z wcześniejszych wpisów podałem przykład kodu obsługi pojedynczej sceny (metoda PlayIntro). W każdej scenie aktualizuje się kilka rzeczy – zegar, klawisze, klatkaż (debug), stan gry i jej opcje, logikę i na koniec bufor ramki – w pętli, aż do zakończenia sceny. Przeniosłem więc te metody z klasy silnika i wrzuciłem je do bazowej klasy sceny. Dzięki temu każda scena może wywoływać ten sam zestaw metod, ale aby mogły wykonywać konkretne czynności, niektóre metody są wirtualne, sukcesywnie nadpisywane na drodze dziedziczenia.

Bazowa klasa wygląda w ten sposób:

type
  TScene = class(TObject)
  private
    procedure UpdateClockBegin();
    procedure UpdateClockEnd();
    procedure UpdateInput();
    procedure UpdatePerformance();
  private
    procedure UpdateWindowPlacement();
    procedure UpdateWindowInterface();
  protected
    procedure UpdateOptions(); virtual;
    procedure UpdateState(); virtual;
    procedure UpdateLogic(); virtual;
    procedure UpdateFrame(); virtual;
  protected
    function Finished(): Boolean; virtual; abstract;
  public
    procedure InitScene(); virtual;
    procedure DoneScene(); virtual;
  public
    procedure PlayScene();
  end;

i metoda PlayScene:

procedure TScene.PlayScene();
begin
  repeat
    UpdateClockBegin();
 
    UpdateInput();
    UpdatePerformance();
    UpdateOptions();
    UpdateState();
    UpdateLogic();
    UpdateFrame();
 
    UpdateWindowPlacement();
    UpdateWindowInterface();
 
    UpdateClockEnd();
  until Finished() or Game.Terminated;
end;

Dzięki temu, że każda klasa sceny nadpisuje metody wirtualne i w ten sposób definiuje własne, dodatkowe operacje, PlayScene może być w klasie absolutnie bazowej. Drugą bazową klasą jest ta dla wszystkich scen wyświetlających filmiki:

type
  TMovieScene = class(TScene)
  protected
    procedure UpdateState(); override;
    procedure UpdateLogic(); override;
    procedure UpdateFrame(); override;
  protected
    function Finished(): Boolean; override;
  public
    procedure InitScene(); override;
    procedure DoneScene(); override;
  end;

InitScene tworzy instancję klasy filmu, DoneScene ją zwalnia. Podczas odtwarzania filmu, możliwe jest jego przerwanie (co obsługiwane jest w metodzie UpdateState), iterowanie po klatkach animacji realizowane jest w metodzie UpdateLogic, a UpdateFrame pozwala wyrenderować klatkę animacji. Finished natomiast jest owym warunkiem końcowym, który dla każdej sceny jest inny.

Końcowa klasa sceny już konkretnego filmu, nadpisuje jedynie InitScene:

type
  TIntroScene = class(TMovieScene)
  public
    procedure InitScene(); override;
  end;

W niej ładuje do pamięci konkretny plik z filmem, na podstawie danych kryteriów. Bo np. intra są dwa i o tym który plik załadować (z długim czy krótkim filmem) decydują dane, przekazane w parametrach uruchomieniowych. Domyślnie jest długie intro.


Obsługa scen

Praktycznie cały kod odpowiedzialny za działanie scen jest wydzielony do bazowej klasy dla nich wszystkich. Dzięki temu aby np. obsłużyć całe menu główne, główna klasa gry woła raptem trzy metody dla tej sceny:

procedure TGame.PlayMenu();
begin
  if Terminated then Exit;
 
  Scenes.Menu.InitScene();
  Scenes.Menu.PlayScene();
  Scenes.Menu.DoneScene();
end;

InitScene inicjalizuje scenę i ładuje wymagane dane do pamięci, PlayScene uruchamia scenę i działa do jej zakończenia, a DoneScene zwalnia pamięć i ją zakańcza. I tak dla wszystkich postałych fragmentów gry – trzy metody i tyle. Wyjątkiem jest metoda obsługi rozgrywki – tu żongluje się trzema scenami, bo te poboczne potrzebują danych ładowanych do pamięci w tej głównej:

procedure TGame.PlayLevelGame();
begin
  if Terminated then Exit;
 
  Scenes.Story.World.Level.Game.InitScene();
    Scenes.Story.World.Level.FadeIn.InitScene();
    Scenes.Story.World.Level.FadeIn.PlayScene();
    Scenes.Story.World.Level.FadeIn.DoneScene();
  Scenes.Story.World.Level.Game.PlayScene();
    Scenes.Story.World.Level.FadeOut.InitScene();
    Scenes.Story.World.Level.FadeOut.PlayScene();
    Scenes.Story.World.Level.FadeOut.DoneScene();
  Scenes.Story.World.Level.Game.DoneScene();
end;

Aby to w ogóle było możliwe, sceny wrzucane są na stos (stos jako własny kontener) – dzięki temu podczas działania jednej sceny, można się wtrącić z kolejną. Ze względu na strukturę projektu, musiałem w ten sposób to wykonać, aby móc odpalić animację FadeIn i FadeOut (ta wymaga załadowanego do pamięci poziomu, co realizuje metoda Game.InitScene). Ten kod jeszcze zostanie rozwinięty, bo póki co nie obsługuje ani cutscenek, ani zgonu bohatera (po wpadnięciu w przepaść).


Podsumowanie

Ciekawostek podałbym o wiele więcej, ale limit długości wpisu nie pozwala… Wyszło naprawdę nieźle – jestem z tego kodu zadowolony. W końcu dodałem sceny FadeIn i FadeOut dla poziomów, co niestety nie było łatwe. Z tych scen korzystać będą też cutscenki, odpalane (opcjonalnie) po wpadnięciu w przepaść. Trochę poeksperymentowałem z tym kodem, używając dość nietypowych technik, np. wspomniane zmienne klasowe, czy wielopoziomowe zagnieżdżanie deklaracji klas (jedna w drugiej, a ta w kolejnej i w kolejnej). Wcześniej z tego nie korzystałem, więc sprawdziłem czy da się coś sensownego z tego wyczarować.

Niestety nie obeszło się bez wnerwiania… Głównie przez to, że moduł FGL nie zawiera klasy dla generycznego stosu (sic!). No i moje eksperymenty ze zmiennymi globalnymi namieszały nieco w grafie zależności modułów. Tak to jest, jak się implementuje wzorzec spider web™, gdzie wszystko korzysta z wszystkiego… Ostatecznie stos obiektów scen jest zwykłą, opakowaną listą generyczną, a brzydki wykres zależności modułów mam w dupie – kod działa prawidłowo, więc nie ma się czym przejmować (w końcu to i tak projekt testowy, bez planów na przyszłość).


Za brak with do nie bijcie – mnie tam długaśne wywołania metod (póki co) nie przeszkadzają. ;)

#free-pascal #lazarus #platformer #retro

czysteskarpety

@furious programming: to wstałeś pewnie dopiero, dzień dobry w takim razie :)

furious programming

No tak około 14:00 – dzień dobry wszystkim! :D

furious programming
2018-08-12 01:43

Projekt platformera nie umarł! Czas na kolejne ciekawostki – tym razem będzie śmiesznie… ;)


Przedwczoraj zająłem się spinaniem silnika gry z klasą zawierającą dane na temat fabuły. Jak już wspominałem, fabuła przewiduje kilka światów (plus ekrany tytułowe), każdy składający się z kilku poziomów. Dodatkowo, pomiędzy poziomami i podczas rozgrywki, przewidziałem możliwość wyświetlania cutscenek. Cutscenki to proste animacje, mogą wyświetlać tło w kolorze lub jako szum telewizyjny oraz posiadać dowolną liczbę elementów (etykiet i obrazków). Mogą pojawiać się z czerni, być widoczne w pełnym świetle oraz zanikać (do dyspozycji są łącznie trzy fazy, różniące się oświetleniem).


Animowanie poziomu

Kończyłem pisanie ostatniego mechanizmu obsługi scen, czyli właściwej sceny rozgrywki. A tu zonk – pojawiła się konieczność animowania poziomu, a konkretniej dwóch faz – wyłaniania się z czerni i zanikania – na potrzeby startu danego poziomu i jego zakończenia. Problem drugi – aby kod tej sceny był zgodny stylem z pozostałymi, powinien odwoływać się do danego obiektu sceny i jedynie wołać wysoce abstrakcyjne, dużo robiące metody. Tyle że animowanie czegokolwiek wymaga liczenia klatek, a takich obliczeń w obiektach scen nie wykonuję. Trzeba było wybrać – albo niepasujące do innych scen babranie się z licznikami, albo skorzystanie z gotowych klas do animacji, wyposażonych w licznik i przydatne metody. Wybrałem więc opcję drugą, ale problem w tym, że te klasy nie obsługują poziomu jako tła…


Cutscenki i ich wyjątkowość

Zostawiłem więc sceny, a skupiłem się na rozwinięciu kodu animacji. Cutscenki dotyczące jednego poziomu (rozpoczęcia, zakończenia i wewnętrzne) pierwotnie osadzane były w plikach z poziomami. Podczas ładowania poziomu do pamięci, wczytywane były wszystkie potrzebne dane – grafiki warstw, macierze hitboxów, dane dotyczące bramek itd. oraz właśnie te cutscenki. Skoro cutscenki dało się wczytywać z pliku, a inne animacje nie (intro, outro, ekran staffu itd.), to trzeba było coś z tym zrobić.

Klasa renderera animacji korzysta z renderera tekstu do malowania napisów, tyle że współrzędne etykiet np. dla intra miałem wrzucone do stałych, a całe intro było generowane z poziomu kodu, a nie ładowane z pliku. Problem numer trzy – nie było możliwości (na potrzeby cutscenek) ładowania z pliku czegokolwiek innego niż tekst i długość trwania animacji. Brak wsparcia obrazków, brak obsługi szumu jako tła, brak współrzędnych. Trzeba było rozwinąć klasy obsługujące animacje.


Animacje a pliki

Zacząłem więc dłubać przy animacjach, tak aby można było je ładować z plików. Wszystkie – intro, outro, staff i trzy typy cutscenek dla poziomów. Problem numer cztery – nie mam takiego generatora. Zostawiłem więc klasy animacji i skupiłem się na generatorze.


Nowy generator

Wymyśliłem strukturę zawartości konfigów, tak aby przewidywały wszystkie wymagane dane i stworzyłem nowy projekt dla nowego generatora. Współrzędne elementów animacji pierwotnie miałem wrzucone do stałych. Jeśli element miał być widoczny w lewym górnym rogu, to współrzędne były dwiema gołymi liczbami. Jednak jeśli coś miało być wyświetlone np. w prawym dolnym rogu, to wartości koordynatów obliczane były za pomocą innych stałych (tu: z rozmiarem ekranu).

Aby animacje z plików binarnych miały sens, powinny uwzględniać rozmiar ekranu, czyli oprócz offsetów, powinny również posiadać dane dotyczące wyrównania tekstu. Skoro animacja ma być umieszczona w binarce, to nie będzie dostępu do stałych z rozmiarem ekranu. Potrzebna jest więc możliwość wyrównywania tekstu do któregoś rogu lub centrowania go na ekranie. Problem numer pięć – renderer tekstu nie obsługuje wyrównania…


Brakujące wyrównywanie tekstu

Porzuciłem więc generator i wróciłem do głównego projektu. Trzeba było dorobić do metod renderera tekstu możliwość podania wyrównania. Póki co wspierał możliwość malowania napisów jednym fontem lub dwoma, łamania linii, pozwalał też malować tekst według konkretnych współrzędnych lub centrowania wewnątrz zadanego obszaru. Nijak się to miało do nowych wymagań. Przepisałem więc kod tak, aby metody obsługiwały dodatkowy parametr (align). Problem numer… sześć – renderer nie potrafi namalować poprawnie tekstu dwoma fontami, jeśli mają różne rozmiary. Nie było to potrzebne – wszystkie fonty były takie same, różniły się jedynie kolorem. Ale skoro mam rozwinąć kod, to przyda się też dorobić kolejną funkcję.


Bottom-up dobiegł końca

Totalnie przeorałem klasę renderera tekstu, tworząc go w pełni uniwersalnego. Teraz pozwala malować tekst jednym lub dwoma fontami, o takim samym rozmiarze lub o różnym, ze wsparciem prostego kerningu. Daje też możliwość malowania treści na podstawie współrzędnych lub wewnątrz obszaru, a także wyrównywania tekstu (do konkretnego rogu lub do środka obszaru). Obsługuje znaczniki łamania linii oraz znacznik swapu fontów. Wyszło nieźle.

Klasa malująca tekst gotowa, więc mogłem wrócić do generatora binarek z animacjami. Okazało się, że generowanie binarek z tak prostymi filmikami jest bardziej skomplikowane niż tworzenie plików z poziomami. Kodu generatora animacji wyszło jedynie 1200 linijek. Narzędzie to finalnie potrafi wypluwać pliki binarne z dowolną liczbą animacji (z dowolną liczbą faz, od jednej do trzech), a każda z nich może zawierać dane na temat tła i dowolnej liczby elementów.

Generator animacji skończyłem, przetestowałem – śmiga. Wygenerowałem sobie przy okazji binarki z dwiema częściami intra, które przydadzą się do testów, kiedy wrócę do rozwijania kodu klas obsługujących cutscenki. Przy okazji jeszcze odwiedziłem pozostałe cztery narzędzia, dodając do nich kilka przydatnych rzeczy, o których wcześniej nie pomyślałem (np. wyświetlanie krótkiej instrukcji obsługi, jeśli uruchomi się program bez parametrów). Ujednoliciłem też ich kod, tak aby wszystkie narzędzia były w tym samym stylu (same drobne poprawki). Teraz wyglądają i działają profesjonalnie.


Podsumowanie

I tak zleciał mi wczorajszy dzień – sporo poprawek, kupa kodu dla nowego narzędzia, reorganizacja struktury plików projektu, dorabianie binarek, ikon i plików readme.txt, oczyszczanie bieżącego kodu. Teraz, skoro już binarki z filmikami są gotowe, można wrócić do klas obsługujących animacje i dodać długo wyczekiwaną metodę – LoadFromFile. Następnie stworzyć klasę animacji poziomu, dodać dwie scenki dla jego pokazywania i zanikania, a na koniec dokończyć główną scenę rozgrywki. I to będzie finał – koniec pisania kodu, zostanie tylko stworzyć ostateczne binarki z poziomami i demówka będzie gotowa do publikacji.

Przy okazji rzuciłem okiem na zawartość katalogu z projektem tej platformówki – zawiera 142 podfoldery, 663 pliki, łącznie o wadze 40MB. A miał to być tylko prosty test przełączania warstw… taki skrumniutki… ;)

#free-pascal #lazarus #platformer #retro

furious programming
2018-07-27 23:23

W poprzednim wpisie opisałem prototyp głównego menu platformówki, które zamierzałem wykonać. No i właśnie ukończyłem prace nad tym menu. Większość z pierwotnych założeń została zrealizowana, jednak na drodze testów co nieco pozmieniałem.

Zrzut ekranu menu – click! (niemal identyczne jak prototyp, również w załączniku na dole).
Animacja intra i menu – click! (intro krótkie, animacja w miarę płynna).

Logo FPR stworzone na kształt loga pewnego producenta z czasów Famicoma. Retro-maniacy powinni poznać bez problemu.

Menu główne demówki składa się z kilku elementów. Tłem jest odpowiedniej wielkości zwykły poziom, z ustawionym bohaterem w wybranym miejscu. Logo gry to zwykła bitmapa, a tekst z instrukcją jest zwyczajnym, dynamicznie renderowanym tekstem. Renderowanie całego menu ogranicza się dosłownie do wywołania kilku metod, z których korzystają również inne sceny gry (przede wszystkim właściwa scena rozgrywki). Wszystkie sceny gry operują na różnych obiektach i ich wysokopoziomowych metodach, więc kilkoma linijkami można sterować sporym kawałkiem logiki. I o to chodzi.

We wcześniejszych wpisach informowałem, że przymierzam się do zrobienia przerywników przed poziomami. W dalszej części planowania zawartości gry, doszedłem do wniosku, że skoro ”fabuła” przewiduje kilka różnych światów, to każdy nowy świat powinien rozpoczynać się od ekranu tytułowego. Wymyśliłem wtedy, że taki ekran powinien składać się z tła w postaci poziomu w odpowiednim kolorze, gdzieś na ekranie powinien być widoczny stojący bohater, a na górze ekranu powinna być wyświetlona nazwa świata, niekoniecznie jako prosty napis, a więc najpewniej jako obrazek. Coś Wam to przypomina? ;)

Tak, każdy ekran tytułowy danego świata docelowo będzie zawierał sporo elementów, z których składa się główne menu demówki. Dlatego też stworzyłem nową klasę, która opisuje ogólnie mówiąc ekran tytułowy (czegokolwiek). Klasa ta zawiera pola do przechowywania instancji zwykłego poziomu i obiektu bitmapy (loga). Dodatkowo, przechowuje również dane na temat lokalizacji bohatera na planszy, pozycji kamery oraz pozycji loga. Pozwala także załadować te dane z pliku binarnego oraz zresetować ustawienia. Klasa głównego menu dziedziczy z bazowej klasy ekranu tytułowego, dodatkowo rozszerzając ją o informacje na temat animacji i dodatkowych elementów.


Menu najpierw wyłania się z czerni. Po skończeniu animacji pojawiania się, rozpoczyna się oczekiwanie na wciśnięcie spacji. W tym czasie tekst na dole ekranu miga powoli, a bohater dumnie stoi przy krawędzi i mruga oczami. Po wciśnięciu spacji, tekst zaczyna migać szybciej – przez kilka chwil. Następnie przestaje migać (odtąd jest cały czas widoczny) oraz w tej samej chwili bohater odwraca się i zaczyna iść w prawo, całkiem poza ekran. Ostatnią fazą jest animacja zaciemnienia menu, skalibrowana w taki sposób, aby bohater zdążył wyjść poza ekran.

Podczas oczekiwania na wciśnięcie klawisza, możliwa jest zmiana rozmiaru okna oraz skorzystanie z kilku przydatnych funkcji, dostępnych w trybie debugowania. Możliwe jest przesuwanie bohatera po ekranie (bez obsługi kolizji i grawitacji), blokowanie i odblokowywanie kamery (tak aby móc znaleźć odpowiednie jej ustawienie), pokazanie i ukrycie warstw poziomu i bohatera, a także wpływanie na klatkaż, przede wszystkim aby móc sprawdzać jak wyglądają poszczególne animacje menu w spowolnieniu. Te opcje pozwoliły mi zaprojektować sam poziom i wybrać odpowiednie ustawienie kamery.


Nową funkcją, o której wspomniałem we wcześniejszym wpisie, jest kolejna animacja bohatera – gdy gracz długo nim nie rusza. W grach na platformę Famicom po kilku sekundach bezruchu, odpalana była prosta animacja (na późniejszych konsolach jedna spośród kilku). Zrobiłem więc swój odpowiednik. Jeśli przez dziesięć sekund gracz nie rusza bohaterem, ten zamyka oczy i zasypia – powoli oddycha, kurcząc się i rozkurczając co sekundę, a nad głową wyświetlany jest napis zZzZ... – zrzut w załączniku. Funkcja ta dostępna jest również w menu głównym – wystarczy trochę poczekać.


Na razie to tyle – teraz trzeba się przyłożyć do kolejnej części gry, czyli do oprogramowania całej rozgrywki. To będzie dość trudne, dlatego że chcę wykorzystać wiele ficzerów, uatrakcyjniając całość.

Na pewno kolejną częścią gry po menu będzie krótki tutorial, który trzeba będzie zaliczyć, aby móc przejść do pierwszego świata. Tutorial będzie niczym innym jak kilkoma prostymi poziomami, poprzedzonymi krótkimi animacjami z instrukcją obsługi. Pozwoli to poznać klawiszologię, przećwiczyć poruszanie bohaterem i zapoznać się z różnymi obiektami (świetlikami oraz bramkami do zmiany warstwy i grawitacji).

No, wracam do pisania – do następnego. ;)

#free-pascal #lazarus #platformer #retro

furious programming
2018-07-24 19:35

Logika bohatera zaimplementowana, świetliki latają i migają – czas w końcu zabrać się za ostateczną strukturę kodu. Jak z poprzednich wpisów wiadomo, intro jest już gotowe, więc następne w kolejce jest główne menu gry.

Menu główne nie powinno być skomplikowane, ani też nie ma co do niego jakichś dużych wymagań. Ma być proste i przede wszystkim wyświetlać logo gry oraz informację o tym jak grę rozpocząć (a konkretniej: co wcisnąć). Po wciśnięciu odpowiedniego przycisku ma się odpalić gra (szczegóły innym razem).


1. Logo gry

Prosty napis – szału nima. Do tekstu z nazwą gry (biała część loga) wykorzystałem font Swiss Siena, natomiast pozostały tekst stworzony za pomocą NESFont (taki sam jak wszystkie inne napisy w grze). Dolny napis z instrukcją nie jest częścią loga – za niego odpowiada renderer tekstu. Tekst ten docelowo będzie wolno migać – na wzór starych gier.

2. Tło

Zamiast tworzyć statyczny obraz z tłem, wykorzystany zostanie silnik gry do wyrenderowania warstw przygotowanego specjalnie dla menu poziomu. Kolor bazowy (tła) poziomu raczej pozostanie taki jaki jest – miły dla oka niebieski. Na pewno widoczny też będzie bohater, którego zadaniem będzie stanie i ciapanie oczami – dla lekkiego urozmaicenia menu. Świetlików raczej nie będę dodawał, coby od razu nie zdradzać wszystkiego czym gra dysponuje.

3. Efekt wejścia i zakończenia

Efektem wejścia menu będzie zwykłe pojawianie się obrazu, z całkowicie czarnego do całkowicie kolorowego, tak jak w przypadku animacji intra. Natomiast do animacji zakończenia menu wykorzystam silnik gry. Po wciśnięciu spacji, logo, tekst i bohater znikną oraz rozpocznie się krótka animacja ciągłego przełączania warstw w górę (wizualnie do bliższej), połączona ze stopniowym zaciemnianiem obrazu. Ta animacja będzie trwać ze dwie sekundy – powinno wyglądać obłędnie.

Ewentualnie wymyślę coś innego.


Jeśli o nowe ficzery chodzi, to rozwinąłem renderer tekstu o możliwość formatowania napisów. Teraz istnieje możliwość namalowania jednego ciągu znaków za pomocą dwóch różnych fontów, tak aby móc wyróżnić pewne jego ważne fragmenty (przykład w prototypie menu, instrukcja na dole ekranu). Tak więc renderer na tę chwilę potrafi malować sformatowany tekst jednoliniowy lub wieloliniowy (obsługuje znaki nowej linii), a także pozwala wyrównywać tekst do lewej, do prawej, centrować go w zadanym obszasze, ew. z dodatkowym, pionowym offsetem.

Drugim nowym ficzerem jest wyświtlanie podpowiedzi po znalezieniu się w obrębie bramki. Stojąc na bramce zmiany warstwy, nad lub pod nią (w zależności od kierunku siły przyciągania) wyświetla się tekst deeper lub nearer. Stojąc na bramce zmiany grawitacji, wyświetla się tekst up (zawsze nad bramką) lub down (zawsze pod nią). Natomiast po znalezieniu się w obrębie bramki z wyjściem, pokazuje się tekst exit (również według kierunku przyciągania). Notka o tym gdzie bramka prowadzi, nieco pomaga w przemierzaniu poziomu.


Z nowości to tyle. Mam nadzieję, że dziś zdążę stworzyć gotowe menu, tak aby jutro zająć się najważniejszą częścią kodu, czyli połączeniem scen z klasą TStory, dzięki której silnik będzie nawigował po wszystkich światach i poziomach. Dziś wpis krótszy, bo te dłuższe chyba nie przypadły Wam do gustu (za dużo do czytania i pewnie mało komu się chciało). ;)

#free-pascal #lazarus #platformer #retro

furious programming

Doszukuję się korelacji pomiędzy długością wpisów (i stopniem szczegółowości opisów) a liczbą komentarzy i ocen. Z jednej strony, sama gra i jej kod nie dostarczą tylu informacji co te bardzo szczegółowe wpisy – wiele rzeczy opisywałem, ale ostatecznie w projekcie się nie znalazły. Wiele rzeczy było w projekcie, ale zostały usunięte lub znacznie zmodyfikowane. Jednak z drugiej strony, jeśli nikogo nie interesują takie ciekawostki, to samo ich przedstawianie raczej nie ma sensu.

Owej korelacji póki co nie zdiagnozowałem – niektóre krótkie wpisy cieszyły się mniejszym zainteresowaniem, a niektóre kobylaste bardzo dużym. I odwrotnie. Ten jest krótszy z bardziej ogólnymi opisami, ale nie dlatego że nie mam o czym pisać – samo menu to temat rzeka. ;)

furious programming

Przy okazji – nie zdążyłem wczoraj zaimplementować menu… ale dodałem jeszcze jeden ficzer związany z bohaterem, zapożyczony z famicomowych platformówek. O nim wspomnę w kolejnym wpisie. ;)

furious programming
2018-07-21 17:21

W poprzednim wpisie napisałem co nieco na temat wyglądu, zachowania i implementacji ruchu głównego bohatera, natomiast w tym napiszę co nieco na temat świetlików, których zbieractwem poniekąd trudnić się będzie nasz klocek.


Zacznijmy od wymagań. Celem bohatera w mojej demówce będzie przemierzanie wielowarstwowych poziomów. Każdy poziom będzie mniej lub bardziej zagmatwanym labiryntem, posiadającym punkt startowy i co najmniej jeden punkt końcowy. Samo szwędanie się po planszy było by nudne, więc zadaniem pobocznym będzie gromadzenie świetlików, rozrzuconych po całym poziomie. Pewna ich część będzie łatwodostępna (na zachętę), natomiast inne będą poukrywane w taki sposób, aby trzeba było nieco pogłówkować – dostać się do nich poprzez ukryte w ścianie przejście, z poziomu innej warstwy lub za pomocą odwracania grawitacji. Ich wygląd musi przykuwać uwagę – nie mogą to być elementy statyczne, więc trzeba użyć animacji.


Talentu artystycznego raczej nie mam, więc postanowiłem, że świetlik będzie po prostu kulką ”światła”. Aby przykuwał uwagę, światełko powinno co jakiś czas migać. Świecić, gasnąć, świecić, gasnąć – różne interwały, różne jasności, co wybrać… Przetestowałem różne animacje i wyszło na to, że najlepiej świetlik wygląda wtedy, gdy cały czas świeci pełną jasnością, a co jakiś czas na chwilkę przygasa.


Miganie światełek gotowe, ale to wciąż mało – świetliki zwykle nie lewitują w stałym miejscu, dlatego też przyda się subtelny ruch. Niewielki, o małej prędkości, aby nie denerwował oka. Biorąc pod uwagę to, że świetlik fizycznie powinien znajdować się wewnątrz obszaru danej kafli poziomu, wybrałem kilka prostych ścieżek ruchu:

  • wyłącznie w osi pionowej – lekko wznosi się, następnie opada, znów znosi się i znów opada,
  • ruch po ścieżce okrąkłej lub owalnej,
  • ruch po ścieżce w kształcie ósemki lub symbolu nieskończoności.

Wyszło mi, że najfajniej świetlik wygląda, jeśli przesuwa się po ścieżce okrągłej. Teraz implementacja.


Pozycją bazową świetlika są współrzędne określające środkowy piksel danej kafli poziomu (pseudośrodkowy, bo kafle są rozmiaru 16x16 pikseli). Bazowa pozycja nigdy się nie zmienia – ona będzie wykorzystywana do sprawdzania kolizji z bohaterem. Bieżący sprajt świetlika wyświetlany jest w zadanej odległości od punktu bazowego, dzięki czemu:

  • nie muszę tworzyć wielu sprajtów z różnym odchyleniem od punktu centralnego,
  • krótki kod logiki wyliczającej offset mogę przenieść do renderera (to samo zrobiłem w przypadku animacji zatapiania i wynurzania bohatera podczas jego chodzenia po platformie),
  • mogę kalibrować prędkość ruchu oraz promień ścieżki modyfikując kilka stałych (szybko i wygodnie),
  • w dowolnym momencie mogę zmienić kształt ścieżki, bez konieczności modyfikowania kolekcji sprajtów.

Wynikiem powyższego są wyłącznie trzy sprajty, różniące się ”jasnością” (w konsekwencji czego: wielkością kulki), kilka linijek kodu wyliczania kąta i offsetu umieściłem wewnątrz klasy renderera świetlików oraz zadeklarowałem kilka stałych z parametrami animacji. Aby nieco urozmaicić animację, dodałem jeszcze jeden parametr – kierunek ruchu. Świetlik może krążyć zgodnie lub przeciwnie z ruchem wskazówek zegara, albo nie krążyć wcale (to na potrzeby licznika świetlików, który wyświetlany będzie w lewym górnym rogu ekranu).


Kalibracja ruchu po okręgu znów pochłonęła trochę czasu – nie mogłem się zdecydować. Ostatecznie ustaliłem, że animacja uwzględniać będzie 30 różnych kątów odchylenia od kąta , pod każdym z tych trzydziestu kątów świetlik wyświetlany będzie prez 8 klatek (co daje pełne okrążenie w 240 klatek, czyli w cztery sekundy), natomiast promień wynosić będzie 2 piksele.

Jeśli o animację świecenia i przygasania chodzi, to postąpiłem pobodnie jak w przypadku mrugania – liczba klatek pełnego świecenia jest losowana i przeliczana na wartości 16, 32, 64, 128 lub 256, natomiast czas wyświetlania każdego sprajtu to 4 klatki. Czas trwania samej animacji przygaścięnia to 12 klatek, czyli 200ms.

Gotowy efekt fruwania i migania świetlików oraz mrugania bohatera tutaj – fireflies.webm


Mniej istotnym, ale mimo wszystko ważnym nowym ficzerem jest implementacja martwego pola kamery. Poruszanie się bohatera w obrębie tego pola nie powoduje przesuwania kamery, dzięki czemu rozgrywka wygląda jakoś ładniej. Ogólnie dead zone nie może być zbyt duży, bo gdyby tak było, to kamera przesuwałaby się tylko wtedy, gdy bohater podejdzie blisko bocznej krawędzi lub gdy mocno zbliży się do górnej/dolnej. To skutecznie utrudniłoby rozgrywkę, dlatego że na ekranie wyświetlany byłby tylko niewielki obszar poziomu, przez co nie wiadomo by było czy np. daną przepaść da się przeskoczyć, lub czy można spaść z platformy i w przepaść nie wpaść (masło maślane).

Niektóre stare platformówki niestety ale tak działały i przesuwały kamerę po znacznym zliżeniu się do krawędzi ekranu – np. Earthworm Jim na SNES-a. Niektóre gdy robiły to inaczej – kamera była ustawiana według kierunku ruchu w poziomie. Tak było to zrealizowane w grze Pitfall: The Mayan Adventure, na tę samą platformę. Uważam, jest to bardzo dobre rozwiązanie.

Możliwych funkcji kamery jest znacznie więcej – można bohatera centrować w kamerze, ale dać możliwość rozglądania się (czyli przesuwania samej kamery we wszystkich kierunkach), można też dynamicznie zwiększać/zmniejszać offset kamery od bohatera podczas trwania ciągłego ruchu (np. im dłużej bohater idzie, tym bardziej dosuwa się go do przeciwnej krawędzi ekranu). Ten pierwszy efekt był w niektórych grach wykorzystywany – np. w grze Aaahh!!! Real Monsters można było użyć grubasa do wystrzelenia jego oka w dowolnym kierunku, co skutkowało przykuciem kamery do niego (oka) i zobaczeniem co jest dużo dalej od bohatera, a za pomocą strzałek dodatkowo umożliwić odsunięcie kamery od oka. Śmieszne rozwiązanie, ale cholernie pomocne podczas eksploracji poziomów i szukania zaułków ze specjalnymi przedmiotami.

Sam jednak ograniczę się do niewielkiego martwego pola, bo już czas na deadline.


Animowanie i wyświetlanie różnych obiektów jest gotowe, sterowanie i klawiszologia także. Teraz pozostaje dodać wykrywanie kolizji bohatera ze świetlikami i aktualizowanie liczników. Po tym będzie już można stworzyć menu i outro, a następnie zabrać się w końcu za tworzenie konkretnych poziomów gry, co będzie ostatnim krokiem na drodze do wydania tej demówki. ;)

#free-pascal #lazarus #platformer #retro

furious programming
2018-07-18 04:46

Platformers coraz bogatszy. W poprzednim wpisie opisałem szczegóły dotyczące ruchu bohatera w pionie i poziomie, więc samo sterowanie miałem już przygotowane. Skupiłem się więc dziś na animowaniu postaci, tak aby przestała być jednokolorowym kwadratem na ekranie.


Pierwszą funkcją animacji bohatera jest chodzenie. Jak już wielokrotnie wspominałem, jednostką pomiaru czasu w silniku tej platformówki są klatki, a więc liczby całkowite – im dłużej ma dana czynność trwać, tym więcej klatek trzeba zliczyć. Tak więc aby móc animować chodzenie bohatera, należy liczyć klatki. Liczyć trzeba od momentu wciśnięcia przycisku ruchu w bok, a zakończyć po puszczeniu tego klawisza lub po spadnięciu z platformy. Ową animacją jest sinsunoidalne zatapianie i wynurzanie się ciała bohatera.

Wygląda to w ten sposób (trzy pierwsze unikalne, reszta dla podglądu, powiększenie trzykrotne):

walk.png

Każdy spajt z powyższej listy wyświetlany jest przez pięć kolejnych klatek, dzięki czemu animacja nie jest ani zbyt szybka, ani zbyt wolna. Aby wiedzieć o ile pikseli przyciąć grafikę, należy obliczyć resztę z dzielenia liczby klatek chodu przez liczbę faz przypadających na pełne zanurzenie i wynurzenie (czyli cztery), a następnie wynik podzielić przez liczbę klatek przypadających na pojedynczą fazę (w której rozmiar grafiki nie zmienia się – czyli pięć):

LWalkPhase := AHero.WalkFrameIndex mod (FRAMES_PER_HERO_WALK * FRAMES_PER_PHASE_HERO_WALK);
LWalkPhase := LWalkPhase div FRAMES_PER_PHASE_HERO_WALK;

W ten sposób uzyskuje się numer fazy chodu – faza pierwsza to brak zanurzenia, druga to zanurzenie o jeden piksel, trzecia o dwa piksele, a czwarta to znów o jeden piksel. Piąta jest równoznaczna z pierwszą – tu następuje zapętlenie animacji. Po obliczeniu rozmiaru przycięcia, wystarczy wyświetlić odpowiedzi fragment. Aby zrobić to prawidłowo, należy wziąć pod uwagę orientację bohatera (kierunek siły przyciągania).


Druga funkcja to rozciąganie bohatera podczas opadania. Tu klatek liczyć nie trzeba, albowiem stopień rozciągnięcia można wyznaczyć na podstawie prędkości wertykalnej – im większa prędkość, tym większe rozciągnięcie. Aby móc namalować rozciągniętego bohatera, należy obliczyć obszar jaki zajmować ma sprajt – a według tego co napisałem zdanie wcześniej, nie jest to trudne:

LArea := AHero.Location.Area;
LArea.Inflate(0, Abs(AHero.VerticalSpeed));

Po kilku minutach zabawy ustaliłem, że najlepiej wygląda bohater, gdy rozciągany jest w pionie wyłącznie wtedy, gdy faktycznie opada. Wznosząc się podczas skoku nie zmienia rozmiaru, po zakończeniu skoku (ale wciąż wznosząc się) też nie – dopiero kiedy osiągnie maksymalną wysokość i prędkość zacznie rosnąć zgodnie z grawitacją. Animacja samego opadania wygląda w ten sposób:

fall.png


Trzecią funkcją jest… mruganie oczkami. ;)

I znów, aby wiedzieć kiedy mrugać a kiedy nie, należy liczyć klatki. Na szczęcie najfajniej jest, gdy klocek mruga bez względu na wykonywaną akcję (stoi, idzie, skacze, spada itd.), więc wystarczy w kółko inkrementować licznik klatek i tak jak przy chodzeniu, obliczać fazę mrugnięcia, aby dało się wybrać odpowiedni sprajt dla animacji.

Mrugnięcie też składa się z czterech faz – pierwsza to otwarte oczy, druga to przymrużone, trzecia to zamknięte, a czwarta to znów przymrużone. Piąta jest taka sama jak pierwsza – animacja zapętla się. Duszki wyglądają tak (trzy pierwsze są unikalne – dalsze tylko dla podglądu):

wink.png

Obliczenie fazy mrugnięcia jest identyczne jak w przypadku liczenia fazy chodu, natomiast odstępy pomiędzy mrugnięciami nie są stałe, bo wyglądało by to tandetnie… Dlatego liczba klatek pomiędzy mrugnięciami jest losowana. Aby klocek mrugał raz częściej, a raz rzadziej, zamiast po prostu losować liczbę klatek z zadanego przedziału, losuję wartość od 4 do 8 i na jej podstawie przesuwam bitowo wartość 1:

FWinkWaitFrames := 1 shl RandomRange(4, 9);

To daje wartości 16, 32, 64, 128 lub 256, a więc bohater czasem mignie kolejny raz po 266ms, a czasem dopiero po kilku sekundach. Rozwiązanie trywialne, a finalny efekt bardzo ładny, bo nieprzewidywalny.


Czwartą i ostatnią funkcją jest mała modyfikacja wcześniej opracowanego efektu rozciągania podczas opadania. W trybie debugowania można w dowolnym momencie odwrócić kierunek grawitacji, natomiast w trybie standardowym, tylko jeśli bohater znajduje się na bramce i trzyma odpowiedni klawisz. Bramki mogą być również w powietrzu, więc możliwe jest odwrócenie siły przyciągania podczas opadania, a więc wtedy, gdy bohater ma rozciągnięte ciało. Aby animacja rozciągania wyglądała dobrze, trzeba było dodać również animację kurczenia.

To nieco utrudniło sprawę, bo bohater musiał być rozciągany nie tylko podczas opadania, ale także podczas wznoszenia, co było zablokowane ze względu na dwie pierwsze fazy skoku, w których rozmiar ciała bohatera miał być stały. Dlatego też trzeba było dodać nowy parametr (wartość logiczną), w którym zapisywana jest informacja o tym, czy klocek wznosi się po skoku. Jeśli po skoku to nie zmienia się długości jego ciała, a jeśli nie – zmienia się.


Animacji tyle, a teraz czas na malowanie grafik. Pomimo tylu różnych funkcji, bohatera opisują zaledwie trzy sprajty, a konkretniej te z fazami mrugania – pierwsza z otwartymi oczami, druga z przymrużonymi, a trzecia z zamkniętymi. Rozciąganie jego ciała to nic innego jak namalowanie rozciągniętej grafiki.

Pewnie się zastanawiacie, co z kierunkiem ruchu i orientacją według grawitacji. A no nic – grafikę można flipnąć w poziomie dla ruchu w lewo, a w pionie dla przyciągania w górę. Do tego celu napisałem dwie proste procedurki, dzięki którym nie muszę tworzyć kolejnych duszków:

type
  TRGBTriple = packed record
    B, G, R: UInt8;
  end;
 
type
  PRGBTripleArr = ^TRGBTripleArr;
  TRGBTripleArr = packed array [0 .. MaxInt div SizeOf(TRGBTriple) - 1] of TRGBTriple;
 
procedure FlipBufferHorizontal(ABuffer: TBitmap);
var
  LLine: PRGBTripleArr;
  LLineIndex, LPixelIndexLeft, LPixelIndexRight: Integer;
var
  LPixel: TRGBTriple;
begin
  ABuffer.BeginUpdate();
 
  for LLineIndex := 0 to ABuffer.Height - 1 do
  begin
    LLine := ABuffer.ScanLine[LLineIndex];
 
    LPixelIndexLeft := 0;
    LPixelIndexRight := ABuffer.Width - 1;
 
    while LPixelIndexLeft < LPixelIndexRight do
    begin
      LPixel := LLine^[LPixelIndexLeft];
      LLine^[LPixelIndexLeft] := LLine^[LPixelIndexRight];
      LLine^[LPixelIndexRight] := LPixel;
 
      LPixelIndexLeft += 1;
      LPixelIndexRight -= 1;
    end;
  end;
 
  ABuffer.EndUpdate();
end;
 
procedure FlipBufferVertical(ABuffer: TBitmap);
var
  LLineTop, LLineBottom: PRGBTripleArr;
  LLineIndexTop, LLineIndexBottom, LPixelIndex: Integer;
var
  LPixel: TRGBTriple;
begin
  ABuffer.BeginUpdate();
 
  LLineIndexTop := 0;
  LLineIndexBottom := ABuffer.Height - 1;
 
  while LLineIndexTop < LLineIndexBottom do
  begin
    LLineTop := ABuffer.ScanLine[LLineIndexTop];
    LLineBottom := ABuffer.ScanLine[LLineIndexBottom];
 
    for LPixelIndex := 0 to ABuffer.Width - 1 do
    begin
      LPixel := LLineTop^[LPixelIndex];
      LLineTop^[LPixelIndex] := LLineBottom^[LPixelIndex];
      LLineBottom^[LPixelIndex] := LPixel;
    end;
 
    LLineIndexTop += 1;
    LLineIndexBottom -= 1;
  end;
 
  ABuffer.EndUpdate();
end;

To wszystko – z finalnego efektu jestem bardzo zadowolony. Wisienką na torcie była zmiana sposobu malowania tła poziomu w kolorze – zamiast wypełniać je jednym kolorem, teraz wypełniane jest subtelnym, wertykalnym gradientem (ostatni załącznik).

Następne w kolejce do animowania są świetliki, czym zajmę się jutro. Póki co jeszcze nie wiem dokładnie jak mają wyglądać, ale na pewno też będą mrugać swoimi świecącymi zadkami, tak samo nieregularnie jak bohater mruga oczami. Na szczęście pomysłów nie brakuje. Do następnego. ;)

#free-pascal #lazarus #platformer #retro

Silv

@cerrato: :D Dziękuję; się cieszę, choć chyba nie powinienem z racji powodu takiego podejścia. ;)

cerrato

Szczerze? Lepsze jest takie podejście, niż (o wiele częściej spotykane) bycie idiotą, który jest przekonany, że jest cudowny, genialny, zabawny i wybitny. Żeby była jasność - ten idiota to nie było o Tobie.

furious programming
2018-07-13 01:39

Koniec prac nad moim skromnym platformerem nieuchronnie się zbliża. Skończyłem właśnie implementować sterowanie bohaterem, fizykę jego ruchu oraz obsługę kolizji z konturem poziomu. Nie było łatwo, więc dziś co nieco na ten temat. ;)


Podstawa to określenie specyfiki ruchu postaci.

Postanowiłem, że zaimplementuję funkcjonalność sterowania i ruchu bohaterem na wzór gry Chip 'n Dale Rescue Rangers z Famicoma. Głównym powodem jest prostota sterowania, ale też niższy stopień skomplikowania implementacji. Ruch horyzontalny zawsze jest stały – bez względu na to czy postać stoi, wznosi się czy opada, w każdej klatce albo nie jest aktualizowany, albo aktualizowany jest o stałą wartość (nie ma efektu przyspieszania i hamowania). Ruch wertykalny jest nieco skomplikowany, bo trzeba uwzględnić stopniową zmianę prędkości wznoszenia lub opadania. Ale jak to u mnie bywa, byle czym się nie zadowolę, a że na łatwiznę iść nienawidzę, to solidnie utrudniłem sobie robotę.

Pierwszym utrudnieniem jest wykorzystanie wyłącznie liczb całkowitych do opisu pozycji i prędkości postaci (à la Famicom). Po drugie, gra obsługuje przyciąganie grawitacyjne – bohater może być przyciągany ku dołowi ekranu lub ku górze (do zmiany kierunku siły przyciągania służą odpowiednie bramki, obsługiwane tak samo jak te do zmiany warstwy). Natomiast trzecim utrudnieniem jest obsługa trybu debugowania, który pozwala na przesuwanie (latanie) bohaterem w dowolnym kierunku, bez detekcji kolizji i przyciągania – dzięki temu możliwe jest szybkie dotarcie do miejsca, które należy przetestować (bez tego ani rusz).


Żeby nie komplikować kodu klasy do obsługi rozgrywki, logikę dotyczącą kolizji i fizyki obiektów wydzieliłem do osobnej klasy. Następnie zabrałem się za obsługę ruchu w poziomie. Bohater jest kwadratowy, więc za punkty kolizji przyjąłem jego rogi (nie użyłem tzw. hot spotu, jak to robiono dawno temu i o czym można przeczytać w artykule, który podlinkowałem w tym wpisie).

Sprawdzenie kolizji horyzontalnej ogranicza się do pobrania dwóch punktów (rogów z lewej lub prawej strony), przesunięcia ich o zadany offset, przeliczenie pozycji na współrzędne kafli i sprawdzenie w konturze, czy kafle określają lity blok czy powietrze. Jeśli lity blok to nową pozycję należy wyrównać do tego bloku. W razie wykroczenia poza widoczny obszar poziomu, współrzędne też należy wyrównać. Tyle wystarczy – jedyne co trzeba pozyskać to nową pozycję po wykonaniu ruchu, a to czy kolizja wystąpiła nie ma znaczenia. Z ruchem wertykalnym jest znacznie, znacznie więcej roboty…

Wykrywanie kolizji w pionie wygląda podobnie jak z tym w poziomie – bierze się dwa punkty, odpowiednio przesuwa, przelicza na współrzędne kafli, sprawdza kontur i w razie kolizji wyrównuje. Jednak w tym przypadku, w grę wchodzi jeszcze kierunek ruchu (w górę lub w dół), kierunek siły przyciągania (orientacja), bieżąca akcja postaci (stoi, skacze, wznosi się po skoku, opada lub lewituje) i jej umiejscowienie (jest na platformie lub w powietrzu). Sporo tego, jak na tak prostą z pozoru funkcjonalność…

Zacząłem od rzeczy najważniejszej, czyli od detekcji gruntu pod bohaterem. Gra musi to wiedzieć. Tu nie było trudno – jedna prosta metoda uwzględniająca kierunek siły przyciągania oraz kilka dodatkowych informacji w klasie bohatera. Jeśli bohater stoi, to nie trzeba nic przeliczać, a jeśli wisi w powietrzu, to trzeba umożliwić mu opadanie, czyli stopniowo zwiększać jego prędkość w pionie (według kierunku przyciągania). Jeśli bohater stoi i wciśnięto klawisz skoku, to trzeba ten skok wykonać – zaktualizować dane o bieżącej akcji, ustawić prędkość skoku według kierunku przyciągania i przesunąć obiekt o zadany offset. To nie wszystko – trzeba też sprawdzić czy świeżo po oddaniu skoku bohater walnął o ”sufit” i jeśli tak, to przerwać skok i rozpocząć opadanie (zaktualizować dane na temat akcji i wyzerować prędkość). A jakby tego było mało, musi istnieć możliwość wpadnięcia bohatera w przepaść (na dole lub u góry, zależy od przyciągania), więc trzeba i na to pozwolić. Warunków przybywało bez końca, ale dało się obsłużyć wszystkie przypadki bez większych problemów.


W końcu gra obsługiwała poprawnie wszystkie funkcje i przypadki: chodzenie, opadanie po zejściu z platformy, wznoszenie się po skoku i następnie opadanie. Obsługiwane były kolizje we wszystkich kierunkach, a także kierunek siły przyciągania. Bohater bardzo ładnie stopniowo nabierał prędkości podczas opadania, a ruch paraboliczny podczas skoku wyglądał znośnie. Jednak tu napotkałem na spory problem – animacja ruchu w pionie była płynna, jednak trwała zbyt krótko. Cała animacja skoku, wznoszenia i opadania była tak dynamiczna, że bardzo utrudniła sterowanie i trzeba było ją znacznie spowolnić…

Odpaliłem emulator z grą Chip 'n Dale i dokładnie wybadałem ruch wiewiórki (wizualnie, nie pod debuggerem emulatora). Chwila zabawy wiewiórką, chwila moim klockiem i z powrotem. W mojej demówce bohater wznosił się podczas skoku dużo szybciej niż wiewiórka, również dużo szybciej nabierał prędkości podczas opadania. Po drugie, po zakończeniu skoku, zbyt łagodnie zwalniał wznoszenie, przez co parabola ruchu była zbyt ”okrągła”.

Znowu teoria… Skok dzieli się na trzy fazy:

  • wznoszenie się o stałej prędkości (kiedy gracz trzyma klawisz skoku),
  • po osiągnięciu pełnej wysokości lub wcześniejszym puszczeniu klawisza skoku, następuje stopniowe zwalnianie wznoszenia,
  • swobodne opadanie aż do lądowania lub wpadnięcia w przepaść.

Zamiast stopniowo zmniejszać prędkość po fazie pierwszej, zrobiłem w ten sposób, że po osiągnięciu maksymalnej wysokości skoku, prędkość wznoszenia dzielona jest na pół. Dzięki temu faza druga trwa o połowę krócej, ale w dalszym ciągu wyglądając dobrze (a nawet lepiej). Aby spowolnić przyspieszenie podczas opadania, prędkość zwiększana jest według siły przyciągania nie w każdej klatce, a co cztery klatki. Doszedłem do tego, że nabieranie prędkości pionowej bazuje na dwóch parametrach – kierunku siły przyciągania (wartość 1 dla przyciągania w dół lub -1 dla przyciągania w górę) oraz częstotliwości aktualizacji tej prędkości (im rzadziej, tym wolniesze nabieranie prędkości podczas opadania).


Kalibracja ruchu w pionie zabrała mi trochę czasu – trzeba było nadać stałym takie wartości, aby jak najbardziej zbliżyć się do ruchu wiewiórki z gry, na której się wzorowałem. Nie chodziło tylko o to, aby wypracować odpowiednią parabolę ruchu, ale też o to, aby pasowała do struktury poziomu. Bohater musiał potrafić skoczyć na odpowiednią wysokość – określiłem, że musi wskoczyć na murek wysokości trzech kafli. I tak jak w oryginale, skoczyć można na różną wysokość – im dłużej przytrzyma się spację, tym wyżej bohater skoczy.


To tyle na dziś, nudnawo, bez zrzutów czy filmików – sam programistyczny bełkot. Na osłodę, coby nie było tak jałowo, niżej zamieszczam bieżącą wersję pięciu najważniejszych, wysokopoziomowych metod realizujących ruch w czterech kierunkach. Pierwsze trzy pochodzą z klasy obsługującej ruch gracza, a kolejne z klasy obsługi rozgrywki, używające tych wcześniejszych.

W razie problemów, błędów czy chęci upiększenia, co nieco mogę jeszcze poprawić. Do następnego. ;)

procedure THeroMovement.MoveLeft(AHero: THero; ALayer: TLayer);
begin
  AHero.Location.Left := HitLeft(AHero, ALayer, HERO_SPEED_MOVE);
  AHero.Attractable := True;
 
  if AHero.Standing and not Standing(AHero, ALayer) then
    AHero.Fall();
end;
 
procedure THeroMovement.MoveRight(AHero: THero; ALayer: TLayer);
begin
  AHero.Location.Right := HitRight(AHero, ALayer, HERO_SPEED_MOVE);
  AHero.Attractable := True;
 
  if AHero.Standing and not Standing(AHero, ALayer) then
    AHero.Fall();
end;
 
procedure THeroMovement.MoveVertical(AHero: THero; ALayer: TLayer; AGravity: Integer; AJump, AFreshJump: Boolean);
var
  LHit: Boolean;
begin
  if AHero.Flying and not AHero.Attractable then
    if AJump or AFreshJump then
    begin
      AHero.Attractable := True;
 
      if Standing(AHero, ALayer) then
        AHero.Land()
      else
        AHero.Fall();
    end
    else
      Exit;
 
  if AHero.Standing and AFreshJump then
  begin
    AHero.JumpStart();
    AHero.VerticalSpeed := HERO_SPEED_JUMP * -AGravity;
  end;
 
  if AHero.Jumping then
  begin
    if AHero.JumpFrameIndex >= FRAMES_PER_HERO_JUMP then
      AHero.JumpFinish()
    else
      if AJump then
        AHero.NextFrameJump()
      else
        AHero.JumpFinish();
 
    AHero.Location.Top := HitVertical(AHero, ALayer, LHit);
 
    if LHit then
      AHero.Hit();
  end
  else
  begin
    AHero.NextFrameFall();
 
    if AHero.FallFrameIndex.Divisible(FRAMES_PER_HERO_FALL) then
      AHero.VerticalSpeed := (AHero.VerticalSpeed + AGravity).TrimToRange(HERO_SPEED_FALL);
 
    AHero.Location.Top := HitVertical(AHero, ALayer, LHit);
 
    if LHit then
      AHero.Land();
  end;
end;
 
procedure TSceneGame.UpdateHero();
begin
  with Level, Keyboard.BasicKeys do
  begin
    if MoveLeft.IsPressed  then Movement.Hero.MoveLeft(Hero, CurrentLayer);
    if MoveRight.IsPressed then Movement.Hero.MoveRight(Hero, CurrentLayer);
 
    Movement.Hero.MoveVertical(Hero, CurrentLayer, Gravity, Jump.IsPressed, Jump.IsFreshPressed);
  end;
end;
 
procedure TSceneGame.UpdateCheatHero();
begin
  if Keyboard.CheatKeys.FlyLeft.IsPressed  then Movement.Hero.FlyLeft(Level.Hero, Level.CurrentLayer);
  if Keyboard.CheatKeys.FlyRight.IsPressed then Movement.Hero.FlyRight(Level.Hero, Level.CurrentLayer);
  if Keyboard.CheatKeys.FlyUp.IsPressed    then Movement.Hero.FlyUp(Level.Hero, Level.CurrentLayer);
  if Keyboard.CheatKeys.FlyDown.IsPressed  then Movement.Hero.FlyDown(Level.Hero, Level.CurrentLayer);
 
  // ...
end;

#free-pascal #lazarus #platformer #retro

Spine

A ja teraz mam 16 GB :)

furious programming
2018-06-26 17:43

Kolejne funkcjonalności w platformersie zaimplementowane – dziś w końcu o właściwej części demówki, czyli o rozgrywce. ;)

Dla tych co nie chcą czytać całości – w załącznikach znajdują się zrzuty poziomów w różnych kolorach, a tutaj nagranie z ruchu kamery z trybu debugowania. Co prawda nagranie troszkę się przycina, jednak ogólnie widać wszystko co chciałem pokazać.


Jak wcześniej wspominałem, platformówka ma wspierać efekt paralaksy. Podczas rozgrywki, mają być widoczne trzy warstwy, gdzie każda dalsza ma być odpowiednio zeskalowana i zabarwiona na odpowiedni kolor. Podczas ruchu kamery, najbliższa (zawsze czarna) warstaw ma się poruszać najszybciej, a każda dalsza odpowiednio wolniej – standard. Niby prosta arytmetyka, a jednak dała mi w kość. Ale nie chodzi tu o sam ruch warstw w różnym tempie, a o specjalny efekt, na temat którego szczegółów wcześniej nie podawałem.

Mowa o możliwości przeniesienia bohatera do bliższej lub dalszej warstwy – dzięki temu płaska platformówka nabiera głębokości, nie tylko tej wizualnej, ale też powierzchniowej. Dlaczego uważam tę funkcjonalność za ciekawą i wartą uwagi?

Przyjrzyjmy się zwyczajnym, płaskim platformówkom. Mając do dyspozycji tylko jedną warstwę, po której porusza się bohater, trzeba rozciągnąć planszę do rozmiaru, który utrudni graczowi poruszanie się i kolekcjonowanie jakichś przedmiotów. Tworzy się swego rodzaju płaski labirynt. Im większy poziom, tym więcej chodzenia i więcej do zapamiętania. Jednak w dalszym ciągu wszystkie ”pomieszczenia” oraz drogi są cały czas widoczne. Aby ukryć i utrudnić rozgrywkę, stosuje się różne efekty – np. w grze Aaahh!!! Real Monsters stworzono ściany, przez które można było przejść.

Dając możliwość chodzenia po wielu warstwach, tworzy się kolejny wymiar, silniej zbliżając do trójwymiaru. Nawet niewielki, acz głęboki wielowarstwowy poziom jest trudniejszy do pokonania niż duży, ale płaski poziom jednowarstwowy. Nasuwa się wiele pytań:

  • coś jest głębiej, ale co?
  • jak głęboko można pójść?
  • widzę, że na dalszej warstwie jest coś ciekawego, ale jak tam dotrzeć?
  • prowadzi tam jedna droga, czy kilka?
  • a jak już dotrę tam, to jak wrócić?

Łącząc wielowarstwowość poziomu ze ścianami, przez które można przejść, tworzy się nie lada łamigłówkę – i test tej funkcjonalności jest głównym powodem powstawania niniejszej demówki.

No dobrze, czas na trochę implementacyjnych szczegółów.


Każda warstwa to zwykła, monochromatyczna bitmapa1. Obrazy wszystkich warstw posiadają ten sam rozmiar. Im dalsza warstwa (podczas rozgrywki), tym większy jej obszar musi być widoczny, bo jest skalowana. Współczynnik skalowania ustaliłem na dwa kafle, czyli 32px – efekt paralaksy jest ładny, dalsze warstwy przewijają się w odpowiednim tempie. To powoduje, że widoczny obszar warstw rośnie o 64px idąc wgłąb. Na ekranie zawsze widoczne są trzy warstwy – jest to kompromis pomiędzy ładnym wyglądem a efektywnością (nieco więcej o tym w dalszej części wpisu).

Aby najdalsza widoczna warstwa zawsze widoczna była w całości, poziom posiada obliczony margines, który brany jest pod uwagę podczas ruchu kamery. Im więcej widocznych warstw, tym większy margines. Gdyby marginesu nie było, po przesunięciu kamery do rogu poziomu, offset lewej i/lub górnej krawędzi najdalszej warstwy były by ujemne, przez co widać by było po bokach puste przestrzenie.

Co do kolorystyki, klasa renderera platform wykorzystuje jeden kolor, który pobiera z klasy reprezentującej poziom. Tło poziomu zawsze wypełniane jest tym kolorem, natomiast dla warstw, ciemniejszy odcień obliczany jest na bieżąco. Najbliższa warstwa zawsze jest czarna, a im dalsza, tym bardziej zbliża się do podstawowego koloru. Tworzy to efekt mgły – ładnie wygląda, a gracz w danym momencie widzi tylko tyle, ile potrzeba. A to ważne, bo dany poziom może mieć dowolną liczbę warstw i malowanie większej liczby warstw zarżnęło by procesor.

Jeśli chodzi o efekt zmiany warstwy, to jest on troszkę skomplikowany. Niezłą zagwostką jest obliczanie obszarów widocznych warstw oraz odcieni dla nich. Klasa poziomu posiada właściwość logiczną określającą czy trwa zmiana warstwy, czy nie. Posiada też dwie inne właściwości – kierunek zmiany (do dalszej lub bliższej warstwy) a także numer klatki, od której rozpoczęła się zmiana. Obie te właściwości wykorzystywane są do obliczeń widocznego obszaru warstw oraz koloru. Warstwy dalsze renderowane są w pętli, natomiast najbliższa, czyli ta która jest przybliżana aż do zniknięcia (dla przejścia do dalszej warstwy) lub która się pojawia (dla przejścia do bliższej warstwy) renderowana jest osobno – dla niej obliczenia są inne. Aby jej zanikanie lub pojawianie się wyglądało dobrze, obszar z każdą klatką animacji odpowiednio maleje do prawie zerowego lub od prawie zerowego rośnie. Dla dalszych warstw współczynnik zmniejszenia jest stały.

Docelowo zmiana warstwy możliwa będzie za pomocą specjalnych bramek. Będą to po prostu kafelki, w których obszarze trzeba będzie się znajdować i wcisnąć strzałkę w górę (aby przejść do dalszej warstwy) lub w dół (aby przejść do warstwy bliższej). Każda bramka będzie umożliwiać przejście w jednym kierunku (bliżej lub wgłąb). Aby móc wrócić tą samą drogą, sąsiadujące warstwy muszą posiadać bramki o przeciwnych kierunkach i o tych samych współrzędnych. Wizualnie bramki te będą po prostu dziurą w obrazie warstwy, ozdobioną odpowiednio, tak aby informowały o swoim przeznaczeniu.

Wszystkie te parametry, czyli liczba widocznych warstw, stopnień pomniejszenia dalszych warstw, rozmiar marginesów, stopień przyciemnienia koloru podstawowego, liczba klatek trwania animacji zmiany warstwy itd. znajdują się w kilku stałych. Silnik respektuje je i na ich podstawie wykonuje obliczenia, dzięki czemu kalibracja wyglądu poziomu oraz animacji zmiany warstwy ogranicza się właśnie do zmiany wartości tych kilku stałych. W sumie to dwóch stałych – liczby widocznych warstw oraz czasu trwania animacji, bo wartości pozostałych stałch są obliczane na ich podstawie. Zechcę pokazać na ekranie nie trzy, a pięć warstw – zmieniam wartość jednej stałej, a reszta, czyli inne stałe i silnik dostosowują się. W ten sposób w kilka chwil można zmienić zachowanie całej gry, co wykorzystałem do określenia ostatecznego wyglądu – trzy widoczne warstwy, współczynnik zoomu to 32px, a czas trwania animacji zmiany warstwy to 250ms, czyli piętnaście klatek.


Jeśli chodzi o mniej znaczące nowości, mocno zmniejszyłem natywną rozdzielczość bufora ramki. Teraz wynosi ona 14x9 kafli, czyli 224x144 piksele. Zmieniłem ją głównie dlatego, aby móc malować więcej widocznych warstw (bo renderowanie przeskalowanych obszarów w zadanym kolorze to dość czasochłonna operacja), a także aby uwidocznić pikselozę. Z racji zmniejszenia rozdzielczości, pozbyłem się wcześniej używanego fontu – literki były za duże. Nadal jednak gra korzysta z dwóch fontów w dwóch kolorach (biały i szary), tyle że tego samego kroju – mowa o NESFont. No i w dalszym ciągu wygląda fajnie.

Natywną rozdziałkę bufora zmieniłem na bardziej panoramiczną. Poprawiłem też sposób renderowania obrazu na płótnie okna. Teraz w trybie pełnoekranowym bez dopasowywania obrazu do ekranu, obraz gry może być malowany z czarnymi pasami po bokach (dla ekranu bardziej panoramicznego niż bufor ramki) lub z pasami u góry i u dołu (dla ekranów bardziej kwadratowych i tych pionowych).

Dodałem też nowy rozmiar okna. Teraz okno może przyjmować rozmiar natywny (czyli 224x144 piksele), powiększony dwa razy, trzy razy, cztery razy (ten jest nowy) i zgodny z rozmiarem ekranu (z utrzymaniem proporcji lub z rozciągnięciem na pełen ekran). Domyślny tryb to zoom3x czyli okno w rozmiarze 672x432 piksele.

Widoczny na filmiku tryb debugowania służy do przeglądania poziomów i testowania różnych ich funkcji (ruch kamery, animacja zmiana warstwy itd.). Jako ciekawostkę podpowiem, że z poziomu kodu można go włączyć na stałe (za pomocą kompilacji warunkowej), tak aby mieć możliwość debugowania dowolnej część gry, ale pozostanie on również w wersji produkcyjnej, do aktywacji w pewien specjalny, klasyczny sposób. ;)


Na dziś to tyle – paralaksa wyszła super, dokładnie tak jak chciałem, więc teraz czas na bohatera i inne obiekty.

[1] Póki co obrazy poziomów składają się z kwadratów, zgodnych z tzw. konturem warstw (kontur zawiera informacje na temat tego, które kafelki to bloki, a które to puste przestrzenie). Obrazy te mogą posiadać dowolne kształty, nie tylko kwadratowe kostki, jednak te przydadzą się do testowania kolizji bohatera ze ścianami.

#free-pascal #lazarus #platformer #retro

kmle

Rozumiem, coś jak budowanie swojego komputera z bramek logicznych. Jakiś fun z tego jest.

furious programming

Coś w tym stylu, mniej więcej.