furious programming
2019-02-14 17:11

A czy Ty debugujesz swoje programy gamepadem? ;)

Lazarus IDE with Gamepad on Linux

The example below uses the MXK program to map eight buttons from an NES-style gamepad to the key combinations most commonly used during debugging. In addition it has useful mappings for the directional pad, but these are commented out by default since a rogue event source can render a system unusable.

[…]

Linuksiarze są nienormalni…

#free-pascal #lazarus

cerrato

Ciekawe, czemu nie da się tego odpalić na Windowsie...

Azarien

Pad na zdjęciu jest nie od NESa tylko od Super Famicoma, japońskiej wersji SNESa (nie NESa)

furious programming
2019-02-10 03:14

Biorąc pod uwagę odpowiedź kolegi @xxx_xx_x na temat optymalizacji algorytmu renderowania linii skanowania ekranu opisanego przeze mnie na blogu kilka dni temu, skusiłem się na własny komentarz w tej sprawie. Powodem istnienia niniejszego wpisu jest fala niezrozumienia moich słów przez @xxx_xx_x i jego słów przeze mnie (coś nie mogliśmy się dogadać), ale też chęć dokładnego sprawdzenia wszystkich wariantów.

Proszę tego wpisu nie traktować jako ataku personalnego i nie napędzać shit-stormu. ;)


1. Przedmiot testów

W wyniku dyskusji w komentarzach pod moim wpisem łącznie uzbierały się trzy sposoby na obróbkę obrazu źródłowego. Pierwszy to moja propozycja, wykorzystująca iterację piksel po pikselu i modyfikująca składowe za pomocą przesunięcia bitowego. Druga propozycja to iteracja w ten sam sposób, piksel po pikselu, jednak modyfikująca składowe na podstawie predefiniowanej palety wartości. Trzecia propozycja padła z palców @xxx_xx_x i dotyczy iteracji porcjami czterobajtowymi, modyfikując inty za pomocą przesunięcia i alternatywy bitowej.


2. Warunki testu

Aby mieć jakieś dane, posłużę się testową aplikacją konsolową, która sprawdzi efektywność każdej wersji algorytmu. W przypadku sugestii związanej z modyfikacją bitmapy za pomocą czterobajtowych intów, posłużę się wersją bez dopełnienia, co wyjdzie na korzyść @xxx_xx_x. Obrabianą w testach grafiką będzie obraz podany w moim wpisie w punkcie 1., powiększony dwukrotnie (czyli o rozmiarze 1344x768 pikseli) oraz drugi o takim samym rozmiarze, ale składający się z pikseli o losowych kolorach.

Aby mieć pewność co do poprawności działania wszystkich wersji, odpowiednie testy porównujące istnieją w kodzie źródłowym.


3. Który sposób jest szybszy?

Początkowo sprawdziłem działanie wszystkich trzech wersji bezpośrednio w mojej grze i na tej podstawie napisałem jak sprawa wygląda. Z tego też względu w ostatnim komentarzu podałem wyniki w procentach, bo w ten sposób moja gra mierzy zużycie mocy obliczeniowej. Najśmieszniejsze jest to, że te procentowe wyniki dotyczą czasu renderowania klatek w grze, a nie czasu pracy samego filtru renderującego linie skanowania ekranu.

Gra jako aplikacja testowa nie jest zbyt wiarygodna, głównie ze względu na możliwość przekłamania wyników przez inne jej mechanizmy. Mimo wszystko wersja zasugerowana przez @xxx_xx_x wypadła najlepiej, modyfikując bitmapę w najkrótszym czasie.


4. Nieporozumienia i problemy

Pierwszym problemem są poniższe słowa z wpisu.

Spotkała się z krytyką:

  1. Ze względu na brak podzielności bitmapy przez 4 i tym samym brak możliwości użycia kodu w sposób uniwersalny dla każdej bitmapy

Początkowo nie zrozumiałem o co chodzi z tym dopełnieniem, więc uznałem ten sposób za nieuniwersalny. Dopiero później zrozumiałem na jakiej zasadzie ma się odbywać dopełnienie, a tym samym już nie negowałem uniwersalizmu tego rozwiązania.

  1. Rozwiązanie z paletą jest szybsze.

