furious programming
2018-06-11 17:12

Dawno nie pisałem o demówce, a że dziś kolejny duży skok wydajnościowy, to trzeba się pochwalić. ;)

Ale może o czymś innym na początek. Intro mam już gotowe od jakiegoś czasu, więc – będąc na fali animacji – postanowiłem stworzyć ekran creditsów. Szło dobrze, jednak zamarzył mi się ekran z tzw. staff podobny jak na Famocomie. Tam też używano dwóch tekstów dla każdego członka zespołu – stanowisko w jakimś kolorze i imię z nazwiskiem poniżej, w innym kolorze. I tu problem – animacje zrobiłem w ten sposób, aby jeden ekran mógł zawierać tylko jeden element – tekst (jeden font, jeden kolor), obrazek, pusty ekran itd. Tak więc musiałem co nieco przerobić.

System animacji rozbudowałem do tego stopnia, że teraz każda klatka może zawierać dowolną liczbę elementów – różnych napisów w różnych krojach i obrazków jednocześnie (zmiana jasności klatki nadal jest obsługiwana). Dzięki temu ekran creditsów teraz zawiera sześć napisów, z użyciem dwóch fontów – mały i szary dla nagłówków (NESFont) oraz większy i biały dla właściwego tekstu (Small Pixel7). Oczywiście aby to było możliwe, musiałem stworzyć nowy font, bo miałem tylko jeden (ten, który wcześniej używałem do intra).

Szkoda tylko, że aby stworzyć pojedynczą animację, trzeba teraz napisać więcej niż jedną linijkę. Trzeba stworzyć obiekt, w konstruktorze nadać mu podstawowe parametry (czas trwania, rodzaj zmiany oświetlenia itd.), a następnie metodami AddLabel oraz AddImage po kolei dodawać elementy.

Podzieliłem więc moduł z animacjami na cztery osobne – dla intra, menu, outra i creditsów. Jest w porządku.


Szkielet głównej pętli mam już napisany, demówka ładnie rozpoczyna działanie od intra, następnie przechodzi do rozgrywki, dalej do outra i do creditsów. Po nich wracamy do punktu wyjścia – znów leci intro. Skupiłem się więc na obsłudze samej rozgrywki. Na pierwszy ogień trzeba przetestować wielowarstwowe renderowanie poziomu, efekt paralaksy oraz obsługę kamery. W uproszczeniu, aby namalować warstwy poziomu, należy:

  • przemalować w całości bufor ramki na wymagany kolor,
  • w pętli dla trzech warstw, od najdalszej:
    • skopiować widoczny fragment warstwy do bufora pomocniczego (biorąc pod uwagę skalowanie – im dalsza warstwa, tym większy fragment i większe zeskalowanie w dół),
    • przemalować widoczne piksele na wymagany kolor (im bliższa warstwa, tym ciemniejszy kolor, dla warstwy najwyżej jest to kolor czarny), czyli trzeba pomalować wszystkie piksele o kolorze innym niż fuksja,
    • przenieść pomalowany fragment warstwy z bufora pomocniczego do bufora ramki (wszystkie piksele oprócz fuksji),
  • namalować obiekty i liczniki.

Skopiowanie fragmentu warstwy z obsługą skalowania i bez rozmywania (to ważne) realizuję za pomocą Canvas.CopyRect, natomiast przeniesienie bitmapy z jednego bufora do drugiego, to nic innego jak namalowanie jednej bitmapy na drugiej – tu wykorzystałem Canvas.Draw, bo respektuje kolor przezroczystości, czyli fuksję.

Wszystko by było dobrze, gdybym nie spojrzał co konkretnie dzieje się w momencie malowania jednej bitmapy na drugiej, pomijając piksele uznawane za przezroczyste. Po stworzeniu obiektu bitmapy w pamięci i jej załadowaniu ze strumienia, wołam Bitmap.Mask(clFuchsia) – dzięki temu ten kolor będzie pomijany, a przezroczystość mam zapewnioną. Otóż problem w tym, że wywołanie tej metody odpala różne wewnętrzne operacje, m.in. stworzenie wewnętrznego obrazu maski, z którego korzysta widgetset, a w konsekwencji jedna z systemowych funkcji. Drugi, wewnętrzny obraz? Nie no, to będzie powolne… Skoro wiem które piksele mają być pomijane, obrazy mam gotowe do obróbki, to nie potrzebuję jakichś internalsowych cudów na kiju.

Wpadłem więc na pomysł, aby nie używać tej wygodnej, wbudowanej przezroczystości, wzamian samemu kopiując fragmenty pamięci z jednej bitmapy do drugiej. W końcu i tak śmigam wskaźnikami po pamięci obrazów (za pomocą ScanLine), np. w procedurze przyciemniającej finalny bufor ramki. Takie ręczne przepisywanie danych z jednej bitmapy do drugiej nie wymaga dodatkowych obrazów i innych czasochłonnych czynności, więc powinno być o wiele szybsze.

Przeprowadziłem więc test – zmierzyłem jak szybko renderowany jest jednokolory obraz. Następnie zmierzyłem czas renderowania ekranu wypełnionego w całości tekstem – tym tekstem były cztery pełne zestawy obsługiwanych znaków (litery małe i duże, cyfry i znaki interpunkcyjne), dwa zestawy fontem małym i dwa dużym. Wszystko z wykorzystaniem standardowego mechanizmu przezroczystości. Wyszło nieźle. Następnie napisałem brzydala, w którym ręcznie kopuję piksele z jednego obrazu do drugiego, z pominięciem pikseli w kolorze fuksji (jest to odpowiednik metody Canvas.Draw):

procedure FastDrawImage(ABuffer: TBitmap; APosition: TPoint; AImage: TBitmap; ALock: Boolean);
var
  LBufferLine, LImageLine: PRGBTripleArr;
  LBufferPixel, LImagePixel: TPoint;
begin
  if ALock then ABuffer.BeginUpdate();
 
  LBufferPixel.Y := APosition.Y;
  LImagePixel.Y := 0;
 
  while LImagePixel.Y < AImage.Height do
  begin
    LBufferPixel.X := APosition.X;
    LImagePixel.X := 0;
 
    LBufferLine := ABuffer.ScanLine[LBufferPixel.Y];
    LImageLine := AImage.ScanLine[LImagePixel.Y];
 
    while LImagePixel.X < AImage.Width do
    begin
      with LImageLine^[LImagePixel.X] do
        if not ((B = 255) and (G = 0) and (R = 255)) then
        begin
          LBufferLine^[LBufferPixel.X].B := B;
          LBufferLine^[LBufferPixel.X].G := G;
          LBufferLine^[LBufferPixel.X].R := R;
        end;
 
      LBufferPixel.X += 1;
      LImagePixel.X += 1;
    end;
 
    LBufferPixel.Y += 1;
    LImagePixel.Y += 1;
  end;
 
  if ALock then ABuffer.EndUpdate();
end;

Paskuda, ale działa prawidłowo (jeszcze upiększę ten kod, bo co nieco hardkodowałem). Trzeba tylko pamiętać, że nie jest zabezpieczona pod kątem malowania poza bitmapą – wtedy kod będzie modyfikował śmieci spoza bitmapy, co może się źle skończyć. Jednak dodatkowych zabezpieczeń nie będę implementował, bo skoro wiem jak ta procedura działa, to wystarczy używać jej z głową.

