NIE MODYFIKOWAĆ TEGO PLIKU!!! Niniejszy plik stanowi instrukcję wyjaśniającą to, w jaki sposób połączyć zawartość pliku GalagaSilnik.cs z resztą projektu.

Aby używać silnika gry, należy raz utworzyć obiekt klasy Silnik. Można to zrobić na przykład na początku funkcji głównej programu (i przekazywać go do różnych metod, jeśli zachodzi taka potrzeba), albo na początku jakiejś funkcji skojarzonej z oknem głównym programu - takim, którego zamknięcie jest równoznaczne z zakończeniem działania programu.

Loz.Silnik silnik = new Loz.Silnik();

Raz utworzony silnik może być już wielokrotnie wykorzystywany do rozpoczęcia nowej gry.

W tym pliku instruktażowym zakładam, że zostaną utworzone tablice obiektów/tekstur elementów ruchomych gry. W oparciu o to założenie pokażę w pewnym miejscu dalszej części tego pliku instruktażowego przykładowy sposób komunikacji z silnikiem w celu wyświetlania tych tekstur we właściwym położeniu. Oczywiście można to zrealizować zupełnie inaczej, wykorzystując te same zapytania silnika. Analogiczne tablice reprezentujące te elementy pod względem logicznym są zawarte w silniku. Długość obydwu tablic danego typu tekstur ma docelowo być taka sama (silnik odczyta ją przy wywołaniu metody resetuj, ale o tym niżej) a jedynym elementem łączącym ze sobą elementy tych dwóch tablic (jednej tablicy w silniku reprezentującej obiekty logicznie a drugiej tablicy innej klasy reprezentującej tekstury) jest identyczna wartość indeksu.

Przykładowo mogłoby to wyglądać tak (nazwy klas są zmyślone, programista-grafik stworzy zapewne klasy o innych nazwach niż te):

TeksturaPrzeciwnika[] teksturyPrzeciwników = new TeksturaPrzeciwnika[64];

for (int i=0; i < teksturyPrzeciwników.Length; i++)
{
	teksturyPrzeciwników[i] = new TeksturaPrzeciwnika();
}
// można wybrać liczbę inną niż 64 i odpowiednio wpisać ją przy resetowaniu silnika (ustawianiu go do nowej pracy, czyli rozpoczęcia nowej gry)
        		
    		

TeksturaPocisku[] teksturyPociskówGracza = new TeksturaPocisku[2];

for (int i=0; i < teksturyPociskówGracza.Length; i++)
{
	teksturyPociskówGracza[i] = new TeksturaPocisku();
}
// maksymalną liczbę dostępnych pocisków gracza też możnaby zmienić, tak samo jak maks. liczbę przeciwników na ekranie
        		
    		

TeksturaPocisku[] teksturyPociskówWroga = new TeksturaPocisku[10]; // można też użyć innej klasy (czyli innej grafiki) dla pocisków wrogów niż dla pocisków gracza, ale wymiary grafik obydwu rodzajów pocisków muszą być równe, bo wymiary hitboxów obydwu rodzajów pocisków są w silniku zawsze równe sobie nawzajem

for (int i=0; i < teksturyPociskówWroga.Length; i++)
{
	teksturyPociskówWroga[i] = new TeksturaPocisku();
}
// maksymalną liczbę dostępnych pocisków wroga też możnaby zmienić, tak samo jak maks. liczbę przeciwników na ekranie
    		
       

TeksturaStatkuGracza teksturaStatku = new TeksturaStatkuGracza(); // zawsze jest tylko jeden statek gracza

Do powyższych przykładowych nazw (utworzonych na potrzeby przykładu) będę się dalej odwoływał w zapowiedzianym przykładzie. Masła.Masło masłoMaślane = new Masła.Masło();

W przypadku zmiany rozmiaru okna, można: silnik.PrzeskalujRozmiary(800,600); // nic by nie zmieniło, bo dałem tu te same wymiary, co poniżej. Można dać inne. Dzięki temu silnik będzie zwracał położenia obiektów (w pikselach) dla nowej wielkości okna. Powyższa linia powinna być wywoływana, jeśli okno gry zmieniło rozmiar - zdarzenie Windows Forms. To, jak odwoływać się z metod obsługujących zdarzenia do silnika, jest wyjaśnione w dalszej części tej instrukcji.