No nie, nie jest. Nie wiem czy te słowa wynikają z niezrozumienia mojego komentarza na ten temat, jednak aby rozwiać wszelkie wątpliwości, napisałem w tym komentarzu od której wersji jest szybsze. A szybsze jest od mojej wersji z przesunięciem bitowym, nie od wersji z intami.

Jednak najważniejszym problemem nie jest wzajemne niezrozumienie, a zgoła inne środowisko testowe. Mój wpis traktuje stricte o algorytmie zaimplementowanym we Free Pascalu, obrabiającym obraz reprezentowany przez obiekt klasy TBitmap. Mój ”oponent” natomiast wykorzystał inny język, inny kompilator i inny, dużo prostszy sposób przechowywania danych obrazu (którego nie da się odzwierciedlić w moim przypadku).

Skoro test przeprowadzany jest w zupełnie innych warunkach, to jego wyniki mogą nie być reprodukowalne.


5. Test w Lazarusie

Żeby wiedzieć jak faktycznie wygląda efektywność poszczególnych rozwiązań, przeprowadziłem własne testy. Aplikacja porównująca napisana jest we Free Pascalu i tak jak w moim wpisie, operuje na obiektach klasy TBitmap. Tester wykorzystuje trzy obiekty bitmap:

  • źródłowy – oryginalna bitmapa, nie podlegająca obróbce,
  • testowy – kopia, podlegająca obróbce za pomocą wszystkich trzech algorytmów,
  • docelowy – kopia, poddana obróbce oryginalnym algorytmem, służąca do walidacji poprawności działania pozostałych rozwiązań.

Aby uzyskać precyzyjne czasy pomiaru, wykorzystałem systemową funkcję QPC i zmierzyłem tylko właściwe pętle, modyfikujące zawartość bitmap. Efektywność wszystkich trzech rozwiązań mierzona jest wielokrotnie. Test przeprowadzony w trybie release, dla wszystkich ustawień optymalizacji. Pełen kod aplikacji testowej znajduje się na Pastebin, a niżej podaję kod samych pętli modyfikujących – dla ogólnego rozeznania.

Oryginalna, używająca przesunięcia bitowego:

for LineIndex := 0 to LinesCount do
begin
  LineTriple := TestFrame.ScanLine[LineIndex];

  for PixelIndex := 0 to PixelsCount do
    with LineTriple^[PixelIndex] do
    begin
      B := B shr 1;
      G := G shr 1;
      R := R shr 1;
    end;
end;

Zaproponowana, używająca predefiniowanej palety wartości:

for LineIndex := 0 to LinesCount do
begin
  LineTriple := TestFrame.ScanLine[LineIndex];

  for PixelIndex := 0 to PixelsCount do
    with LineTriple^[PixelIndex] do
    begin
      B := Palette[B];
      G := Palette[G];
      R := Palette[R];
    end;
end;

Zaproponowana, modyfikująca czterobajtowe bloki:

for LineIndex := 0 to LinesCount do
begin
  LineBlock := TestFrame.ScanLine[LineIndex];

  for PixelIndex := 0 to PixelsCount do
    LineBlock^[PixelIndex] := LineBlock^[PixelIndex] shr UInt32(1) and UInt32($7F7F7F7F);
end;

Rezultaty pomiarów:

typ optymalizacji    brak   -O1    -O2    -O3    -O4

wariant z shiftem    34151  34160  32204  32212  32214
wariant z paletą     33127  33145  33152  33148  33152
wariant z intami     19755  19795  19863  19869  19876

Podsumowanie

Jak widać wersja modyfikująca czterobajtowe bloki jest szybsza, jednak nie trzykrotnie – nawet nie dwukrotnie. Co ciekawe, oryginalne rozwiązanie ładnie poddaje się optymalizacji, wersja z predefiniowaną paletą jest na nią odporna, natomiast efektywność wersji z intami jest w wyniku podnoszenia siły optymalizacji delikatnie obniżana.

Modyfikowanie bloków pamięci jest najlepszym rozwiązaniem – z dopełnieniem czy bez, tutaj znaczącej różnicy nie będzie. Przy czym jeszcze lepszy wynik zapewne można uzyskać wykorzystując 64-bitowe inty w 64-bitowym środowisku.