Przystosowalem kod wszystkich metod rendererów, tak aby używały nowej procedurki, a także wyłączyłem standardową przezroczystość we wszystkich ładowanych do pamięci bitmapach (zaremowałem linijki z Bitmap.Mask(clFuchsia). Zmierzyłem czas – bingo! ;)

Czas renderowania jednokolorowego ekranu z licznikami obciążenia i fps wynosił w obu przypadkach średnio 26% czasu przydzielonego na obsugę jednej klatki – podstawa do obliczeń. Czas renderowania ekranu wypełnionego tekstem, z wykorzystaniem wbudowanego mechanizmu przezroczystości to średnio 64%. Natomiast skorzystanie z procedury FastDrawImage, skróciło czas generowania obrazu klatki do 40%. Samo renderowanie charsetów wcześniej zajmowało 38% przydzielonego czasu, a teraz zajmuje raptem 14% – przyspieszenie rzędu 2.7x.

Dzięki temu, że ręcznie kopiuję dane z jednego obszaru do drugiego, a tym samym używam BeginUpdate i EndUpdate, mogę tę pierwszą metodę wywołać raz, na początku renderowania finalnej klatki obrazu, a tę drugą na koniec. Wszystko co trzeba namalować na buforze klatki, zamierzam wykonać w ten sam sposób – używać metod ręcznie kopiujących pamięć, zamiast metod z klasy TCanvas. To pozwoli kilkakrotnie szybciej namalować wszystkie obrazki. Natomiast z metod płótna skorzystać tylko do malowania obiektów, takich jak postać gracza i świetliki, czyli wszystkich składających się z prostych figur, przede wszystkim z prostokątów (chyba innych nie będę używał).


Zastanawiacie się pewnie nad tym, dlaczego tak bardzo zależy mi na szybkim malowaniu bitmap na buforze ramki. Otóż im szybsze malowanie obrazu, tym m.in. krótszy czas malowania warstw. Im krótszy czas malowania warstw, tym więcej ich będę mógł użyć do malowania poziomu. A im więcej warstw, tym lepszy efekt głębi. Dlatego procedurka FastDrawImage jest dla mnie i dla samej gry bardzo ważna.


Dziś jedyne co nowego mogę pokazać, to ekrany testu renderowania tekstu – w załącznikach. Nic specjalnego, choć przynajmniej widać z jakich znaków gra korzysta i jak te znaki wyglądają. Jednak skoro wiem, że mam super-szybki sposób malowania bitmap, teraz mogę ze spokojem zabrać się za właściwy kod rozgrywki. Mam nadzieję, że nie napotkam większych problemów i w kolejnym wpisie z tego cyklu pokażę już tryb debugowania poziomu.

Do następnego. ;)

#free-pascal #lazarus #platformer #retro

furious programming

jak wypuszczę crapa, to zaklepię sobie miejsce na galerze – przyda się więc wypucowane wiosło. :D

cerrato

obejrzałem odcinek 99, przeczytałem Wasze komentarze i już wiem o co chodzi :D Ale i tak podtrzymuję moje zdanie - gry to nie moja bajka.

furious programming
2018-06-06 23:43

Kolejna kupka niusów związanych z platformersem. Niestety bez obrazków, ale za to z nowym intro – click! ;)

Dziś spiąłem poślady i w końcu przerobiłem główną pętlę silnika, tak aby mogła już obsługiwać wszystkie części gry, czyli intro, menu, rozgrywkę oraz outro. Na początku uznałem, że silnik powinen wykorzystywać tylko jedną pętlę, w której to miała by być przetwarzana konkretna część gry. Jednak to zmuszało do selekcji części gry, np. w postaci intrukcji wyboru, do przechowywania stanu gry w zmiennej i jej aktualizowaniu po zmianie stanu. Trochę kłopotliwe i mało wydajne.

Zrezygnowałem z jednej pętli na rzecz kilku – główna, zapętlająca całą grę (intro, menu, rozgrywkę i outro):

procedure TEngine.Start();
begin
  InitWindow();
 
  repeat
    if not Terminated then PlayIntro();
    if not Terminated then PlayMenu();
    if not Terminated then PlayGame();
    if not Terminated then PlayOutro();
  until Terminated;
 
  CloseWindow();
end;

Trochę dziwnie te warunki wyglądają, ale lepiej żeby były tutaj, niż wewnątrz metod Play*.
a każda część gry została wydzielona do osobnej metody (jw.). W każdej z nich jest osobna pętla, np.:

procedure TEngine.PlayIntro();
begin
  Intro.Reset();
 
  repeat
    UpdateClock();
    UpdateInput();
 
    UpdateIntroOptions();
    UpdateIntroState();
 
    UpdateEmulation();
 
    UpdateIntroAnimation();
    UpdateIntroFrame();
 
    Intro.NextFrame();
 
    UpdateWindowPlacement();
    UpdateWindowInterface();
 
    UpdateClock();
  until Intro.Completed or Terminated;
end;

Metody działające zawsze tak samo, bez względu na to która część gry jest aktualnie przetwarzana, mają prefiks Update* (np. UpdateClock), natomiast te przeznaczone dla konkretnej części mają dłuższy prefiks – UpdateIntro* (np. UpdateIntroFrame). To pozwala zdefiniować różne zachowania dla każdej części z osobna, podtrzymując jednolitość i czytelność kodu.

Teraz gra działa prawidłowo – rozpoczyna się intrem, następnie przechodzi do menu, rozgrywki, outra i na początek, czyli do intro. Póki co tylko intro jest oprogramowane, więc kręci się w kółko – przynajmniej dobrze się testuje.

Jeśli o same animacje chodzi, to dodałem do nich nowy parametr – Skippable. Pozwala on klawiszem pauzy lub skoku na przejście do kolejnej animacji (kolejnej w zestawie animacji, nie na zakończenie całego ”filmu”). To które animacje można pominąć, deklaruje się podczas ich tworzenia. Jeśli o intro chodzi, to zakończyć można animację wyświetlającą logo oraz napis presents – tylko środkowe animacje, czyli te bez zmiany jasności. Ogólnie chodzi w tym o to, aby działało to tak samo jak w pełnoprawnych grach – i tak teraz działa.


Kolejnym ficzerem jest nowy tryb wyświetlania okna. O ile wcześniej istniała możliwość powiększenia okna na pełen ekran, to obraz gry był dopasowany do ekranu. Fatalnie wyglądało to na ekranie panoramicznym – piksele były mocno prostokątne. Tak więc dodałem kolejny tryb wyświetlania, który pozwala na rozciągnięcie okna na pełen ekran, i który wyświetla obraz gry bez zmiany proporcji. Bufor ramki ma proporcje 4:3, więc gra po bokach maluje czarne pasy.

Teraz dostępnych jest pięć trybów – native, zoom2x, zoom3x, fullscreen (bez zmiany proporcji) i fillscreen (dopasowany do ekranu).


Rozszerzyłem też wachlarz parametrów wejściowych. Z linii poleceń można teraz dodatkowo wybrać numer monitora, na którym pojawi się okno gry (monitor 0 dla głównego, monitor 1 dla drugiego itd.), a także długość intra (intro long dla pełnego i intro tiny, które pomija zabawę z telewizorem).

