16.05.2016

Motywacja

Wiele inspiracji, przy realizowaniu tego projektu, popłynęło z atari ("Robbo", "Crystal Mines 2"), amigi ("Stone Age", "Imagems") i gp2x'a ("Sqrxz").
Czyli niemodna inspiracja najczęściej prowadzi do niemodnego projektu. Mimo to, "niemodny" nie musi oznaczać "no modding".

12.05.2016

Dlaczego użyłem SDL 1.2, a nie 2.0, w tym projekcie

Próbowałem SDL'a w wersji 2.0 jakiś czas temu, w środowisku programistycznym MinGW, na Win. XP i słyszałem, że ta wersja ma jeszcze jakieś niedociągnięcia. W przypadku zaistnienia potrzeby użycia GPU jest jeszcze biblioteka SDL_gpu, która  rekompensuje brak wspomagania grafiki w SDL'u 1.2.
Poza tym, aplikacja która kładzie nacisk na użyteczność (lub długotrwałą grywalność) i nie wymaga efekciarskich bajerów wizualnych dla poklasku, może korzystać tylko z CPU, w ten sposób staje się bardziej przenośna, bez fizycznej zależności od GPU (i standardów z nim związanych) jakiejś konkretnej korporacji lub konsorcjum. Piksele to tylko piksele, są apolityczne.

07.05.2016

Przemieszczanie obiektu wizualnego

W grze przemieścić można obiekty, które niekoniecznie muszą mieć animowane sekwencje ramek, dlatego funkcje realizujące przemieszczanie są składowymi ogólnego obiektu wizualnego (zdefiniowane w "engine2d\resources.cpp").
Funkcja visObj::startMove dodaje timer z callback'iem (visObjCallback) do rzucania zdarzenia programowego EVENT_MOVE_OBJ, które jest wykorzystywane do aktualizacji macierzy sektorów, poprzez funkcję visObj::deltaMove, oraz do renderowania tego obiektu funkcją visObj::draw.

Funkcja visObj::deltaMove, oprócz modyfikowania współrzędnych, wykonuje też następujące czynności:
* Oblicza jakie jest nowe położenie obiektu w macierzy sektorów (poprzez ustawione wcześniej zmienne: dx, dy).
* Oblicza nowy rozmiar (w sektorach) zajmowany przez obiekt w macierzy sektorów (po aktualizacji współrzędnych).
* Modyfikuje kontenery sektorów ze wskaźnikami do tego obiektu (jeżeli położenie lub rozmiar obiektu wykroczył poza sektory z wcześniejszego stanu).
* Sprawdza czy przemieszczenie nie osiągnęło ustalonego wcześniej zasięgu (poprzez zmienne: x_moveRange, y_moveRange), jeżeli tak to przemieszczenie jest zastopowane za pomocą visObj::stopMove.

06.05.2016

Modernizacja obiektu wizualnego

Postanowiłem przebudować trochę sposób dostępu do struktury okna, z każdego obiektu wizualnego (w tym animacji). Pliki źródłowe, których dotyczą te modyfikacje to: "engine2d\resources.h", "engine2d\resources.cpp", "engine2d\screen.cpp" (funkcja theWindow::putOnMatrix).
Wcześniej tylko obiekt animacji typu tileSeq zawierał wskaźnik do okna. Teraz, w taki wskaźnik jest wyposażony każdy obiekt wizualny typu visObj. Pociąga to za sobą kilka zmian w sposobie  przygotowywania obiektów przy budowie mapy.

Obecnie, zainicjowanie animacji (np. dla okna planszy) wygląda w ten sposób:
someAnimation.actualVO = Game.screens[ SCREEN_GAME ].windows[ WINDOW_LEVEL ]->putSeqOnMatrix( plane, &someAnimation, posX, posY );

Bez dodatkowej linii dowiązującej tę animację do konkretnego okna:
someAnimation.win = Game.screens[ SCREEN_GAME ].windows[ WINDOW_LEVEL ];


Funkcja składowa putSeqOnMatrix jest nadbudówką funkcji putOnMatrix. Samodzielnie sprawdza, która powierzchnia stanowi aktualną ramkę animacji, poprzez zmienną actualSeq w obiekcie tileSeq.

30.04.2016

Odświeżanie animacji

Obiekt animacji typu tileSeq zawiera wskaźnik do okna (poprzez wskaźnik do aktualnej ramki actualVO), w którym ma być wyświetlany, za pomocą funkcji theWindow::redrawField. Po upływie czasu przerwy między przejściami ramek, wyświetlana i odświeżana jest kolejna ramka, poprzez funkcję składową tileSeq::draw(). Indeks tej ramki ustalany jest za pomocą sumy wartości zmiennej actualFrame w aktualnej sekwencji i zmiennej nextFrame. Jeżeli zmienna nextFrame ma wartość ujemną wtedy taka animacja będzie odtwarzana wstecz.