#free-pascal #lazarus

Bartosz Wójcik

Po prostu przepisz to na SSE2 zamiast używać takich przestarzałych kobył jak StretchBlt.

furious programming

Jednak nie – docelowo wołana jest StretchMaskBlt z widgetsetu. Zastanowię się nad zmianą.

furious programming
2019-02-07 23:25

Dawno temu umieściłem wpis na temat optymalizacji procedury przyciemniającej bieżącą klatkę, którą wykorzystywałem w moim Platformerze we wszelkich animacjach pojawiania się i zanikania obrazu scen. Dzięki znacznemu przyspieszeniu działania owej procedury, nie trzeba było rezygnować z niektórych graficznych elementów czy wręcz znów zmniejszać rozdzielczości natywnego bufora ramki.

Dziś też napiszę co nieco o optymalizacji procedury graficznej. Co prawda posłużę się zrzutami z platformera do pokazania efektów wizualnych, jednak funkcjonalność ta nie będzie zaimplementowana w mojej grze, ze względu na jej zbyt dużą siłę ssącą. Troszkę szkoda, ale cóż – narzucone ograniczenia nie pozwalają (przynajmniej na słabszych komputerach).

Tu należy nieco doprecyzować. W platformerze bez problemu można zastosować omawiany niżej filtr i nie będzie on miał praktycznie żadnego narzutu – problemem jest sam sposób renderowania obrazu w oknie. Obecnie każda klatka renderowana jest na buforze o niewielkim rozmiarze, który to malowany jest na płótnie okna w sposób umożliwiający jego rozciągnięcie. Aby był sens dodania tego filtru, należałoby go zastosować w końcowym obrazie klatki, zgodnym z rozmiarem okna. Konieczne byłoby wykorzystanie kolejnego tylnego bufora, jednak o dużo większym rozmiarze niż te istniejące (zgodnym z wielkością okna, a więc nawet w rozmiarze całego ekranu), co dramatycznie wydłużyłoby czas generowania obrazów.

1. Scan Lines

Tak, przedmiotem tego wpisu będzie obróbka obrazu, której wynikiem będą śliczne, ciemne poziome linie, sprawiające wrażenie, jakby obraz był wyświetlony na ekranie kineskopowym. W sieci można znaleźć masę szablonów z tego typu efektem, jednak w tym wpisie opiszę algorytm w wersji dość prostej, a mimo to dający całkiem przyzwoity efekt końcowy. Wydajność tego jest kluczowa i będzie bardzo wysoka, jednak kosztem braku możliwości konkretnej kalibracji i wierności końcowych odcieni.

Przykładowym obrazem poddawanym obróbce będzie poniższa klatka:

0.png


2. Charakterystyka algorytmu

Głównym elementem efektu linii skanowania są mocno przyciemnione poziome linie, renderowane w pewnym odstępie od siebie. Zwykle wykorzystuje się schemat, w których każda kolejna linia jest przyciemniona nieco inaczej – jedna bardziej, druga mniej. Samo przyciemnienie jednak nie wystarczy (obraz końcowy wyszedłby dużo ciemniejszy), dlatego niektóre linie rozjaśnia się – również w różnym stopniu, jednak w tym przypadku nieznacznie. To sprawi, że linie skanowania będą widoczne również na bardzo ciemnych i czarnych obszarach. Aby nie utracić ogólnej jakości barw, niektóre linie powinny pozostać w postaci niezmienionej.

Omawiana procedura będzie przyciemniać pierwszą linię, lekko rozjaśniać kolejną, a następną pozostawi bez zmian – i tak w kółko. Za podstawę pisania jej kodu posłuży poniższy szablon i pomocnicze typy danych:

type
  TRGBTriple = packed record
    B, G, R: UInt8;
  end;

type
  PRGBTripleArr = ^TRGBTripleArr;
  TRGBTripleArr = packed array [0 .. MaxInt div SizeOf(TRGBTriple) - 1] of TRGBTriple;

procedure DrawScanLines(ABuffer: TBitmap);
begin