Tak więc teraz z poziomu konsoli można określić rozmiar okna, numer monitora, górny limit fps, długość trwania czarnego ekranu przed intrem oraz długość samego intra. Wszystkie parametry przydają się do testowania, ale pozostawię je i będą dostępne w wersji produkcyjnej.


Przystosowałem też tryb debugowania do bieżącej funkcjonalności gry. Przerobiłem kod tak, aby z poziomu kompilacji warunkowej móc włączyć stały tryb debugowania (”cheat mode”) lub całkowicie pominąć intro. Dzięki temu będę mógł od razu po włączeniu gry debugować samą rozgrywkę, a w przyszłości, móc od razu przenieść się do outra. Dobra rzecz, koniecznie musi być, aby oszczędzać czas.


Cóż, animacje działają prawidłowo i są wystarczająco funkcjonalne, intro odpicowane jak należy. Czas więc przejść do kolejnej, najważniejszej części, czyli do właściwej rozgrywki. Klasę reprezentującą pojedynczy poziom już mam, więc teraz trzeba skupić się na kodzie renderującym poziomy oraz na obsłudze kamery (chyba się to view zone zwie), a następnie za głównego bohatera, sterowanie nim oraz oczywiście za fizykę.

Będzie fun – byle nie zagmatwać kodu… ;)

#free-pascal #lazarus #platformer #retro

GThoro

@furious programming: może zainteresuj się https://asphyre.net/products/pxl bardzo fajny framework OpenGL/DX do Paszczaka :)

furious programming

@GThoro: jeśli będę chciał stworzyć konkretną grę, to na pewno użyję normalnego API, zamiast tak kombinować. Ten programik natomiast postanowiłem zrobić w trybie okienkowym i bez dodatkowych bibliotek, jako takie małe wyzwanie, bardziej w postaci dema technologicznego niż faktycznej gry.

Trochę wodotrysków, trochę gameplay'u, trochę humoru, easter eggów oraz nawiązań do starych konsol. ;)

furious programming
2018-06-05 00:51

Od ostatniego wpisu na temat platformówki minęły prawie trzy dni. Od tamtej pory sporo się zmieniło.

Najważniejszymi elementami, jakie zdążyłem doprowadzić do używalności, są animacje. Owe animacje służą do budowy intra, outra oraz przerywników pomiędzy poziomami (tak jak Super Mario Bros.) – są uniwersalne. Klasa TAnimation przechowuje dane na temat pojedynczej animacji, którą może być np.:

  • szum telewizyjny z tekstem, bez zmiany jasności klatek,
  • szum telewizyjny z tekstem, z rozjaśnianiem lub przyciemnianiem obrazu,
  • jednolite tło z tekstem lub bez, z przyciemnianiem lub bez itd.

Dane jakie są w niej przechowywane to m.in. styl animacji, długość jej trwania (w liczbie klatek, bo główną pętlę zaimplementowałem tak jak miało to miejsce na Famicomie), kolor tła i/lub obraz, tekst do wyświetlenia. Do tego jest klasa jako lista takich animacji – TAnimations – która grupuje zestaw w jeden długi film.

Jeśli chcę np. wyświetlić ekran z logiem, który ma stopniowo pojawić się na ekranie (od całkowicie przyciemnionego do tego o pełnej jasności), chwilę być wyświetlany w maksymalnej jasności, a następnie stopniowo zaniknąć, dodaję do listy trzy animacje. W ten sposób można dowolnie łączyć ze sobą różne ekrany. Stworzenie długiej animacji – np. całego intra – ogranicza się do deklaracji klasy dziedziczącej po TAnimations, wywołania kilku wewnętrznych metod w odpowiedniej kolejności, podając parametry animacji w argumentach.

Za wygenerowanie pełnego intra odpowiada poniższa metoda:

procedure TIntro.FillWithAnimations();
begin
  AddBlank(CommandLine.InitialIntroDelay, clBlack);
 
  AddNoise(180, clBlack, FNoiseLabel, dsDecreasing);
  AddNoise(90,  clBlack, FNoiseLabel);
 
  AddBlank(8,   clWhite);
  AddBlank(60,  clBlack);
 
  AddImage(24,  clBlack, FDevLogo, FrameBufferRect, dsDecreasing);
  AddImage(180, clBlack, FDevLogo, FrameBufferRect, dsOriginal);
  AddImage(24,  clBlack, FDevLogo, FrameBufferRect, dsIncreasing);
 
  AddBlank(15, clBlack);
 
  AddLabel(24,  clBlack, FPresentsLabel, dsDecreasing);
  AddLabel(120, clBlack, FPresentsLabel, dsOriginal);
  AddLabel(24,  clBlack, FPresentsLabel, dsIncreasing);
 
  AddBlank(15, clBlack);
end;

Jak widać kodu niewiele, a animacja trwająca ponad 12s gotowa. Podobnie będzie wyglądalo tworzenie outra – kilka linijek i gotowe.

Pokrótce, AddBlank dodaje pusty ekran, tworząc wizualną przerwę; AddNoise dodaje animację szumu telewizyjnego; AddImage dodaje ekran z obrazkiem; a AddLabel dodaje jednokolorowy ekran z wycentrowanym tekstem (jednoliniowym lub wieloliniowym). Hardkodowane liczby określają liczbę klatek trwania animacji (nie wydzielałem ich do stałych, bo w ten sposób łatwiej i szybciej jest kalibrować animację), a enumy z prefiksem ds określają sposób przyciemniania obrazu (dsOriginal nie wpływa na jasność, dsDecreasing oznacza rozjaśnienie ekranu, a dsIncreasing przyciemnienie). O obiekcie CommandLine napiszę w dalszej części.

W załącznikach podaję wycięte klatki z animacji (zoom 2x), a film prezentujący działanie znajduje się tutaj – platformer intro.webm1.

Póki co zawiera wejście à la włączenie CeeRTeka na kanale Video, następnie mignięcie ekranu na biało imituje włączenie pegazusa, a dalej już logo producenta itd. Póki co nie mam ekranu z logiem gry, więc na presents się intro kończy.

[1] Logo wymyślone na szybko – znalazłem pierwszą lepszą grafikę pixel-artową w wyszukiwarce. Ale podoba mi się.


Kolejną rzeczą doprowadzoną do perfekcyjnego działania jest tryb debugowania, dostępny m.in. za pomocą kompilacji waunkowej. Po skorzystaniu z niego, możliwe jest przede wszystkim manipulowanie czasem – można zmniejszyć liczbę klatek na sekundę lub zwiększyć (przedział 1-60). Dodatkowo, na ekranie wyświetlane są: indeks bieżącej klatki, zużycie czasu potrzebnego na obsługę aktualnej klatki (w procentach), a także licznik FPS. Ten ostatni możliwy będzie do wyświetlenia i bez trybu debugowania, za pomocą klawisza Ins.

Do debugowania przydatna jest też możliwość zmiany rozmiaru okna – teraz za pomocą klawiszy + i - odpowiednio zwiększa się rozmiar okna lub zmniejsza. Gra póki co obsługuje poniższe tryby:

  • native – okno o rozmiarze 320x240 pikseli,
  • zoom 2x – okno o wymiarach 640x480 pikseli,
  • zoom 3x – o rozmiarach 960x720 pikseli,
  • full screen – rozciągnięte na pełen ekran (przykrywa pasek zadań, a także ukrywa kursor).