Jeżeli aktualna ramka osiągnie indeks graniczny, dla jej sekwencji, wtedy aktualny indeks ustawiany jest na początek w tej sekwencji (w przypadku zapętlonej animacji), lub jest wstrzymywana cała animacja (poprzez wyłączenie timera dla tego obiektu).

29.04.2016

Funkcja do sprawdzania obszaru wspólnego dwóch przestrzeni dwuwymiarowych

Funkcja do obliczania obszaru wspólnego dwóch przestrzeni dwuwymiarowych, w tym projekcie, użyta jest do sprawdzenia czy wyświetlany obiekt wizualny nie wychodzi poza okno lub widzialny obszar do odświeżenia zmian (taki obszar nie musi mieć rozmiarów całego okna). W przyszłości będzie też potrzebna do detektora kolizji.

Definicja tej funkcji znajduje się w "engine2d\resources.cpp", a jej deklaracja wygląda następująco:

bool jointField( _POS f1x, _POS f1y, _SIZE f1w, _SIZE f1h, _POS f2x, _POS f2y, _POS f2w, _POS f2h, _POS& jf1x, _POS& jf1y, _SIZE& jfw, _SIZE& jfh );

Pierwsze osiem argumentów tej funkcji to pozycja i rozmiary obydwu obszarów. Kolejne argumenty referencyjne zawierają wynikowy obszar wspólny, czyli jego pozycję (jf1x, jf1y) względem pierwszego obszaru, oraz szerokość i wysokość (jfw, jfh).
 

23.04.2016

Inicjacja timera animacji

Rozpoczęcie animacji następuje po wywołaniu funkcji składowej tileSeq::start(). Funkcja ta dodaje timer, który po upływie czasu (podanego w zmiennej interval) generuje programowe zdarzenie wychwytywane w pętli głównej (każde zdarzenie jest przetwarzane według kolejki dodania). Obsługa zdarzenia animacji jest realizowana za pomocą funkcji attendProgEvents w "events_prog.cpp", która inicjuje renderowanie ramki w określonym oknie.
Implementacja timera zależy od użytej biblioteki. W przypadku tego projektu jest to SDL_AddTimer, który umieściłem w inline'owej, zuniwersalizowanej funkcji o nazwie _ADD_TIMER. Organizację kodu w aspekcie systemu operacyjnego omówiłem już we wcześniejszej notce.

22.04.2016

Dodanie animacji do macierzy sektorów

Animacja jako obiekt typu tileSeq jest zdefiniowana w "engine2d\resources.h". Zawiera kontener z ramkami typu _SURFACE, identyfikator animacji, wektor z sekwencjami, aktualnie ustawiona sekwencja, położenie i wskaźnik do aktualnie wyświetlanej ramki.
Pojedyncza sekwencja zwiera informacje - od której do której ramki odtwarzać określoną animację. Każda animacja może być wyświetlana na wiele sposobów np. od końcowej do początkowej ramki, albo np. od ramki piątej do osiemnastej i właśnie do tego służą sekwencje.
Tworzenie obiektu animacji może przebiegać na dwa sposoby. Pierwszy, poprzez konstruktor z podaną ścieżką do pliku mozaiki, z której ma zostać utworzona animacja. Drugi sposób, poprzez użycie funkcji składowej tileSeq::prepare( tileSet* t ). Przypomnę, że obiekt typu tileSet zawiera kontener z już załadowanymi powierzchniami RGBA (typu _SURFACE). Domyślnie jest tworzona jedna sekwencja: od ramki pierwszej (z indeksem 0) do ramki ostatniej.
Dodanie takiej animacji do macierzy sektorów (w celu jej przyszłego wyświetlenia), w określonym oknie, na konkretną pozycję, odbywa się za pomocą funkcji theWindow::putSeqOnMatrix.

16.04.2016

Fizyka mapy

Obecnie, każda jednostka mapy zawiera flagi informujące:
* czy przez ten sektor można przemieścić cokolwiek (nie tylko pojazd gracza);
* czy w tym sektorze znajduje się przedmiot, który można zabrać do inwentarza;
* czy w tym sektorze znajduje się obiekt, który można przemieścić;
* czy w tym sektorze znajduje się dziura;
* czy podłoga tego sektora to ruchoma platforma wodna;
* czy obiekt znajdujący sie na podłodze może eksplodować.

Początkowe stany flag są ustawiane w funkcji theLevel::buildMap przy konstruowaniu mapy z zasobów.

