26.03.2016

Sposób na wyświetlanie tekstu bez użycia dodatkowej biblioteki

Od strony zasobów, font jest zapisany jak zwykła mozaika poszczególnych znaków, w jednym pliku, czyli sposób jej wczytywania nie różni się niczym od innych grafik tego typu. Po załadowaniu pliku z fontem tworzona jest powierzchnia RGBA, z której wycinane są znaki (na oddzielne, mniejsze powierzchnie), a ich wskaźniki umieszczane są w kontenerze vector<_SURFACE*> tiles w obiekcie typu tileSet.
Można zmienić kolor fontu poprzez funkcję tileSet::changeColor.
Indeks każdego znaku w kontenerze jest taki sam jak jego reprezentacja w standardowym kodzie ASCII.
Definicja funkcji drukującej dla okna znajduje się w "engine2d\screen.cpp". Jej deklaracja wygląda w ten sposób:
_SIZE print( tileSet* font, _SURFACE* d, _POS x, _POS y, const char *fmt, ... );

Funkcja zwraca liczbę wyświetlonych znaków (typ _SIZE i _POS jest wcześniej zdefiniowany jako unsigned int). Argument _SURFACE* d to jest powierzchnia gdzie ma się znaleźć dany tekst zapisany fontem (wskaźnik do obiektu typu tileSet). Lista argumentów dla tej funkcji jest przetwarzana przy pomocy vsprintf z biblioteki standardowej C.
Każde okno posiada flagę anchoredText, którą się ustawia wtedy, gdy chcemy, aby drukowany napis był zakotwiczony na danej powierzchni (wraz z przyjęciem kanału alfa powierzchni tła).

Powierzchnie z napisem można umieścić na macierzy sektorów, o której wspominałem tutaj. Lub też na wektorze wskaźników do obiektów wizualnych: vector< vector<visObj*> > vObjs;  (dla każdej warstwy oddzielny wektor), gdy nie chcemy umieszczać ich w macierzy sektorów. Przy nanoszeniu całego obrazu okna na wyświetlacz, poszczególne warstwy wektorów są wyświetlane zaraz po określonej warstwie macierzy.

25.03.2016

Wyświetlanie zawartości okna za pomocą macierzy sektorów

Postanowiłem trochę rozbudować i zoptymalizować sposób wyświetlania zawartości okien. W starszej wersji silnika, plany okna typu theWindow w całości stanowiły powierzchnie RGBA. To proste rozwiązanie ma jednak mankament, duże plansze wymagają alokacji dużej ilości pamięci, bo wszystkie obiekty (nawet jeżeli powtarzają się w kilku miejscach) są nakładane na całą powierzchnię planu okna, czyli są powielane w pamięci.
Dlatego dodałem do obiektu typu theWindow macierz sektorów ze wskaźnikami do powierzchni poszczególnych obiektów w oknie. Każdy plan okna ma swoją własną macierz dwuwymiarową złożoną z obiektów typu mSector. Każdy sektor zawiera wektor wskaźników do obiektów, które znajdują się w danej lokalizacji oraz flagę informującą czy ten sektor całkowicie zasłania niższe warstwy planów. Obiekty wizualne, znajdujące się na wektorze w sektorach, mogą mieć rozmiary większe od jednego sektora, w takim przypadku ten sam wskaźnik zostanie dodany do wektora sąsiedniego sektora (lub do wielu sąsiednich, w zależności od rozmiarów obiektu).
W ten sposób każdy obiekt wizualny o tym samym wyglądzie nie jest powielany na planie, jest jedynie dodany wskaźnik do odpowiedniej lokalizacji.

21.03.2016

Narzędzie do przygotowywania plików graficznych

ImageMagick jest prawdopodobnie najlepszym narzędziem do przygotowywania mozaiki (i nie tylko) grafik wykorzystywanych w tego typu projektach.

Np., aby utworzyć mozaikę składającą się z kilku obrazków, jeden pod kolejnym, do postaci jednego pliku:
# convert 1.png 2.png 3.png 4.png -append all.png
albo: # convert -append *.png all.png
Należy pamiętać, że w tym drugim przypadku pliki są brane w kolejności alfabetycznej (a nie numerycznie naturalnej).