We wszystkich rozmiarach oprócz pełnoekranowego, za pomocą myszy możliwe jest przesuwanie oknem (okno nigdy nie posiada ramki, a więc jest to konieczność). Powiększanie i pomniejszanie, oprócz zmiany rozmiaru okna, również je centruje (na dowolnym ekranie, także na pobocznych). Zastanawiam się jeszcze nad dodaniem opcji pełnoekranowej, bez zmiany proprcji obrazu (z czarnymi pasami po bokach).

Przewidziana jest możliwość uruchamiania gry z linii poleceń, w celu określenia początkowych ustawień – przydatna zarówno dla graczy, jak i w procesie testowania. W linii poleceń określa się parametry parami – para to typ opcji oraz jej wartość. Kolejność par nie ma znaczenia. Aktualnie dostępne opcje:

  • size – rozmiar okna, obsługuje wartości native, zoom2x, zoom3x oraz fullscreen,
  • fps – początkowa liczba FPS, obsługuje wartości z zakresu od 1 do 60,
  • delay – długość pierwszej animacji intra, czyli czarnego ekranu (przydała mi się do nagrania filmu – ActivePresenter chwilę startuje, więc później mogłem odciąć nadmiarowy początek).

Nie wiem jeszcze które opcje pozostaną dla graczy (size na pewno), ani ile ich jeszcze dojdzie. Na pewno muszę dodać wybór ekranu (główny lub poboczny), bo obecnie okno zasłania mi edytor i muszę cudować. Przykładowe wywołanie z konsoli:

> platformer.exe size zoom2x fps 30 delay 180

Co oznacza, że gra uruchomi się w rozdziałce 640x480, z limitem 30fps oraz z początkowym zamrożeniem na 3s.


Bardzo istotną informacją jest bieżąca wydajność – intro w rozmiarze natywnym zjada od kilku do 20% czasu wyznaczonego dla klatki, a na pełnym ekranie nie więcej niż 60%. Tak więc na moim złomie mam spory zapas. Przetestowałem też działanie metod renderujących pod kątem maksymalnego obciążenia (takiego jakie będzie podczas rozgrywki) – skopiowanie fragmentów kilku warstw, wypełnienie ich kolorem, namalowanie ich na buforze ramki i domalowanie do tego jeszcze kilku obiektów i napisów bez problemu potrwa mniej niż ~16ms, a więc gra będzie zawsze działać w stałych 60fps.

Jeśli chodzi o sprawy stricte plikowe, to projekt gry składa się w tym momencie z 18 modułów, łącznie 3337 linijek kodu. W skład wersji release wchodzi póki co sześć plików – plik wykonywalny (waży raptem 1,76MB), dwa pliki tekstowe (licencja i readme), jeden font, jeden poziom i jedno logo. Wszystkie te pliki ważą łącznie 1,90MB – finalna wersja nie będzie zajmować więcej niż 10MB.


Na dziś to tyle – do następnego. ;)

#free-pascal #lazarus #platformer #retro

furious programming

Zobacz np. na świeży film o pewnym popularnym kartridżu – Contra 168-in-1: Przegląd gier z kultowego kartridża Pegasusa. O ile telewizora nie widać podczas podłączania kasety i włączania konsoli, to na początku ekran jest czarny (kanał video), w momencie przesunięcia suwaka power obraz błyska na biało, a następnie wchodzi intro gry. Tak chcę, aby wyglądało intro mojej demówki, tyle że również pokazując początkowy stan ekranu TV.

W sumie nic mnie to nie kosztuje, bo animacja szumu oraz jednokolorowego ekranu jest i będzie używana w innych miejscach gry. Tak więc różnica między prostym intrem a tym bajeranckim, to różnica góra 5 linijek kodu. ;)

furious programming

Dobra, intro poprawione. Teraz jest mniej więcej tak, jak opisałem dwa komentarze wyżej, tyle że dodałem skakanie po kanałach – najpierw kanał 02, następnie przełączenie na 01, następnie na VIDEO i włączenie konsoli. Wygląda cudnie. :]

furious programming
2018-06-02 03:15

Implementuję kolejne funkcjonalności związane z platformówką. Kod puchnie, nowych klas i obiektów przybywa, co mnie cieszy. Jednak wraz ze wzrostem liczby funkcji w grze, równolegle rośnie zapotrzebowanie na moc obliczeniową CPU, co już mnie mniej cieszy. Jednak problemy związane z wydajnością dają okazję do pogłówkowania i przypomnienia sobie czasów, kiedy kompilator nie krzątał sobie głowy optymalizacjami, tym bardziej agresywnymi.

Dzisiejszym ficzerem jest m.in. efekt stopniowego przyciemniania obrazu – procedurka otrzymująca na wejściu surową bitmapę i przyciemniająca ją o zadany stopień, wywoływana w pętli. W tym wpisie opiszę problemy związane z wydajnością oraz sposób optymalizacji kodu, który znacząco wpłynął na czas renderowania klatek.


Najpierw trochę prostej matematyki.

Gra z założenia ma działać w sześćdziesięciu klatkach na sekundę. To daje w zaokrągleniu w dół 16ms na obsługę każdej z nich. W tych 16ms gra musi zdążyć zaktualizować stan gry (input, logikę itd.), wyrenderować klatkę, przemalować okno i przetworzyć kolejkę komunikatów. Jeśli zdąży to wykonać w zadanym czasie, pozostałą resztę powinna zaczekać (à la Famicom i NMI).

Problemem była wydajność procedury przyciemniającej bitmapę. Początkowo wyglądała w ten sposób:

procedure DimFrame(ABitmap: TBitmap; ALevel: UInt8);
var
  LLine: PRGBTripleArr;
  LLineIndex, LPixelIndex: Integer;
begin
  ABitmap.BeginUpdate();
 
  for LLineIndex := 0 to ABitmap.Height - 1 do
  begin
    LLine := ABitmap.ScanLine[LLineIndex];
 
    for LPixelIndex := 0 to ABitmap.Width - 1 do
      with LLine^[LPixelIndex] do
      begin
        B := B.ToDarker(ALevel);
        G := G.ToDarker(ALevel);
        R := R.ToDarker(ALevel);
      end;
  end;
 
  ABitmap.EndUpdate();
end;

Dla każdego piksela bitmapy wejściowej, należy obliczyć nową wartość składowej i ją zaktualizować. Łącznie 76800 razy, bo tyle pikseli zawiera bufor klatki. Metoda ToDarker znajdowała się we własnym helperze (nie jest standardowa), jej kod nie jest na razie istotny. Działa elegancko – prawidłowo przyciemnia obraz.

I teraz tak – wyrenderowanie klatki bez zmiany jasności twa średnio 15% zadanego czasu (czyli 15% z 16ms). Natomiast wyrenderowanie tej samej klatki ze zmianą jasności zjada 50% zadanego czasu. Dla okna rozciągniętego na pełen ekran procent zjadanego czasu przydzielonego dla klatki to odpowiednio 52% i aż 88% – bardzo niedobrze… Trzeba przyjrzeć się tej procedurze, bo ewidentnie jest wąskim gardłem.