Rozpoczęcie i przebieg nowej gry (aż do jej zakończenia) powinno być wywołane np. w odpowiedniej pętli lub w metodzie obsługującej zdarzenie (np. w sytuacji powrotu do manu i ponownego rozpoczęcia gry), poprzez użycie następującego ciągu instrukcji:

silnik.Resetuj( 800 , 600 , 11 , 0.08 , 0.1 , 0.004 , 64 , 0.08 , 0.1 , 0.008 , 2 , 0.01 , 0.08 , 0.032 , 3 , 100 , 40 , 5000 , 10 , 300 , 0.008 ); /*

Można dobrać inne parametry dla zwiększenia grywalności - wartość każdego parametru można sobie dowolnie wybrać. Warto poeksperymentować, gdy gra ogólnie będzie już działać. Znaczenie kolejnych parametrów:

  1. - długość przestrzenii gry w poziomie (w pikselach, typu int), warto to odczytać z rozmiarów okna, bo ktoś mógł je zmienić przed rozpoczęciem gry
  2. - długość przestrzenii gry w pionie (w pikselach, typu int), warto to odczytać z rozmiarów okna, bo ktoś mógł je zmienić przed rozpoczęciem gry
  3. - liczba milisekund trwania czekania w pojedynczym okresie silnika, wartości takie jak 16, 15, 14... powinny dawać około 60 kroków na sekundę, przy czym warto pamiętać, że kroków może być np. 200 na sekundę i nie będzie to sprawiało żadnych problemów
  4. - rozmiar hitbox'a statku gracza w poziomie jako ułamek rozmiaru całej przestrzenii w poziomie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY BYŁY RÓWNE ODPOWIEDNIM ILORAZOM, na przykład tutaj powinna być to szerokość tekstury statku gracza w pikselach podzielona przez długość przestrzenii gry w poziomie w pikselach (rzutowane na double)
  5. - rozmiar hitbox'a statku gracza w pionie jako ułamek rozmiaru całej przestrzenii w pionie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY...
  6. - długość kroku przestrzennego gracza jako ułamek rozmiaru całej przestrzenii w poziomie (nie w pionie!, wartość double 0 < x <= 1) polecana wartość to jedna dwudziesta długosci poziomej hitboxa statku gracza (1/20 z tego ułamka, będącego ilorazem)
  7. - maksymalna liczba przeciwników (jednocześnie) na ekranie (musi się pokrywać z liczbą dostępnych (identycznych?) obiektów tekstur przeciwników, dowolnie wybrana wartość typu int)
  8. - rozmiar hitbox'a przeciwnika w poziomie jako ułamek rozmiaru całej przestrzenii w poziomie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY...
  9. - rozmiar hitbox'a przeciwnika w pionie jako ułamek rozmiaru całej przestrzenii w pionie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY...
  10. - długość kroku przestrzennego przeciwnika jako ułamek rozmiaru całej przestrzenii w poziomie (nie w pionie!, wartość double 0 < x <= 1) polecana wartość to jedna dziesiąta długości poziomej hitboxa przeciwnika (1/10 z tego ułamka, będącego ilorazem)
  11. - maksymalna liczba pocisków gracza jednocześnie na ekranie (musi się pokrywać z liczbą dostępnych identycznych obiektów tekstur pocisków gracza, typu int)
  12. - rozmiar hitbox'a pocisku w poziomie jako ułamek rozmiaru całej przestrzenii w poziomie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY...
  13. - rozmiar hitbox'a pocisku w pionie jako ułamek rozmiaru całej przestrzenii w pionie (wartość double 0 < x <= 1) UWAGA!!! TEGO TYPU WARTOŚCI POWINNY BYĆ DOBRANE TAK, ABY...
  14. - długość kroku przestrzennego pocisku gracza jako ułamek rozmiaru całej przestrzenii w pionie (nie w poziomie!, wartość double 0 < x <=

1) polecana wartość to 4 razy wyższa niż długość kroku przestrzennego przeciwnika (pociski 4x razy szybsze niż przeciwnicy) - początkowa liczba żyć (nie da się ich zyskać, można jedynie stracić) - liczba punktów za jednego przeciwnika, np. +100 - liczba okresów (kroków) silnika pomiędzy wejściem na scenę dwóch poszczególnych przeciwników - liczba okresów (kroków) silnika pomiędzy nowymi wejściami oddziałów przeciwników na scenę - maksymalna liczba pocisków wroga jednocześnie na ekranie (musi się pokrywać z liczbą dostępnych identycznych tekstur pocisków wroga, typu int) - rzadkość strzelania (typu int) - im większa, tym rzadziej wrogowie strzelają; ustawienie wartości około w = 1000/liczba_milisekund (parametr drugi) powoduje, że każdy wrogi statek spróbuje oddać strzał średnio raz na sekundę (w losowym momencie). Zalecana wartość to trzykrotnie więcej, czyli strzał średnio co 3 sekundy. - długość kroku przestrzennego pocisku wroga jako ułamek rozmiaru całej przestrzenii w pionie (nie w poziomie!, wartość double 0 < x <= 1) polecana wartość to 2 razy wyższa niż długość kroku przestrzennego gracza (pociski 2x razy szybsze niż gracz)

*/
do // ta pętla powinna wystąpić od razu po zresetowaniu silnika, a zresetowanie silnika musi poprzedzać każde nowe rozpoczęcie gry
{
	silnik.WykonajKrok(); // ta linia jest na swoim właściwym miejscu
	
	// Jest i obiecany przykład - domyślam się, że nazwy klas tekstur, obiektów tekstur i ich metod będą znacznie inne od
	// przedstawionych przeze mnie poniżej (wymyślonych na potrzeby przykładu). Nazwy metod klasy Silnik wywoływane na obiekcie silnik są jednak autentyczne.
	// Oczywiście możnaby je wykorzystać w inny sposób, w zależności od potrzeb.
	
	/* Tu pokazać/przesunąć/schować elementy ruchome - przypadnie to zatem raz na każdy krok silnika: */
	teksturaStatku.UstawX(silnik.ZwróćXStatku()); // zwraca liczbę całkowitą w pikselach, lewy górny róg, te wszystkie metody silnika już same uwzględniają to, że oś Y przy wyświetlaniu jest skierowana w dół
	teksturaStatku.UstawY(silnik.ZwróćYStatku()); // zwraca liczbę całkowitą w pikselach, lewy górny róg, to ma sens, bo może nastąpić przeskalowanie wielkości okna
	for (int i=0; i < teksturyPrzeciwników.Length; i++)
	{
		if (silnik.CzyPrzeciwnikIstnieje(i))
		{
			teksturyPrzeciwników[i].UstawX(silnik.ZwróćXPrzeciwnika(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			teksturyPrzeciwników[i].UstawY(silnik.ZwróćYPrzeciwnika(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			if (teksturyPrzeciwników[i].JestUkryty())
				teksturyPrzeciwników[i].Pokaż();// gdyby np. każdy nowy przeciwnik miał wchodzić na ekran z jakąś animacją, to właśnie tu należałoby ją wstawić
		}
		else
		{
			if (!teksturyPrzeciwników[i].JestUkryty())
				teksturyPrzeciwników[i].Ukryj(); // można tu jeszcze dodać jakąś animację wybuchu, czy coś takiego
		}
	}
	for (int i=0; i < teksturyPociskówGracza.Length; i++)
	{
		if (silnik.CzyPociskGraczaIstnieje(i)) // może być chwilowo ukryty i czekać na wystrzał
		{
			teksturyPociskówGracza[i].UstawX(silnik.ZwróćXPociskuGracza(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			teksturyPociskówGracza[i].UstawY(silnik.ZwróćYPociskuGracza(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			if (teksturyPociskówGracza[i].JestUkryty())
				teksturyPociskówGracza[i].Pokaż(); // tu można do wystrzału wykonanego przez gracza dorzucić jakąś animację
		}
		else
		{
			if (!teksturyPociskówGracza[i].JestUkryty())
				teksturyPociskówGracza[i].Ukryj();
		}
	}
	for (int i=0; i < teksturyPociskówWroga.Length; i++)
	{
		if (silnik.CzyPociskWrogaIstnieje(i)) // może być chwilowo ukryty i czekać na wystrzał
		{
			teksturyPociskówWroga[i].UstawX(silnik.ZwróćXPociskuWroga(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			teksturyPociskówWroga[i].UstawY(silnik.ZwróćYPociskuWroga(i)); // zwraca liczbę całkowitą w pikselach, lewy górny róg
			if (teksturyPociskówWroga[i].JestUkryty())
				teksturyPociskówWroga[i].Pokaż();// tu można do wystrzału wykonanego przez wrogi statek dorzucić jakąś animację
		}
		else
		{
			if (!teksturyPociskówWroga[i].JestUkryty())
				teksturyPociskówWroga[i].Ukryj();
		}
	}
	// tu zaktualizować wyświetlaną liczbę punktów, odczytując ją z silnik.LiczbaPunktów // własność typu int
	// tu zaktualizować wyświetlaną liczbę żyć, odczytując ją z silnik.LiczbaŻyć // własność typu int, liczba żyć nigdy nie rośnie, może jedynie spadać
} while (!silnik.GraSkończona); // własność typu bool

// tu zmienić liczbę żyć na 0, wyświetlić komunikat o końcu gry, można ostatecznie wyświetlić liczbę punktów (jakoś elegancko) (jakiś ekran GAME OVER)
		

Obsługa klawiatury leży po stronie zdarzeń Windows Forms, czyli po stronie graficznej projektu. Na obiekcie klasy silnik można wywoływać: silnik.NaciśniętoKlawiszSpace(); // powyższa odpowiada za strzał z działka silnik.UstawKlawiszStrzałkiWLewo(); silnik.UstawKlawiszStrzałkiWPrawo(); silnik.UstawKlawiszStrzałkiŻaden(); Powyższe 3 metody ustawiają kierunek ruchu statku gracza (lub fakt pozornego stania w miejscu) na stałe - utrzymuje się on przez kolejne kroki silnika tak długo, dopóki inna z tych trzech funkcji tego nie zmieni. Silnik oczywiście sam pilnuje, żeby gracz nie wyjechał poza ekran - wtedy stan się sam zmienia na Żaden, a pozycja gracza jest lekko resetowana.

Problemem jest to, w jaki sposób z metody obsługującej zdarzenie naciśnięcia klawisza odwołać się do silnika. Proponuję rozwiązać to następująco (można wymyślić inne rozwiązanie): W przestrzenii nazw Galaga należy utworzyć klasę statyczną:

static class PomocnikKlawiatury
{
	public static Loz.Silnik MójSilnik;
	static PomocnikKlawiatury()
	{
		MójSilnik = null;
	}
	public static void UstawPrzycisk(int który)
	{
		switch (który)
		{
			case 1:
				MójSilnik.NaciśniętoKlawiszSpace(); // tu zwalnianie przycisku nie ma żadnego znaczenia, jedynie jego naciśnięcie (aktywacja działa)
				break;
			case 2:
				MójSilnik.UstawKlawiszStrzałkiWLewo();
				break;
			case 3:
				MójSilnik.UstawKlawiszStrzałkiWPrawo();
				break;
			case 4:
				MójSilnik.UstawKlawiszStrzałkiŻaden();
				break;
		}
	}
}
		

Wraz z utworzeniem silnika należy wtedy (raz, na początku) wykonać: // to już tu było: Loz.Silnik silnik = new Loz.Silnik(); // a linijkę niżej wystarczy dodać: PomocnikKlawiatury.MójSilnik = silnik;

Pozostaje już tylko w funkcjach zdarzeń naciśnięcia klawiszy na klawiaturze (strzałki obsługuje się niestety inaczej niż większość przycisków) odpowiednio wywoływać powyższą metodę statyczną, jak w poniższym przykładzie: Galaga.PomocnikKlawiatury.UstawPrzycisk(2); // w sumie możnaby od razu: Galaga.PomocnikKlawiatury.MójSilnik.UstawKlawiszStrzałkiWLewo(); // i wtedy nie trzeba pisać dodatkowych metod statycznych w klasie PomocnikKlawiatury

Należy szczególnie uważać na to, że zdarzenie zwolnienia przycisku powinno rozróżniać, który przycisk klawiatury został zwolniony. To, który (z trzech: lewo; prawo; spacja też, ale to nie ważne) jest chwilowo przytrzymany, a który nie, można też ZAPISYWAĆ - aby dało się go zwolnic - na przykład właśnie W POWYŻSZEJ KLASIE PomocnikKlawiatury, rozszerzając ją o odpowiednie pola (publiczne statyczne). Przykład: Jeżeli trzymana była strzałka w lewo i została zwolniona, to powinna zostać wywołana metoda: MójSilnik.UstawKlawiszStrzałkiŻaden();

I to już wszystko. O resztę zadba magia silnika.