end;

3. Przyciemnianie linii

Przyciemnienie kolorów pikseli to nic innego jak obniżenie wartości wszystkich jego składowych o zadany procent (lub na podstawie innej skali). Jednak obliczenia związane z procentami nie są konieczne – wystarczy wartość każdej składowej koloru wejściowego potraktować przesunięciem bitowym w prawo. To tylko jedna prymitywna operacja, więc zmiana wartości składowej będzie bardzo efektywna. Jednak ma ona spore ograniczenia, bo wartość bajtu (bo taki jest rozmiar składowej) można przesunąć co najwyżej o jedną, góra dwie pozycje – większe przesunięcie da odcień zbyt bliski czerni.

Skorzystajmy więc z przesunięcia o jedną pozycję w prawo:

procedure DrawScanLines(ABuffer: TBitmap);
var
  LineDarker: PRGBTripleArr;
  LineIndex, PixelIndex: Integer;
begin
  ABuffer.BeginUpdate();
  LineIndex := 0;

  while LineIndex < ABuffer.Height - 1 do
  begin
    LineDarker := ABuffer.ScanLine[LineIndex];

    for PixelIndex := 0 to ABuffer.Width - 1 do
      with LineDarker^[PixelIndex] do
      begin
        B := B shr 1;
        G := G shr 1;
        R := R shr 1;
      end;

    LineIndex += 3;
  end;

  ABuffer.EndUpdate();
end;

Modyfikacja składowych pikseli w ten sposób da poniższy efekt:

1.png


4. Rozjaśnianie linii

Z rozjaśnianiem kolorów pikseli jest nieco trudniej, dlatego że jest to algorytm dużo bardziej złożony niż ten dotyczący przyciemniania (w stosunku do wersji bazującej na floatach). W innym swoim projekcie wykorzystuję poniższe obliczenia, jednak funkcja je wykonująca używana jest raz na ruski rok, więc nie musi być efektywna (i za sprawą operacji na liczbach zmiennoprzecinkowych nie jest).

// wzór dawno temu ukradziony z internetów
B := Trunc(B * (100 - APercent) / 100) + Round(255 - (100 - APercent) / 100 * 255);
G := Trunc(G * (100 - APercent) / 100) + Round(255 - (100 - APercent) / 100 * 255);
R := Trunc(R * (100 - APercent) / 100) + Round(255 - (100 - APercent) / 100 * 255);

Nie będę się jednak pastwił nad tymi obliczeniami. Co prawda można zmienić skalę z procentażu na zakres wartości bajtu i pokombinować ze zamianą dzielenia i mnożenia na przesunięcia bitowe, ale tu trzeba podejść nieco inaczej – w końcu na efektywności nam zależy najbardziej.

Znów potrzebujemy triku, który pozwoli rozjaśnić kolor, maksymalnie oszczędzając moc obliczeniową procesora. Tu z pomocą przychodzi operacja alternatywy bitowej – za jej pomocą możemy zapalić kilka najmłodszych bitów, lekko zwiększając wartości składowych.

Ale tutaj też mamy niewielkie pole manewru, ze względu na bajtowy rozmiar składowych. Jeśli zapalimy zbyt wiele bitów, a tym samym mocno zwiększymy wartości składowych, w rezultacie dostaniemy za bardzo przekłamane odcienie. Niewielkie rozjaśnienie bez widocznego defektu wypaczenia kolorów można uzyskać zapalając trzy najmłodsze bity każdego komponentu piksela.

Kod odpowiedzialny za samo rozjaśnianie wybranych linii źródłowego obrazu:

procedure DrawScanLines(ABuffer: TBitmap);
var
  LineBrighter: PRGBTripleArr;
  LineIndex, PixelIndex: Integer;
begin
  ABuffer.BeginUpdate();
  LineIndex := 0;

  while LineIndex < ABuffer.Height - 1 do
  begin
    LineBrighter := ABuffer.ScanLine[LineIndex];

    for PixelIndex := 0 to ABuffer.Width - 1 do
      with LineBrighter^[PixelIndex] do
      begin
        B := B or %00000111;
        G := G or %00000111;
        R := R or %00000111;
      end;

    LineIndex += 3;
  end;

  ABuffer.EndUpdate();