Pierwsze co zrobiłem to pozbyłem się wywołań metod z helperów i przekleiłem z nich kod bezpośrednio do tej procedury (niżej tylko zmodyfikowane linijki):

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

Widać inline'owanie metod helperów zostało przez kompilator zignorowane, bo jest trochę lepiej. Mimo wszystko w dalszym ciągu ta procedura pożera za dużo mocy, dlatego znów trzeba coś skrócić. Kto pomyślał: „a na cholerę te floaty i zaokrąglenia?!” Sam też tak pomyślałem! Dzielenie i ucinanie liczb zmiennoprzecinkowych nie jest konieczne, a trwa dużo dłużej niż dzielenie całkowite. No to zmieniamy:

B := B * (255 - ALevel) div 255;
G := G * (255 - ALevel) div 255;
R := R * (255 - ALevel) div 255;

Znów jest lepiej – kilka procent szybciej niż wcześniej. Kto wie co jeszcze można zastąpić mniej czasochłonną operacją? Podpowiem – dzielenie, albowiem można tu zastosować przesunięcie bitowe (czym optymalizator w trybie -O1 nie zajął się):

B := B * (255 - ALevel) shr 8;
G := G * (255 - ALevel) shr 8;
R := R * (255 - ALevel) shr 8;

Kolejne procenty zaoszczędzone. Skoro tak dobrze idzie, to może coś jeszcze da się poprawić? Owszem, da się usunąć odejmowanie. Trzykrotne obliczanie różnicy nie jest potrzebne – można to zrobić raz (przed pętlami), a do obliczeń użyć samej zmiennej:

ALevel := 255 - ALevel;
 
{..}
 
B := B * ALevel shr 8;
G := G * ALevel shr 8;
R := R * ALevel shr 8;

Dzięki temu znów mamy skok wydajności – kolejne milisekundy zaoszczędzone.

Niestety nie mam więcej pomysłów na optymalizację, więc poniższy kod procedury jest tym ostatecznym:

procedure DimFrame(ABitmap: TBitmap; ALevel: UInt8);
var
  LLine: PRGBTripleArr;
  LLineIndex, LPixelIndex: Integer;
begin
  ABitmap.BeginUpdate();
  ALevel := 255 - ALevel;
 
  for LLineIndex := 0 to ABitmap.Height - 1 do
  begin
    LLine := ABitmap.ScanLine[LLineIndex];
 
    for LPixelIndex := 0 to ABitmap.Width - 1 do
      with LLine^[LPixelIndex] do
      begin
        B := B * ALevel shr 8;
        G := G * ALevel shr 8;
        R := R * ALevel shr 8;
      end;
  end;
 
  ABitmap.EndUpdate();
end;

Podsumowując, początkowo procedura przyciemniająca obraz zabierała nawet 40% czasu przydzielonego na klatkę. Po jej poprawieniu, czas działania przyciemniania wynosi teraz średnio 11%, co daje prawie czterokrotnie wyższą efektywność, zachowując pierwotny efekt wizualny. ;)

#free-pascal #lazarus #platformer #retro

loza_szydercow

Czyli jak Paint to obstawiam że zapisał ci full color bez indeksacji - można i tak choć trochę szkoda pamięci na coś co nigdy nie będzie wykorzystane (i narzut przy obliczeniach).

furious programming

Nie nie – w Paint tylko maluję (bo szybciej i mi wygodniej), a kompresuję w GIMP. Natomiast samej kompresji dokonuję wyłącznie w celu zmniejszenia rozmiaru binarek na dysku – dla kodu nie ma to znaczenia.

furious programming
2018-05-31 00:12

Dziwną rzecz napotkałem, jeśli o moją platformówkę chodzi.

Zabrałem się za implementację funkcji umożliwiającej zmianę rozmiaru okna. Klawiszami + oraz - zwiększa się lub zmniejsza rozmiar okna. Zadziałało dobrze, ale zauważyłem problem z wydajnością. Otóż jeśli rozmiar okna był mniejszy niż rozmiar ekranu oraz okno posiadało standardowe obramowanie (styl bsSingle), klatkaż utrzymywał się na poziomie 60. Ale jeśli rozciągnąłem okno na pełen ekran i usunąłem obramowanie (styl bsNone), liczba klatek spadała do 35. Formularz miał i ma zawsze ustawione podwójne buforowanie.

Przeprowadziłem więc test różnych ustawień obramowania i rozmiaru okna – wyniki niżej.

+-------------------+--------------------------+-----------+
|    BorderStyle    |    rozmiar okna          |    FPS    |
+-------------------+--------------------------+-----------+
|    bsSingle       |    mniejsze niż ekran    |    60     |
|    bsNone         |    na pełen ekran        |    35     |
+-------------------+--------------------------+-----------+
|    bsSizeable     |    natywny               |    60     |
|                   |    mniejszy niż ekran    |    40     |
|    bsNone         |    na pełen ekran        |    35     |
+-------------------+--------------------------+-----------+
|    bsNone         |    dowolny               |    60     |
+-------------------+--------------------------+-----------+

Jeśli początkowy styl obramowania będzie inny niż bsNone, to na niepełnoekranowych oknach aplikacja osiąga wymagany klatkaż (oprócz bsSizeable), ale nigdy po usunięciu obramowania (podczas działania programu) i powiększeniu na pełen ekran. Jak widać ustawienie stylu obramowania znacząco wpływa na szybkość odmalowywania formularza, jednak nie wiem dlaczego.

Wolę aby okno mniejsze niż ekran miało ramkę, ale cóż – ważniejsza jest płynność działania. Dlatego też ostatecznie wyłączyłem obramowanie na stałe. Jednak aby móc je przesuwać, nadpisałem metodę WMLButtonDown w klasie formularza:

procedure TPlatformerForm.WMLButtonDown(var AMessage: TLMLButtonDown);
begin
  inherited WMLButtonDown(AMessage);
 
  if not FullScreen then
  begin
    ReleaseCapture();
    SendMessage(Self.Handle, LM_NCLBUTTONDOWN, HTCAPTION, 0);
  end;
end;

Myślę, że to dobry kompromis. Niestety, ale pisanie gier działających sprawnie w trybie okienkowym nie jest proste… ;)

#free-pascal #lazarus #platformer #retro

grzesiek51114

@furious programming: masz jakieś rysunki koncepcyjne, które pokazywałyby jak to będzie wyglądało finalnie? Jestem bardzo ciekaw, szczególnie, że wiele razy dałeś do zrozumienia, że lubisz retro. :-)

furious programming

Lubię retro, szczególnie Famicoma i Super Famicoma. ;)

Mam jeden rysunek koncepcyjny, ale zawiera wyłącznie przykładowe warstwy poziomów (jako surowe, kwadratowe kafelki) i główną postać. Nie ma tekstu, nie ma liczników i świetlików, no i nie ma też nieregularnych kształtów poziomów, ktore w oryginale będą.

prototype

furious programming
2018-05-30 01:11

Dziś był dość pracowity dzień, jeśli chodzi o moją platformówkę – czas na trochę niusów.