15.04.2016

Przydział podłogi przy budowie mapy

Format zapisu mapy (opisany wstępnie tutaj) jest dwuwymiarowy, więc funkcja odpowiedzialna za konstruowanie mapy musi sama rozpoznać, które jej elementy są położone na podłodze (czyli musi być dodana podłoga, w niższej macierzy sektorów), a które stanowią jej poziom. Część programu odpowiedzialna za tą czynność umieszczona jest w theLevel::buildMap w "level.cpp".
Przykładowa mapa "tutorial 1" z rozmieszczonymi na niej elementami, wygląda w ten sposób:
Zamiast:

10.04.2016

Przygotowanie wizualnych elementów mapy

Niektóre obiekty na mapie nie są identyczną kopią tego co znajduje się w pliku zasobów grafiki. Dlatego trzeba je najpierw odpowiednio przygotować przed umieszczeniem ich w kontenerze kafli mapy.

Np. ruchome platformy:
mogą być skierowane w różnych kierunkach, więc trzeba utworzyć ich obrócone, o odpowiednią liczbę stopni, odpowiedniki.

W przypadku przejść kierunkowych oprócz obrotu strzałki:
trzeba też nałożyć na siebie jej obraz z samym typem przejścia, np.:


 Wszystkie te sprawy są zebrane w funkcji pomocniczej prepareMainTiles w "resources_constructor.cpp".

08.04.2016

Odwzorowanie znaków ASCII z pliku mapy na identyfikatory powierzchni

Mapa planszy jest umieszczona w obiekcie typu theLevel i składa się z jednostek obszarów mapUnit.
Funkcja interpretująca dane z pliku mapy theLevel::buildMap( _CHAR* file ) sprawdza najpierw jaka jest długość pierwszej linii (z pominięciem linii konfiguracyjnej). Jeżeli rozmiar linii jest nieparzysty (a musi być, gdyż każdy obszar mapy reprezentowany jest dwoma znakami) funkcja zwraca komunikat o błędnym wprowadzeniu danych dla mapy. Podobnie jest w przypadku, gdy długość którejś następnej linii jest inna niż długość pierwszej. Ta funkcja sprawdza też czy istnieje w ogóle reprezentacja obszaru podana określonymi znakami.

Jednostka mapy zawiera informacje o tym co znajduje się w tym obszarze, jaki jest identyfikator podłogi i identyfikator obiektu, który znajduje się na tej podłodze (jeżeli jest). Czy określony obiekt można poruszyć, czy w ogóle można przemieścić coś na daną lokalizację, czy znajduje się na niej ruchoma platforma, itp.

02.04.2016

Format zapisu mapy

W katalogu "data\levels" znajdują się pliki zasobów związanych z mapami i informacjami do wyświetlenia dla gracza, przed określoną misją.
Format zapisu mapy stanowi standardowy tekst ASCII. Podstawowa jednostka obszaru mapy reprezentowana jest przez dwa znaki. Np.: "##" - ściana, ".," - podłoga, "';" - woda, ">>" - ruchoma platforma wodna, "k2" - zielony klucz, itd.
Pierwsza mapa samouczka, w pliku "data\levels\tutorial 1", wygląda następująco:

120 100 9 0 7 0 8 5 intro1
.,.,.,.,.,.,.,.,.,.,';';';';';
.,##############.,.,';';';';';
.,##.,.,.,.,.,##.,.,.,';';';';
.,##.,.,g1.,.,##.,.,.,.,';';';
.,####.,.,.,.,d2.,b2.,.,.,';';
.,##.,.,g3.,.,##.,.,.,PL.,';';
.,##############.,.,.,.,';';';
.,##.,.,.,.,.,.,.,.,.,.,';';';
.,##.,';';c#';';.,.,';';';';';
.,##.,';';k2>>';.,.,';';';';';
.,##.,';';';';';.,.,';';';';';
.,##.,.,.,.,.,.,.,.,';';';';';
.,##############.,.,';';';';';
.,.,.,.,.,.,.,.,.,.,';';';';';
.
g1....
..b2..
....g3
W pierwszej linii znajdują się podstawowe informacje odnośnie tej mapy i początkowe zasoby pojazdu: czas w sekundach, paliwo, HP, amunicja (3 typy), modyfikatory uszkodzeń, "intro1" to nazwa pliku, w którym zapisane są informacje wprowadzające (wyświetlane dla gracza przed misją).
Linie pod mapą definiują cel ukończenia misji - ułożenie odpowiednich obiektów, w odpowiedniej kombinacji (można zdefiniować wiele następujących po sobie celów).

01.04.2016

Dodawanie nowych okien do określonego ekranu