end;

Efekt końcowy jest całkiem zadowalający – miłe, leciutkie rozjaśnienie:

2.png


5. Postać końcowa procedury

Skoro mamy już określone sposoby przyciemniania i rozjaśniania linii, teraz wystarczy je połączyć w jeden algorytm. Aby w głównej pętli iterującej po liniach obrazu nie nadźgać niepotrzebnych warunków, dobrym rozwiązaniem będzie jednoczesna obróbka dwóch sąsiadujących ze sobą linii (trzecia jest pomijana, bo ma mieć oryginalne odcienie).

Z tego właśnie powodu indeksacja głównej pętli we wcześniejszych przykładach pomijała ostatnią linię obrazu. Zrobiłem to z myślą o postaci końcowej, której kod znajduje się niżej i w którym ze względu na brak warunku wewnątrz głównej pętli służy do zabezpieczenia przed wykroczeniem poza zakres indeksów linii.
procedure DrawScanLines(ABuffer: TBitmap);
var
  LineDarker, LineBrighter: PRGBTripleArr;
  LineIndex, PixelIndex: Integer;
begin
  ABuffer.BeginUpdate();
  LineIndex := 0;

  while LineIndex < ABuffer.Height - 1 do
  begin
    LineDarker := ABuffer.ScanLine[LineIndex];
    LineBrighter := ABuffer.ScanLine[LineIndex + 1];

    for PixelIndex := 0 to ABuffer.Width - 1 do
    begin
      with LineDarker^[PixelIndex] do
      begin
        B := B shr 1;
        G := G shr 1;
        R := R shr 1;
      end;

      with LineBrighter^[PixelIndex] do
      begin
        B := B or %00000111;
        G := G or %00000111;
        R := R or %00000111;
      end;
    end;

    LineIndex += 3;
  end;

  ABuffer.EndUpdate();
end;

I efekt końcowy (pełna klatka w ostatnim załączniku):

3.png


Podsumowanie

Przedstawiony sposób renderowania linii skanowania ekranu nie jest jakoś szczególnie ładny czy wierny, jednak mimo wszystko stanowi namiastkę tego, co za dawnych czasów było normą. Oczywiście minusem tego rozwiązania są dość spore ograniczenia związane z możliwością kalibracji siły rozjaśniania i przyciemniania (oraz dość prymitywny, hardkodowany wzorzec), ale w tym algorytmie ważniejsza od elastyczności była efektywność.

Jak wspomniałem wcześniej, mój platformers nie będzie wykorzystywał tego filtru ze względu na i tak zbyt dużą czasochłonność działania tej procedury. Wszystko dlatego, że jedyny sens implementacji takiego filtru to obróbka końcowego buforu ramki o rozmiarze zgodnym z rozmiarem okna. Nie tylko wydłużyłby się czas renderowania klatki przez samą procedurkę malującą linie, ale też przez konieczność wykorzystania kolejnego tylnego buforu o rozmiarze zgodnym z rozmiarem okna i namalowania rozciągniętego natywnego buforu na tym docelowym.

Tak więc trzeba się obejść smakiem. ;)

#free-pascal #lazarus #retro

furious programming

Co do tej palety to mylisz się. Sposób zaproponowany przez @ŁF jest szybszy – przetestowałem go i faktycznie jego efektywność jest nieco wyższa w stosunku do tego co podałem we wpisie (czyli sam shr 1), bez zmiany konstrukcji pętli.

furious programming

@xxx_xx_x: porównałem wszystkie trzy sposoby.

Mój wykorzystujący przesunięcie bitowe dla przyciemniania i alternatywę dla rozjaśniania wykonuje się w 50-51%; sposób dotyczący predefiniowanych palet to 48-49%; a Twój z działaniami na całych intach około 48%. Tak więc ciut szybciej niż to do czego sam doszedłem, natomiast jakiejś konkretnej przewagi nad wariacją z paletami nie widać.

Twoja propozycja sprowadza się do poniższego kodu:

procedure DrawScanLines(ABuffer: TBitmap);
type
  PRGBIntArray = ^TRGBIntArray;
  TRGBIntArray = array [0 .. MaxInt div SizeOf(Int32)] of Int32;