Jeśli chodzi o własne narzędzia potrzebne do tworzenia tej platformówki, to do tej pory machnąłem trzy – generator obrazów, który na podstawie informacji zawatych w konfigu generuje dwukolorową bitmapę z podstawowym kształtem platform (jedna bitmapa to jedna warstwa); generator poziomów, który na podstawie konfigów i obrazków wypluwa binarkę z jednym poziomem; a także generator fontów, który generuje binarkę z zestawem grafik dla obsługiwanych znaków.

Generatory to proste aplikacje konsolowe, otrzymujące w parametrze ścieżkę katalogu zawierającego wymagane pliki konfiguracyjne (skorzystałem z plików INI z rozszerzeniem .ini i .inf), obrazki itd. Po odpaleniu generowany jest odpowiedni plik (bitmapa lub binarka). Abym nie musiał klepać komend w terminalu, utworzyłem sobie skróty do aplikacji, które wystarczy dwukliknąć i gotowe – plik można skopiować do właściwego katalogu. Póki co nie implementowałem logów, więc te generatory nic w konsoli nie wyświetlają (nawet błędów), ale to dodam kiedy indziej.


Natomiast główny projekt gry pochłonął dziś sporo czasu i choć pisałem kod na pałę (kto by tam cokolwiek projektował…), to jestem bardzo zadowolony z jego wyglądu, modularyzacji i oczywiście działania. W chwili obecnej główny projekt składa się z 14 modułów (w tym jeden z formularzem) i zawiera nieco ponad 2kLoC, bez komentarzy (w ogóle ich nie ma, nie żebym pominął przy liczeniu). Pierwszy raz miałem okazję skorzystać z przestrzeni nazw – wybrałem dla niej nazwę Platformer – i muszę przyznać, że bardzo fajna funkcjonalność (choć zabugowana w środowisku…).

Do tej pory zaimplementowałem kilkanaście klas. Najciekawsze znajdują się w poniższych modułach:

  • Platformer.Engine.pp – zawiera główną klasę gry z główną pętlą.

  • Platformer.Buffers.pp – zawiera zestaw tylnych buforów w postaci małych bitmap, potrzebnych do renderowania klatek. Jeden natywny bufor o rozmiarze 320x240 pikseli jest głównym, na którym renderowany jest ostateczny obraz danej klatki oraz drugi pomocniczy bufor, o rozmiarze 160x120 pikseli, do renderowania bardziej rozpikselowanych warstw (póki co wyłącznie do szumu). Do tego dojdzie jeszcze jeden bufor w natywnym dla gry rozmiarze – przyda się do przygotowywania obrazów warstw poziomów. Obstawiem że na trzech buforach się skończy.

  • Platformer.Renderers.pp – zawiera póki co jeden renderer, klasę do malowania tekstu z wykorzystaniem dedykowanego fontu. Docelowo będzie ich więcej, ale jeszcze nie wiem ile.

  • Platformer.Fonts.pp – zawiera klasę reprezentującą pojedynczy font ładowany z binarki. W chwili obecnej posiada podstawową funkcjonalność oraz obsługuje zestaw 72 znaków (literki małe i duże, cyfry oraz trochę znaków specjalnych), jednak rozszerzenie charsetu o nowe znaki to kilka minut roboty.

  • Platformer.Inputs.pp – zawiera klasę do obsługi klawiatury. Klasa ta przechowuje zestaw informacji na temat obsługiwanych w grze klawiszy – kod klawisza oraz jego stan (czy jest wciśnięty w bieżącej klatce i czy był wciśnięty w poprzedniej klatce). Dodatkowo każdy z klawiszy może posiadać klawisz przeciwny, dzięki czemu wciśnięcie jednego automatycznie gasi ten przeciwny. W ten sposób zabezpieczyłem możliwość np. wciśnięcia jednocześnie strzałki w lewo i prawo, aby silnik gry nie ocipiał.

  • Platformer.Counters.pp – zawiera klasę liczącą FPS-y oraz odmierzającą czas pomiędzy klatkami, aby klatkaż był stały. Pożera całą dostępną moc jednego jądra procesora, ale dzięki temu program działa płynnie w środowisku okienkowym. Działa wyśmienicie.

W pozostałych (niewymienionych) modułach znajdują się też klasy reprezentujące pojedyncze warstwy poziomu oraz poziom, bohatera, a także pozostałe obiekty (których nie chcę póki co zdradzać).

Skorzystałem z kilku zmiennych globalnych – przechowuję w nich instancje klas, które żyją od początku do końca sesji. Dzięki temu nie muszę ich deklarować w głównej klasie ”silnika” gry, a i nazwy tych zmiennych są przyjazne (w stylu Engine, Font, Keyboard, FrameBuffer itd.). Instancje te tworzone są w sekcji inicjalizacji modułów, a zwalniane w sekcji finalizacji. Wygodne.

Jeśli chodzi o to co już działa – główna pętla kręci prawidłowo, klawiatura działa, tylne bufory zdają egzamin, liczenie klatek oraz renderowanie tekstu też. Dodałem sobie już możliwość pauzowania działania (w dowolnym momencie) oraz przerywania działania aplikacji. Dodatkowo, dostępnych jest kilka funkcji pomagających w debuggowaniu, czyli możliwości manipulacji klatkażem (zakres od 1 do 60 klatek na sekundę).

Projekt wspiera kompilację warunkową, więc debug mode nie trafi na produkcję (choć pomyślę nad własnym Konami Code).


Póki co to tyle nowinek. Nie mam za bardzo co pokazać, więc w załączniku podaję ikonkę gry oraz animację przedstawiającą test trybu debugowania (trzeba obserwować prawy górny róg oraz środek). W kolejnych wpisach powinienem mieć już jakieś konkrety, chociaż samo działające intro.

Przy okazji – im więcej kodu naklepane, tym chętniej pisze się kolejne linijki i testuje kolejne funkcje. Nie sądziłem, że pisanie takiej pseudo-gierki może sprawiać tyle frajdy. A będzie jej jeszcze więcej! ;)

#free-pascal #lazarus #platformer #retro

Spine

Każda gra jest pełnoprawna.... just watch :P https://youtu.be/kDAKxjG7VaI?t=2m15s

furious programming
2018-05-27 02:14

Jakiś czas temu wrzuciłem na bloga dwa wpisy dotyczące grafiki – pierwszy o malowaniu tekstu z obramowaniem oraz drugi, o malowaniu kolorowych ikonek z otoczką. Wpisy raczej się podobały – szczególnie @kate87 – a więc dziś trzeci z cyklu bazgrołów we Free Pascalu. ;)


Niedawno informowałem, że robię małą demówkę, aby przetestować wymyślony efekt do hipotetycznej retro-gry platformowej. Wpadłem więc na pomysł, aby wykorzystać w niej efekt telewizyjnego szumu w pewnych momentach gry. Tak więc niżej przedstawiam najbardziej prymitywny sposób na wypełnienie bitmapy takim szumem. Kod jest bardzo prosty, ale efekt całkiem niezły.

Potrzebne 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;
 
type
  TNoiseDepth = (
    nd4Shades = 4,
    nd8Shades = 8,
    nd16Shades = 16,
    nd32Shades = 32,
    nd64Shades = 64,
    nd128Shades = 128
  );

Właściwa procedurka wypełniająca całą bitmapę wejściową:

procedure PaintTVNoise(ABitmap: TBitmap; ADepth: TNoiseDepth);
var
  LLine: PRGBTripleArr;
  LLineIndex, LPixelIndex: Integer;
var
  LShade, LRange, LMultiplier: UInt8;
begin
  LRange := UInt8(ADepth);
  LMultiplier := 256 div LRange;
 
  ABitmap.BeginUpdate();
 
  for LLineIndex := 0 to ABitmap.Height - 1 do
  begin
    LLine := ABitmap.ScanLine[LLineIndex];
 
    for LPixelIndex := 0 to ABitmap.Width - 1 do
    begin
      LShade := Random(LRange) * LMultiplier;
 
      LLine^[LPixelIndex].B := LShade;
      LLine^[LPixelIndex].G := LShade;
      LLine^[LPixelIndex].R := LShade;
    end;
  end;
 
  ABitmap.EndUpdate();
end;

Mamy do dyspozycji sześć ”głębokości” szumu, czyli sześć ustawień dotyczących liczby losowanych odcieni szarości – od 4 do 128 odcieni. Kolor biały nigdy nie jest używany (nigdy nie losuje liczby równej 255) – wzamian używany jest czarny, który bardziej będzie pasować do ponurej, ciemnej scenerii poziomów.

Rezultaty działania procedury dla poszczególnych ustawień głębi:

noise.png

Im większa głębia, tym więcej jaśniejszych pikseli, a więc jaśniejszy obraz – choć różnice są subtelne. Powyższy algorytm jest w sumie dość ubogi, jednak wbrew pozorom, możliwości regulacji jest bardzo dużo. Dostosowywać można nie tylko liczbę odcieni szarości, ale także zmniejszyć zakres czy kolor, przesunąć go w stronę koloru czarnego lub białego itd.

W załączniku podaję też plik .gif demonstrujący działanie szumu w pętli (zoom 2x).

#free-pascal #lazarus #platformer #retro

furious programming

@czysteskarpety, @cerrato: nie, bo jak opublikuję kod projektu to nie będę mógł wziąć udziału w konkursie. A bez kodu projektu czy chociażby binarki to już nie to samo, więc pozostaje mikroblog i co najwyżej zrzuty ekranu. Ten fragment kodu jest w sumie uniwersalny, dlatego pokazałem go tutaj.

@wioletta90: i jedno i drugie. Z tym że pracy w Polsce związanej z Free Pascalem raczej nie znajdzie – rozumiesz. ;)

cerrato

@wioletta90: uważaj.. bo zaraz wywołasz totalną gównoburzę... Z Pascalem jest jak z grzebaniem w nosie albo pierdzeniem - nawet jeśli to czasem robisz, nie wolno się przyznawać publicznie, bo zaraz zacznie się hejtowanie - że to żenada, brak profesjonalizmu, ten język to przeżytek, jedynie C++ albo Java, reszta jest dla ludzi zbyt ograniczonych, żeby zajmować się czymkolwiek na porządnym poziomie itp. Kiedyś popełniłem ten błąd i wspomniałem o Lazarusie (w dużym uproszczeniu - darmowy "klon" Delphi, do tego na wiele systemów). Co się później działo - możesz sobie poczytać tutaj: https://4programmers.net/Forum/1450206

furious programming
2018-05-20 20:22

Jakiś czas temu, gdy beztrosko młuciłem w Zeldę, dość sporo myślałem na temat tego, jak mogłaby wyglądać ta gra, gdyby nie ograniczenia sprzętowe. Jak można by ulepszyć grafikę, z jakich efektów skorzystać czy jakie dodatkowe funkcje mógłby posiadać główny bohater – utrzymując całość w mniej więcej oryginalnym stylu. Ale że bardziej wolałem (i nadal wolę) platformówki, to z kolei myślałem nad tym, jak można odpicować dwuwymiarową, płaską platformówkę w stylu retro.

Wpadłem więc na pomysł połączenia niektórych popularnych sztuczek imitujących głębię (przede wszystkim parallax scrolling) i pewnej, nie stosowanej właściwości warstw[1]. Całość ma być bardzo prosta graficznie, bo to zwykły PoC, więc skorzystam z prostych kształtów (kafle kwadratowe, jednokolorowe) i stylu stosowanego np. w grze Limbo, tyle że operującego na odcieniach danego koloru, a nie skali szarości. Chodzi przede wszystkim o to, aby sprawdzić, czy mechanika będzie się w ogóle nadawała do gier, a bez demówki trudno cokolwiek wywnioskować. Co nie zmienia faktu, że będzie to też gra (choć jako apka okienkowa – szybciej pójdzie), w którą będzie można pograć. Z jakimś bliżej nieokreślonym celem – przede wszystkim przechodzić kolorowe poziomy, może coś pozbierać (trochę frajdy musi być).

Zabrałem się właśnie za pierwsze linijki kodu – trzeba sobie przygotować zaplecze. Najpierw przygotuję sobie konsolaki do generowania binarek z poziomami, aby móc testować ”fizykę” oraz mechanizmy renderowania. Przyda się więc mieć jakiś byle jaki poziom na samym początku. Nie wiem ile to potrwa, ale myślę, że nie dłużej niż kilka dni i demówka będzie gotowa.


Przy okazji wpadłem na wątek dotyczący konkursu tworzenia gier w Lazarusie, więc źródła opublikuję najpierw tam. Taki jest jeden z warunków udziału – projekt nie może być już dostępny w sieci. Nie żeby mi zależało na wygranej – po prostu więcej ludzi zobaczy tę demówkę i może dzięki temu ktoś skorzysta z mojego pomysłu do stworzenia konkretnego produktu.


[1] Tej właściwości póki co nie chcę zdradzać, aby nie zepsuć efektu. Trzymajcie kciuki, aby się udało. ;)

#free-pascal #lazarus #platformer #retro

furious programming

O matko… jak ja dawno nie widziałem ditheringu… świetna gierka! :D

furious programming

Narzędzia mam gotowe, przykładowy kompletny poziom też. Teraz zabieram się za właściwy kod. :]

furious programming
2018-02-21 21:45

Pod moim poprzednim wpisem na temat tekstowych właściwości, @spartanPAGE skromnie stwierdził, że właściwości we Free Pascalu są chyba najbardziej rozwklekłe syntaktycznie jakie w ogóle są. Trudno mi powiedzieć (nie znam wszystkich języków), jednak jest w tym dużo prawdy – choć wynika to nie tylko z narzutu składniowego, ale też ze sporej funkcjonalności.

Prawda, jest trochę do napisania, nawet jeśli chodzi o zwykłą, typową właściwość:

public
  property Size: Integer read GetSize write SetSize;

No nie jest to krótki zapis, choć jakiś szczególnie długi też nie. Oczywiście nic nie stoi na przeszkodzie, aby skorzystać z kilku ficzerów i stworzyć takiego potwora:

public
  property Size[AFoo: Integer; ABar: Boolean; const ABaz: String]: Integer read GetSize write SetSize; default; platform; deprecated 'use something else';

Brakło mi sporego kawałka ekranu. Ja wiem, że to przykład bez większego sensu, jednak to daje jakieś pojęcie o możliwej objętości linii deklaracji właściwości (a była by jeszcze dłuższa, gdyby użyć dłuższej nazwy dla właściwości, a tym samym również dla settera i gettera).