Np., aby powycinać z dużego pliku mniejsze obrazki o określonych rozmiarach (np. 16 na 32 pikseli) i zapisać je w oddzielnych plikach, z indeksami 3 cyfrowymi z wypełnieniem po lewej cyfrą 0:
# convert -crop 16x32 big_file.png %03d_out.png
Tworzenie indeksów z tą samą liczbą cyfr jest ważne, gdy później chcemy zachować numerycznie naturalną kolejność w przypadku podania tych plików maską "*_out.png" do innych operacji.

Np., aby wypakować z animacji poszczególne ramki (klatki) obrazów i zapisać je do osobnych plików:
# convert animation.avi out_frame_%03d.png

18.03.2016

Skrypt AutoHotkey'a, używany przy tym projekcie

Każdy kto zainteresował się automatyzacją monotonnych czynności w systemach MS Windows pewnie zna narzędzie AutoHotkey.

Używam go między innymi przy tym projekcie do:

# Nadpisywania clipboard'a:
lalt & space::
if clipboard !=
  saveClipboard := clipboard . "`r`n"
send ^c
clipboard := saveClipboard . clipboard
return

# Wpisywania zmiennych z clipboard'a do funkcji debugującej fprintf( stderr, "(var1)%i  (var2)%i ...\n", var1, var2, ...):  
!rshift::
SendInput {enter 2}{up}fprintf( stderr, "
if clipboard =
{
  SendInput _1\n" );{enter}
  return
}
Loop, parse, clipboard, `n, `r
{
  SendInput {space}{raw}(%A_LoopField%)`%i
}
SendInput \n"
Loop, parse, clipboard, `r, `n
{
  SendInput {raw}, %A_LoopField%
}
SendInput {space});{enter}
return

# Przełączania na okno konsoli tekstowej i uruchamiania skryptu kompilującego skrótem left ctrl + 1:
NOTEPAD_TITLE := " - Notepad++"
CPP_TITLE := "c/c++_projekt"
PHP_TITLE := "$php$_projekt"


lctrl & 1::
Title := topWindow()
StringGetPos, sp_pos, Title, %NOTEPAD_TITLE%
if sp_pos > 0
{
send ^s
WinGetTitle, Title, A
Title := SubStr( Title, 1, sp_pos )
SplitPath, Title, file_name, file_dir, file_ext, name_no_ext, file_drive

file_ext := Trim( file_ext )
if( file_ext = "cpp" or file_ext = "c" or file_ext = "h" )
{
IfWinExist, %CPP_TITLE%
{
  WinActivate
;  WinMaximize
} else
{
  run, "%SYS_SHORTCUTS_DIR%ansicon_projekt.bat" %file_drive% "%file_dir%" %CPP_TITLE%
  WinActivate, %CPP_TITLE%
  sleep, 100
}
Send, ccpp{Enter}
} else
if( file_ext = "php" )
{
  IfWinExist, %PHP_TITLE%
{
  WinActivate
} else
{
  run, "%SYS_SHORTCUTS_DIR%ansicon_projekt.bat" %file_drive% "%file_dir%" %PHP_TITLE%
  WinActivate, %PHP_TITLE%
  sleep, 100
}
  send cls{enter}
  new_main =
  ifNotExist, %file_dir%\%name_no_ext%.exe
    new_main = embeder2 new %name_no_ext%{Enter}    
  send, bcomp %name_no_ext%.php{Enter}%new_main%embeder2 main %name_no_ext% %name_no_ext%.phb{Enter}%name_no_ext%.exe{Enter}
}
}
return


topWindow() {
  WinGetTitle, Title, A
  if( Title = "" )
  {
    send !{Tab}
    Sleep, 35
    WinGetTitle, Title, A
  }
  return Title
}
Ta część skryptu sprawdza czy obecne okno to Notepad++, a następnie rozkłada pełną ścieżkę pliku (z tytułu okna edytora) na czynniki podstawowe. Jeżeli plik ma rozszerzenie "cpp", "c", lub "h" to odpalany jest skrypt kompilujący dla projektu c/c++, w katalogu wziętym również z informacji z tytułu okna edytora. Jeżeli plik ma rozszerzenie "php" to odpalany jest kompilator dla projektu php. Oczywiście można też "ręcznie" uruchamiać kompilację poprzez wpisywanie komendy "m.bat", lub ccpp, w oknie konsoli dla "c/c++_projekt", i bcomp ... w przypadku "$php$_projekt".
"ansicon_projekt.bat" uruchamia cmd.exe, w określonym katalogu, z zainstalowaną konsolą obsługującą kody ANSI Esc, ustawia zmienne środowiskowe, domyślne ścieżki dostępu do narzędzi systemu i kompilatora.

16.03.2016

Prosty sposób debugowania, używany w tym projekcie

Jak do tej pory, do pracy nad tym projektem używam notepada++ (w wersji minimalistycznej) wraz z kompilatorem zawartym w MinGW i swoim skryptem kompilującym. Bez żadnych dedykowanych edytorów. Na razie nawet nie zauważyłem potrzeby użycia gdb.
Kiedy chcę poobserwować zachowanie jakichś zmiennych używam funkcji fprintf( stderr,...)
Zwykły printf komplikuje sprawy buforowaniem standardowego wyjścia, czasami wymaga użycia fflush.

W przypadku MinGW z SDL'em, aby w ogóle można było użyć standardowego wyjścia, trzeba wyrzucić flagę -mwindows przy linkowaniu całości (sprawdź konfigurację skryptu kompilującego). Opcja ta jest odpowiedzialna za zamykanie okna konsoli tekstowej. Możliwe jest też użycie opcji -mconsole razem z -mwindows, (aby zachować okno konsoli, w niektórych wersjach MinGW).

Przy tego typu debugowaniu (poprzez standardowe wyjście tekstowe) warto jest uruchamiać program interpreterem poleceń z nakładką, która przetwarza kody ANSI Esc (np. ansicon.exe, jednorazowe wywołanie z opcją -i instaluje ją na stałe, zaś opcja -u odinstalowuje). Wtedy można m.in. wyróżnić kolorem i rozmieścić debugowane informacje na ekranie konsoli tekstowej.

11.03.2016

Organizacja kodu w aspekcie silnika

Kod źródłowy, który nie jest bezpośrednio zależny od ramy systemowej ("framework'u"), systemu operacyjnego i mechanizmów samej gry, umieszczam w katalogu "engine2d", czyli wszystko to co może być wykorzystane w dowolnie innym programie z tego typu interfejsem graficznym 2d. Odwołania pośrednio związane z systemem są już zuniwersalizowane poprzez "typedef'y" i funkcje inline, w "framework_dependent/fw.h", np. typy: _PIXEL, _COLOR, _SIZE, i funkcje: _LOAD_IMG, _CREATE_RGBA_SURFACE, _APPLY_ON_DISPLAY, _UPDATE_DISPLAY itp.

Obecnie  w "engine2d" znajdują się sprawy związane z oknami, konstruktor obiektu typu theWindow, przykładowa funkcja składowa theWindow::redrawAllPlanes(). W folderze tym są też umieszczone funkcje pomocnicze związane z wczytywaniem i przetwarzaniem zasobów typu tileSet. Powinna tam też znaleźć się implementacja animacji, detektor kolizji, sposoby wyświetlania tekstu, i wszystko co będzie miało praktyczną użyteczność dla tego typu powierzchni pikseli (_SURFACE) i innych rodzajów warstw wizualnych, które mogą zostać dodane w pszyszłości.

10.03.2016

O swoim skrypcie kompilującym, napisanym w php

Uważam, że skrypt "ccpp.php" jest całkowicie wystarczający i uniwersalny dla średnich i małych projektów, które korzystają z dowolnego środowiska programistycznego. Potrzebna jest jedynie wiedza o tym w jaki sposób uruchamiany jest kompilator z linii poleceń, w powłoce tekstowej systemu i dowolne narzędzie do odpalenia samego skryptu (np. phc-win, nawet nie trzeba instalować powłoki php).
Skrypt wykonuje same proste czynności, w sposób zautomatyzowany:
1. Przeszukiwane są określone katalogi w celu znalezienia plików źródłowych.
2. Kompilowane są moduły dla każdego pliku źródłowego, jeżeli uległy zmianie (data modyfikacji źródła jest nowsza od daty modyfikacji lub utworzenia modułu).
3. Linkowane są wszystkie moduły.

W tym projekcie, obecny plik konfiguracyjny "!ccpp.txt" dla skryptu kompilującego wygląda następująco:

-c
-mwindows -luser32 -lmingw32 -lSDLmain -lSDL -lSDL_image -s
framework_dependent
engine2d
.

Pierwsza linijka to opcje kompilatora, użyte przy kompilacji poszczególnych plików źródłowych. Druga linia to opcje użyte przy linkowaniu całości. Pozostałe linijki to lista katalogów, z których mają być brane pliki do kompilacji (ostatni katalog to kropka, czyli aktualny folder, z którego został odpalony skrypt kompilujący).

Opcje z linii poleceń dla skryptu są następujące:
# ccpp --help
Easy compiling for c++ v2.1, usage: ccpp [options]

-e (default '_')
-cpp (default '.cpp')
-obj (default '.o')
-exe (default 'main.exe')
-compiler (default 'g++')

-linker (default 'g++')
-stop (default ' error: ')
-makefile (default '!ccpp.txt')

Jest możliwość wyłączenia niektórych plików źródłowych z kompilacji, poprzez opcję -e, która  określa prefiks w nazwie takiego pliku (domyślnie jest to znak "_").
Opcja -stop nakazuje zatrzymanie działania skryptu, po napotkaniu określonego tekstu w standardowym wyjściu komunikatów kompilatora (domyślnie jest to " error: ", bo g++ komunikuje w ten sposób błąd kompilacji). W zależności od potrzeby można też ustawić, poprzez tą opcję, zatrzymanie skryptu w przypadku ostrzeżenia (np. "warning:" dla g++), lub dla dowolnie innego przypadku, z określonym tekstem, po którym nie warto kontynuować dalszej kompilacji.

09.03.2016

Ładowanie grafiki z zasobów i tworzenie powierzchni RGBA

Ścieżki do plików graficznych znajdują się w "config_resources.h". Sposób Wczytywania grafiki zależy od użytej biblioteki. W moim przypadku użyłem funkcji SDL'a do ładownia plików jpg i png. Produktem na wyjściu takiej funkcji jest surowy, nieskompresowany ciąg danych reprezentujących kolejne piksele w formacie RGBA. Wskaźniki do tych danych znajdują się w strukturach typu _SURFACE. Wszystkie nazwy załadowanych plików zasobów i identyfikatory powierzchni będą się znajdowały w "config_resources.h".
Obiekty typu tileSet (zdefiniowanego w "engine2d/resources.cpp") zawierają powierzchnie typu _SURFACE wczytanych kolejno ramek (jeżeli plik był z obrazem mozaiki trzeba podać też szerokość i wysokość jednej ramki). Domyślnie wczytywany jest cały obraz do jednej powierzchni. Typ tileSet ma też zdefiniowane m.in. funkcje składowe do zmiany określonego koloru i przeźroczystości (jeżeli jest taka potrzeba).
Każde okno dla określonego ekranu może mieć kilka warstw powierzchni. Powierzchnie są nakładane na siebie w kolejności według indeksu wektora wskaźników. Na samym początku, przy budowie tła określonej warstwy, jeżeli rozmiary wczytanej wcześniej ramki (która ma wypełnić daną warstwę) są mniejsze od rozmiarów warstwy, to ta ramka zostaje powielona sąsiadująco.
Np. tak wygląda pierwsza warstwa (czyli najdalszy plan wizualny) planszy, na której znajduje się motyw wody. Ramka tekstury z wodą została powielona sąsiadująco, gdyż po wczytaniu z pliku była mniejsza od rozmiarów całego planu poziomu:

05.03.2016

Ogólna organizacja ekranów i okien własnych gry

Pisząc słowo "ekran", w kontekście tego projektu, mam na myśli cały, aktualnie wyświetlany obszar (okno systemowe zainicjowane do trybu graficznego), który jest przeznaczony na tą grę, po jej odpaleniu. Od strony abstrakcyjnej, gra może mieć wiele ekranów, które można dowolnie przełączać. Gra, w starszej wersji, ma zmontowane trzy ekrany: ekran menu (pojawiający się zaraz po odpaleniu gry), ekran związany z samą grą (mapa z graczem), oraz ekran wyświetlany przed pojawieniem się mapy planszy, z informacjami, które chcemy przekazać graczowi przed rozpoczęciem gry. W nowszej wersji zachowam tą samą organizację ekranów.
Od strony interfejsu programistycznego, ekrany to obiekty typu theScreen umiejscowione w głównym, globalnym obiekcie typu theGame - obiekt ten, oprócz tablicy ekranów, posiada też: identyfikator aktualnie wyświetlanego ekranu, wskaźnik do zasobów gry, wskaźnik do aktualnej planszy. Obiekty ekranu zawierają kontenery z oknami własnymi (wskaźniki do obiektów typu theWindow), które są zupełnie inne niż okna systemu operacyjnego. Te obiekty, typu theWindow (zdefiniowanego w pliku "engine2d/screen.h") zawierają niezbędne minimum, czyli: rozmiary i pozycję okna na ekranie, wskaźnik do użytych w nim warstw planów, informacje o przewijaniu zawartości (bo nie zawsze powierzchnia graficzna związana z określonym oknem mieści się w jego rozmiarach), funkcje składowe związane z wyświetlaniem grafiki i tekstu.
Każdy ekran ma swoją własną obsługę zdarzeń urządzenia sterującego, poprzez odwołania do globalnej tablicy funkcji KeyEventsTab zdefiniowanej w "globals.h".
Okna własne określonego ekranu, są podstawą konstrukcji interfejsu graficznego w tym projekcie.

 
Ekran menu


Intro do określonej mapy

Ekran z mapą i graczem

03.03.2016

O estetycznym stylu kodowania, użytym w nowszej wersji tego projektu

Nie wnikałem, czy istnieje jakaś fachowa nazwa tego stylu kodowania. Zauważyłem go przy przeglądaniu początkowych wersji źródeł biblioteki GLFW i spodobał mi się. Chodzi m.in. o dodawanie spacji po nawiasie w wywołaniu funkcji i w instrukcjach takich jak if, while, for, itp. Moim zdaniem (bo jest to sprawa czysto subiektywna) taki kod jest bardziej przejrzysty.
np.: nazwa_funkcji( arg1, arg2 );
zamiast: nazwa_funkcji (arg1, arg2);

albo: for( int i = 0; i < max; i++ )
zamiast: for (int i=0; i<max; i++)

Z kolei, w wyrażeniach arytmetycznych i rzutowaniu typów, bardziej czytelne według mnie, jest brak spacji przy nawiasach:
np: a = (float)(b + c) / x;
zamiast: a = ( float )( b + c ) / x;

W przypadku wyrażeń arytmetycznych wewnątrz indeksu, w nawiasach kwadratowych:
np.: t[ (a * 3) ] = str[ (a + b * w[ x ]) / 2 ];
zamiast:  t[(a*3)] = str[(a+b*w[x])/2];


Jeżeli chodzi o zapis deklarujący zmienną wskaźnikową to czytelniej (moim zdaniem, też trochę logiczniej) jest zrobić spację przed nazwą zmiennej typu wskaźnikowego:
np.:     char* str;
zamiast: char *str;

np.: char** argc;
zamiast: char **argc;

lub np.: typedef int* int_ptr;
zamiast: typedef int *int_ptr;
Myślę, że w tym pierwszym przypadku widać trochę wyraźniej, że chodzi o wskaźnik określonego typu, a nie o "zmyłkową" dereferencję zmiennej niewskaźnikowej, lub zdefiniowanie "typu dereferencyjnego" (w przypadku typedef).

01.03.2016

Organizacja kodu w aspekcie systemu operacyjnego

Na samym początku przebudowy kodu, wszystkie funkcje, zmienne, struktury zależne bezpośrednio od biblioteki systemowej (lub ramy systemowej, w moim przypadku od SDL'a) umieściłem w oddzielnym katalogu "framework_dependent". Każdy inny moduł, z poza tego folderu, będzie już odnosił się tylko do kodu samej gry, lub silnika gry, z możliwością odwołania się pośredniego do spraw czysto systemowych, poprzez odpowiednio zuniwersalizowane nazwy zdefiniowane w pliku nagłówkowym "framework_dependent/fw.h" i poprzez zmienne globalne z "globals.h". Wszystkie nazwy zmiennych globalnych zapisuje z dużej litery.
Przy wstępnym obmyślaniu mapy powiązań plików nagłówkowych z logiczną strukturą kodu, zauważyłem jeden aspekt organizacyjny. Mianowicie unikanie, wszędzie tam gdzie to jest możliwe, dyrektywy #include w plikach nagłówkowych znacznie zmniejszyło częstotliwość występowania konfliktów deklaracji.

Nazwy typów, funkcji i zmiennych, które są zależne od systemu (lub od biblioteki ramowej, oprócz tych najbardziej pierwotnych np. uint32, uint8), piszę dużymi literami, ze znakiem "_" na początku. Są to nazwy, które informują w sposób zuniwersalizowany (w miarę możliwości) o zadaniu, z którym są związane.
Główna funkcja odpalająca cały program znajduje się w "main.cpp", poza katalogiem "framework_dependent" i obecnie wygląda nastepująco:
int main( int argc, char** argv ) {
  _FRAMEWORK_INIT();
  initGame();
  _FRAMEWORK_LOOP();
  _FRAMEWORK_END();
  return 0;
}

Pętla główna w "framework_dependent/loop.cpp" odnotowuje sam fakt wystąpienia zdarzenia (systemowego, sprzętowego, lub związanego z mechaniką samej gry) i umieszcza tą informację na tablicy globalnej. Pętla ta jest zainicjowana w taki sposób, że reaguje jedynie na zdarzenia klawiatury, zamknięcie okna i zdarzenia zdefiniowane przez programistę. Ten zabieg pozwala zmniejszyć obciążenie procesora, w przypadku gdy na ekranie nic się nie dzieje, lub gdy gracz nie wykonuje w danej chwili żadnej akcji na urządzeniu sterującym. Gra nie generuje żadnych dodatkowych obliczeń w tle.

Kilka wstępnych słów

Witam.

Pod koniec 2014 roku stworzyłem prototyp gry logicznej "Metod", który umieściłem na stronie: http://dftruf.dx.am Są tam zrzuty ekranu i link do skompilowanej wersji dla systemów MS Windows, którą można odpalić i wstępnie zaznajomić się z interfejsem i ogólnymi zasadami gry.
Na GitHub'ie:  https://github.com/konstruktywnie/metod-older/blob/master/metod-older-0_9_2_s rc.zip
umieściłem kod źródłowy (w C/C++) wraz z zasobami programu (grafiki, animacje, font, mapy plansz), bibliotekami dll i własnym skryptem napisanym w php ("ccpp.php") do kompilowania całości, ponieważ przez większość pracy nad prototypem używałem jedynie notepada++ i MinGW (na Windows XP) z Code::Blocks'a, bez "drag'n'dropowego", zintegrowanego środowiska programistycznego i bez sztucznie zagmatwanego (moim zdaniem) unix'owego makefile'a.
Całość napisałem w spontaniczny i nieuporządkowany sposób, bez zachowania logicznej struktury podziału na tzw. "framework" (po polsku tłumaczę to jako: podstawa, lub rama systemowa), algorytmy organizacji zasobów, zdarzenia programowe i kod związany z samą grą.

Teraz postaram się uporządkować i przebudować cały kod silnika tak, aby można było w stosunkowo prosty sposób zmienić ramę systemową, z użytego przeze mnie SDL'a (Simple DirectMedia Layer), na każdą inną, łatwo przenośną, działającą na większości systemów operacyjnych. Zmienię też sposób implementacji ruchomych obiektów i animacji z wątkowego na podejście z użyciem "timerów", które, w tym przypadku, są znacznie bardziej odpowiednie. Myślę też nad dodaniem trybu współpracy, gdzie dwóch, lub większa ilość graczy, będzie mogła wspólnie rozpracowywać określone plansze.
Nowy kod aktualizuje na: https://github.com/konstruktywnie/metod

Tutaj będę umieszczał zapiski pokładowe o postępie prac z tej przygody.