var
  LineDarker, LineBrighter: PRGBIntArray;
  LineIndex, PixelIndex: Integer;
begin
  ABuffer.BeginUpdate();
  LineIndex := 0;

  while LineIndex < ABuffer.Height - 1 do
  begin
    LineDarker := ABuffer.ScanLine[LineIndex];
    LineBrighter := ABuffer.ScanLine[LineIndex + 1];

    for PixelIndex := 0 to (ABuffer.Width * 3) div SizeOf(Int32) - 1 do
    begin
      LineDarker^[PixelIndex] := LineDarker^[PixelIndex] shr 1 and $7F7F7F7F;
      LineBrighter^[PixelIndex] := LineBrighter^[PixelIndex] or $07070707;
    end;

    LineIndex += 3;
  end;

  ABuffer.EndUpdate();
end;

Całość testowałem na bitmapie o rozmiarze 672x384 pikseli, więc nie trzeba było dopełnienia (czyli kolejnej pętli) implementować.

furious programming
2019-01-25 21:11

Powolutku zbliżam się do zakończenia prac nad projektem Deep Platformer. Jedną z ostatnich rzeczy zaimplementowanych w kodzie źródłowym był ten dotyczący sceny tzw. staffu, czyli ekranu zawierającego informacje na temat ekipy twórców. Z reguły scena ta jest swoistym demem technologicznym (pisząc głównie w kontekście gier na konsolę NES), która oprócz przekazania pewnych informacji, pozwala skorzystać z różnych wizualnych efektów, nad którymi miło zawiesić oko.

Projekt tej gry jest w dużej części takim demkiem technologicznym, dlatego też sam postanowiłem odpicować tę scenę, wykorzystując już zaimplementowane funkcjonalności i zamiast prostych, pojawiających się po kolei tekstów (jak w przypadku cutscenek), zrobiłem konkretną, kolorową animację. Mowa o animacji, której tłem jest nieskończenie głęboki poziom, którego kolor jest zmieniany płynnie, a tekst z informacjami powoli wyjeżdża z dołu do góry ekranu.

W niniejszym wpisie przybliżę temat dwóch elementów tej sceny (płynnej zmiany koloru tła poziomu oraz ciągłej zmiany warstwy), w ramach kolejnego dotyczącego zabaw z grafiką. Poprzednie wpisy na temat grafiki cieszyły się zainteresowaniem, więc ten również powinien się podobać. No i zawsze to kolejne kawałki kodu do wykorzystania w innych celach. ;)


1. Animacja zmiany koloru tła

Mamy dany zestaw kolorów: niebieski, turkusowy, zielony, brunatny, pomarańczowy oraz czerwony. Wymaganie jest takie, aby kolor poziomu zmieniany był płynnie – z niebieskiego na turkusowy, z turkusowego na zielony itd. Zmiana musi trwać długo, tak aby użytkownik nie dostał oczopląsu. Dodatkowo, sama płynna zmiana koloru nie może trwać cały czas – główny kolor (jeden ze zbioru, np. niebieski) musi być użyty przez pewien czas, następnie przez pewien czas ma trwać płynna zmiana z bieżącego na kolejny (np. z niebieskiego na turkusowy), następnie ten kolejny znów musi być używany przez chwilę itd.

Rozbijmy więc ten problem na dwa osobne, aby łatwiej było zrozumieć o co chodzi.


1.1. Obliczenie koloru pośredniego

Aby móc płynnie sterować zmianą koloru tła, potrzebujemy funkcji, która potrafić będzie obliczyć kolor pośredni, na podstawie dwóch kolorów bazowych (dla uproszczenia A i B) i stopnia zmieszania. Im wyższy procent zmieszania, tym wynikowy odcień oddali się od koloru A i jednocześnie zbliży się do koloru B. Jak obliczyć taki kolor? Bardzo łatwo.