Z drugiej strony, Free Pascal umożliwia deklarację właściwości w sposób bardzo krótki. Mowa o poniższej konstrukcji:

public
  property Size;

Ufff… zrobiło się przyjemniej. :]

Ale ale – nie ma typu danych, nie ma też określonej metody dostępowej i/lub zmieniającej, więc jak to niby ma działać…? A no działa, bo jest to specjalna konstrukcja. Specjalna, bo służy przede wszystkim do zmiany widoczności właściwości.

Konstrukcja ta stosowana jest powszechnie w komponentach – bazowe klasy komponentów posiadają określony, niemały zbiór właściwości uniwersalnych, które zadeklarowane są w sekcji public. Dzięki temu domyślnie nie są one wyświetlane w oknie inspektora obiektów. Każda klasa dziedzicząca po bazowej może używać danej właściwości, ale nie musi. Jeśli autor klasy komponentu uzna, że użytkownik powinien mieć do niej dostęp z poziomu okna inspektora obiektów1, zwiększa jej widoczność zapisując samą jej nazwę:

published
  property Size;  // w klasie bazowej znajduje się w sekcji public – zwiększamy widoczność

W ten sposób autor klasy ma możliwość określenia wymaganego zbioru właściwości, bez zaśmiecania inspektora tymi, które i tak nie będą wykorzystywane.


[1] Przeniesienie właściwości z sekcji public na published powoduje więcej zmian, niż tylko pokazanie jej w oknie inspektora obiektów.

#free-pascal #lazarus

Azarien

nikt ci przecież nie zabrania podzielić tej długiej linii na kilka…

furious programming

Tak samo w przypadku każdej innej dłuższej linijki, np. deklaracji metody – czy to cokolwiek zmienia? W dalszym ciągu zapis jest długi i jak nie zajmuje zbyt wiele miejsca w poziomie, to zajmuje go w pionie.

Sam nie mam zbyt wiele powodów do narzekania – mnie składnia FP odpowiada (choć małym wyjątkiem są generyki).

furious programming
2018-02-18 12:14

Dziś co nieco na temat komponentów, a dokładniej, o tekstowych właściwościach i ich przechowywaniu.

Opublikowane właściwości komponentów mogą posiadać wartości domyślne – dzięki temu nie są one zapisywane w plikach .lfm. To definiuje się za pomocą słówka default na końcu deklaracji właściwości. Właściwości tekstowe (typu String) też mogą posiadać wartości domyślne, nie składowane w plikach .lfm, jednak składnia jest nieco inna.

Otóż użyć należy słówka stored, a po nim podać wartość logiczną lub metodę zwracającą dane tego typu. Przykład:

published
  property Foo: String read FFoo write SetFoo stored True;

Powyższe oznacza, że wartość zawsze będzie przechowywana w pliku zasobów. O wiele częściej spotykaną praktyką jest używanie metody w stylu Is*Stored, zamiast literałów:

private
  function IsFooStored(): Boolean;
published
  property Foo: String read FFoo write SetFoo stored IsFooStored;

Metoda IsFooStored może wyglądać np. tak:

function TMyControl.IsFooStored(): Boolean;
begin
  Result := FFoo <> 'foo';
end;

A co jeśli mamy kilka właściwości tekstowych? Pasuje je zgrupować, czyli zamiast tworzyć wiele osobnych pól, stworzyć jedną macierz i odwoływać się do konkretnych pól za pomocą indeksu. Preferuję enumy zamiast zwykłych liczb. Przykład:

const
  MY_CONTROL_FOO = 'foo';
  MY_CONTROL_BAR = 'bar';
  MY_CONTROL_BAZ = 'baz';
 
type
  TMyControlTextKind = (mctFoo, mctBar, mctBaz);
  TMyControlTextsArr = array [TMyControlTextKind] of String;
 
{..}
 
private
  FTexts: TMyControlTextsArr;
private
  function GetText(AKind: TMyControlTextKind): String;
  procedure SetText(AKind: TMyControlTextKind; const AText: String);
published
  property Foo: String index mctFoo read GetText write SetText;
  property Bar: String index mctBar read GetText write SetText;
  property Baz: String index mctBaz read GetText write SetText;

Miodzio. Teraz czas na wartości domyślne. W dokumentacji czytamy (w jakże zacnym punkcie):

6.6.6 Storage information
The stored specifier should be either a boolean constant, a boolean field of the class, or a parameterless function which returns a boolean result. […]

Czyli dla każdej właściwości trzeba zrobić osobną metodę Is*Stored… Odczyt i zapis dało się ładnie uprościć za pomocą indeksowania, a tu taka lipa… Na szczęście przywykłem do tego, aby nie wierzyć dokumentacji (tym bardziej, że najnowsza wersja kompilatora to 3.0.4, a dokumentacja jest dla 3.0.2), więc sprawdziłem, czy faktycznie metoda Is*Stored musi być bezparametrowa:

const
  MY_CONTROL_TEXTS: TMyControlTextsArr = (MY_CONTROL_FOO, MY_CONTROL_BAR, MY_CONTROL_BAZ);
 
{..}
 
private
  function IsTextStored(AKind: TMyControlTextKind): Boolean;
published
  property Foo: String index mctFoo read GetText write SetText stored IsTextStored;
  property Bar: String index mctBar read GetText write SetText stored IsTextStored;
  property Baz: String index mctBaz read GetText write SetText stored IsTextStored;
 
{..}
 
function TMyControl.IsTextStored(AKind: TMyControlTextKind): Boolean;
begin
  Result := FTexts[AKind] <> MY_CONTROL_TEXTS[AKind];
end;

No i działa. Nie tylko kompiluje się bezbłędnie, ale i działa właściwie – poprawnie waliduje ciągi i do pliku .lfm zapisuje je wyłącznie wtedy, gdy różnią się od wartości uznawanych za domyślne (czyli tych ze stałych). To miło, bo dzięki temu można mieć wiele właściwości łańcuchowych w jednej klasie, ale tylko jedną metodę określającą czy posiadają domyślne dane (i tak samo: jedną do odczytu konkretnego ciągu i jedną do zapisu).

Jak to dobrze być dociekliwym. :D

#free-pascal #lazarus

furious programming

Nazwa kategorii dotyczy języków programowania, nie narzędzi. Obecna informuje, że zawiera wątki dotyczące Delphi i wszystkich innych języków wywodzących się z klasycznego Pascala. Tak samo sprawa wygląda np. z kategorią C/C++.

Natomiast całkiem niedawno uzupełniałem opisy kategorii (widoczne na stronie z ich listą), dodając informację o Free Pascalu, Lazarusie, Code Typhoon i Oxygene. Tego brakowało już od dawna, więc trzeba było co nieco poprawić.

cerrato

To jeszcze można byłoby zrobić małą dyskusję, jak traktować Delphi. Moim zdaniem (aczkolwiek są różne opinie/podejścia, więc nie musicie się zgadzać) w przypadku Delphi językiem jest Object Pascal, a słowo Delphi oznacza właśnie tą całą "otoczkę" - głównie IDE i VCL, czyli to co nazwałeś "narzędziem".