Od strony interfejsu programistycznego, dodawanie nowych okien wygląda w ten sposób (na przykładzie menu początkowego z theGame::buildMenu() w "game.cpp"):

  screens[ SCREEN_MENU ].addWindow( TITLE_POSX, TITLE_POSY, TITLE_WIDTH, TITLE_HEIGHT );
  screens[ SCREEN_MENU ].addWindow( MENU_POSX, MENU_POSY, MENU_WIDTH, MENU_HEIGHT );

  theWindow* menuW = screens[ SCREEN_MENU ].windows[ 0 ];
  theWindow* menuW1 = screens[ SCREEN_MENU ].windows[ 1 ];
  theWindow* menuW2 = screens[ SCREEN_MENU ].windows[ 2 ];


  
  menuW->addPlaneMatrix( res->bGround[ BGR_MENU ]->w, res->bGround[ BGR_MENU ]->h );
  menuW->makeBackgroundMatrix( 0, res->bGround[ BGR_MENU ] );
  menuW1->addPlaneMatrix();

  menuW2->addPlaneMatrix( res->bGround[ BGR_MENU_LIST ]->w, res->bGround[ BGR_MENU_LIST ]->h );
  menuW2->makeBackgroundMatrix( 0, res->bGround[ BGR_MENU_LIST ] );

  _SURFACE* mTitle = _CREATE_RGBA_SURFACE( TITLE_WIDTH, TITLE_HEIGHT, MENU_BG_COLOR );
  _SURFACE* txtChoose = _CREATE_RGBA_SURFACE( res->font.tiles[ 0 ]->w * strlen( MENU_CHOOSE_LEVEL_TXT ), res->font.tiles[ 0 ]->h, 0 );
  _SURFACE* txtExit = _CREATE_RGBA_SURFACE( res->font.tiles[ 0 ]->w * strlen( MENU_EXIT_TXT ), res->font.tiles[ 0 ]->h, 0 );
 
  menuW1->anchoredText = true;
  menuW1->print( &res->font, mTitle, TITLE_TEXT_POSX, TITLE_TEXT_POSY, TITLE_TEXT );
  menuW2->anchoredText = false;
  menuW2->print( &res->font, txtChoose, 0, 0, MENU_CHOOSE_LEVEL_TXT );
  menuW2->print( &res->font, txtExit, 0, 0, MENU_EXIT_TXT );

  menuW1->putOnMatrix( 0, mTitle, 0, 0 );
  menuW2->addPlaneMatrix();
  menuW2->putOnMatrix( 1, txtChoose, MENU_TXT_POSX, MENU_TXT_POSY );
  menuW2->putOnMatrix( 1, txtExit, MENU_EXIT_POSX, MENU_EXIT_POSY );

  menuW1->makeBorder( BORDER_MENU_WIDTH, BORDER_MENU_COLOR );
  menuW2->makeBorder( BORDER_MENU_WIDTH, BORDER_MENU_COLOR );

Jest też drugi sposób, z użyciem obiektów wizualnych (bez tekstu dodanego do macierzy sektorów):
//zamiast:
  menuW1->putOnMatrix( 0, mTitle, 0, 0 );
  menuW2->addPlaneMatrix();
  menuW2->putOnMatrix( 1, txtChoose, MENU_TXT_POSX, MENU_TXT_POSY );
  menuW2->putOnMatrix( 1, txtExit, MENU_EXIT_POSX, MENU_EXIT_POSY );

//można też:
  menuW1->newVObj( 0, 0, mTitle, 0, 0 );
  menuW2->newVObj( 0, 0, txtChoose, MENU_TXT_POSX, MENU_TXT_POSY );
  menuW2->newVObj( 0, 1, txtExit, MENU_EXIT_POSX, MENU_EXIT_POSY );

Ekran z identyfikatorem SCREEN_MENU zawiera 3 okna. Pierwsze okno to tło całego ekranu. Drugie okno zawiera napis tytułu gry "METOD". Trzecie okno zawiera menu właściwe (obecnie napisy "Choose level:" i "Exit"). Napis tytułu jest lekko przeźroczysty (identycznie jak tło tego okna), ponieważ jest osadzony w powierzchnię tła tego okna, które ma częściowo przeźroczysty kolor.

Funkcja składowa theWindow::makeBorder( _SIZE bw, _COLOR bc ) dodaje nowy plan z obramowaniem okna, bez żadnego wypełnienia. Pierwszy argument dla funkcji theWindow::putOnMatrix i theWindow::newVObj to indeks macierzy do której ma być dodana określona powierzchnia. Drugi argument dla funkcji theWindow::newVObj to indeks kolejności renderowania powierzchni obiektu wizualnego.

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.