Każda składowa koloru to liczba z przedziału od 0 do 255. Mając składowe obu kolorów, należy wziąć parę z danego kanału (np. składową R z koloru A i z koloru B), wyliczyć różnicę ich wartości, podzielić przez 100 (czyli przez sto procent) i pomnożyć przez zadany procentaż. W ten sposób uzyskujemy wartość, którą należy dodać do składowej pierwszego koloru. Im wyższy wejściowy procentaż, tym wartość wynikowej składowej zbliży się do tej z koloru B. Wzór poniżej:

{
  AR  – składowa R koloru A (źródłowego)
  BR  – składowa R koloru B (docelowego)
  DR  – składowa R wyjściowego koloru

  100 – maksimum skali (tu: procentażu)
  P   – stopień zmieszania kolorów (tu: procent)
}

DR := AR + (BR - AR) / 100 * P;

Taką operację należy wykonać trzy razy, dla każdej składowej. Powyższy wzór obejmuje kilka kluczowych przypadków. Nie tylko prawidłowo oblicza odcień wyjściowy, ale też nie jest wrażliwy na kolejność podania kolorów i ich wartości (składowa koloru A może mieć wartość większą, mniejszą lub taką samą jak składowa koloru B), a także zwraca kolor bazowy dla procentażu 0 (kolor A) i kolor docelowy dla procentażu 100 (kolor B).

Oczywiście nic nie stoi na przeszkodzie, aby wykonać to samo dla kanału alpha (aby zmieszać też przezroczystość), ani też by użyć innej skali niż procentażu (można użyć innego zakresu niż 0 .. 100).

Implementacja funkcji jest bardzo prosta:

uses
  Graphics;

type
  TRGBTriple = packed record
    B, G, R: UInt8;
  end;

  function CombineColors(AColorA, AColorB: TColor; APercent: UInt8): TColor;
  var
    ColorA, ColorB, ColorDest: TRGBTriple;
  begin
    RedGreenBlue(AColorA, ColorA.R, ColorA.G, ColorA.B);
    RedGreenBlue(AColorB, ColorB.R, ColorB.G, ColorB.B);

    ColorDest.R := Round(ColorA.R + (ColorB.R - ColorA.R) / 100 * APercent);
    ColorDest.G := Round(ColorA.G + (ColorB.G - ColorA.G) / 100 * APercent);
    ColorDest.B := Round(ColorA.B + (ColorB.B - ColorA.B) / 100 * APercent);

    Result := RGBToColor(ColorDest.R, ColorDest.G, ColorDest.B);
  end;

Do przetestowania tej funkcji napisałem sobie proste narzędzie okienkowe.


1.2. Kontener przechowujący kolory

Drugą rzeczą potrzebną do animacji jest kontener przechowujący listę wszystkich bazowych kolorów. Oprócz zdolności ich przechowywania, musi także pamiętać indeksy bieżącej pary kolorów, której używamy do obliczeń. Musi potrafić też zapętlić indeksowanie, tak aby po dotarciu na koniec listy kolorów, za kolejną parę uznał kolor ostatni i pierwszy, czyli musi działać jak bufor cykliczny.

Do tego celu wystarczy zwykła klasa z polem przechowującym generyczną listę kolorów, kolejnymi polami przechowującymi indeksy dwóch kolorów, właściwością pozwalającą odczytać bieżący odcień oraz czymś pozwalającym przechować bieżący procentaż (w przypadku mojego platformera jest to po prostu indeks klatki, inkrementowany w każdej klatce sceny). Aby kolor bazowy mógł być przez pewien czas używany stale (bez mieszania), przyda się też dodatkowe pole logiczne ze stanem mieszania (trwa mieszanie lub oczekiwanie).

Nagłówek takiej prostej klasy poniżej, a tutaj wrzucam cały jej kod, łącznie z definicjami metod. Co prawda nie zawiera deklaracji wszystkich użytych elementów (np. stałych), ale za niedługo i tak kod całego projektu będzie publiczny, więc każdy będzie mógł sobie go przeanalizować.

type
  TStaffColors = class(TObject)
  private type
    TColors = specialize TFPGList<TColor>;
  private
    FColors: TColors;
  private
    FColorAIndex: Integer;
    FColorBIndex: Integer;
  private
    FTransitionActive: Boolean;
    FTransitionFrameIndex: Integer;
  private
    FCurrent: TColor;
  private
    procedure InitColors();
    procedure DoneColors();
  public
    constructor Create();
    destructor Destroy(); override;
  public
    procedure NextFrameTransition();
    procedure Clean();
  public
    property Current: TColor read FCurrent;
  end;

2. Animacja poziomu

Drugim ficzerem jest animacja ciągłej zmiany warstwy wgłąb, w taki sposób, aby poziom sprawiał wrażenie nieskończenie głębokiego. Oczywiście sam poziom ma skończoną liczbę warstw (dokładnie dwanaście), jednak trik polega na tym, aby w odpowiednim momencie zmienić indeks bieżącej warstwy.

Tym momentem jest chwila, w której animacja zmiany warstwy zostaje zakończona (bieżąca warstwa – ta czarna – renderowana jest bez skalowania) oraz gdy jej indeks wynosi n-1, gdzie n to liczba warstw widocznych na ekranie (n równe jest 3). Aby taka zmiana indeksu nie była zauważalna, trzy najgłębsze warstwy (o indeksach 0 .. 2) muszą być takie same jak trzy najbliższe (o indeksach 9 .. 11).

Cała animacja ciągłego przełączania warstw i jej zapętlenie ogranicza się do kilku linijek kodu:

procedure TStaff.NextFrameSwitch();
begin
  if not FLevel.SwitchingContinues then
  begin
    if FLevel.CurrentLayerIndex = LAYERS_VISIBLE_COUNT - 1 then
      FLevel.CurrentLayerIndex := FLevel.Layers.Count - 1;

    FLevel.GoDeeper();
  end;

  FLevel.NextFrameSwitch();
end;

Logika animowania poziomu w klasie sceny to dokładnie dwie linijki:

procedure TStaffScene.UpdateLevel();
begin
  Staff.NextFrameSwitch();
  Staff.NextFrameTransition();
end;

Staff to obiekt przechowujący informacje na temat sceny, metoda NextFrameSwitch wykonuje kolejną klatkę animacji przełączania warstwy poziomu, a metoda NextFrameTransition wykonuje kolejną klatkę zmiany koloru. Kody obu znajdziecie wyżej i we wklejce na Pastebin.


3. Gotowy efekt na przykładzie platformera

Niżej przedstawiam scenę staffu mojego platformera, z której usunąłem przesuwający się tekst, tak aby tło było widoczne. Na potrzeby podglądu skróciłem czas trwania przemiany koloru – w projekcie trwa ona dłużej i mniej rzuca się w oczy. Scena ta co prawda posiada więcej ruchomych i animowanych elementów, jednak nie chcę wszystkiego teraz zdradzać – niech co nieco pozostanie niespodzianką. ;)

Deep Platformer – staff scene background.webm


Podsumowanie

Wykonanie opisanej wyżej animacji poziomu nie było zbyt trudne. Nie trzeba ani dużo kodu, ani też skomplikowanej logiki czy specjalistycznej wiedzy – trochę obliczeń i tyle. Ja wiem, że animacje bazujące na liczeniu klatek to może być dla wielu kosmos, jednak to samo można wykonać na podstawie delty, jak w normalnych grach.

Oczywiście tego typu animacje mogą być wykorzystane gdziekolwiek, również w komponentach dla aplikacji okienkowych. Można np. stworzyć sobie panel, którego tło płynnie zmieni swój kolor z zielonego na czerwony (stan normalny na stan zagrożenia) i w drugą stronę. Całość można oprzeć na zwykłym timerze, którego zdarzenie OnTimer pozwoli liczyć wywołania (klatki) i odpowiednio przygotowywać odcień dla tła. Można też kolorować w ten sposób inne elementy interfejsu, np. etykiety, przyciski czy wręcz całe obramowanie okna (o ile korzystamy z customowego, który da się łatwo kolorować). Zastosowań jest nieskończenie wiele – ograniczeniem jest jedynie wyobraźnia.

#free-pascal #lazarus #platformer #retro

czysteskarpety

wiadomo, kilka osób trzeba spierdzielić w postach, nie ma ucieczki od tego :)

furious programming

Dokładnie – coś jak w tym rysunku:

duty_calls

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. ;)