Jak zrobić pochodnię lub efekt ognia w grze 2D?
Powiedzmy, że chcielibyśmy zrobić coś takiego:
Jak się zastanowić to okazuje się, że nie jest to takie trudne. Poniżej podam sposób rozwiązania, oczywiście jako zwolennik DELPHI będzie to kod w tym kompilatorze (wersja Delphi 5 plus Omega). A niech w końcu i zwolennicy CPP sobie coś tłumaczą 🙂
Co będzie nam potrzebne?
a) obraz tła, które przyciemnimy w kodzie programu aby udawało noc
b) obraz ognia i drzewca pochodni
c) obraz promienia światła- może to trochę dziwić, ale da się taki pomysł wykorzystać
Bardzo ważne, aby tło promienia światła i tło ognia było czarne. Pytanie dlaczego? A to tylko dla tego, że wykorzystamy operacje AND nakładania bitów tła świata, obrazu promienia światła i obrazu płomieni (poniżej odpowiednia instrukcja).
1 |
Image.Draw(Round(x),Round(y),0,0.5,0.5,1,1,OmegaColor(200,200,100,255),Round(AnimPos),bmSrcAdd); |
Ale po kolei…
1. Tworzymy klasę pochodni. Przyjmiemy, że pochodnia to drzewce na którym pali się ogień, a ogień rzuca promienie światła. Jak by nie było tak to wygląda w rzeczywistości. Jako, że pochodnię można przenosić to również zasymulujemy jej ruch wzdłuż osi X. Klasa Pochodni może wyglądać jak poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//*****POCHODNIA******************* TPochodnia=class(TSprite) private //ponizsze dane mozna odczytac w dowolnym edytorze grafiki np. MSPaint czubek:array[0..1]of TPoint;// będą współrzędne ognia swiatlo, plomien:TPlomien; protected dVx, dVy:single;//zmienne przyrostu polożenia constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem; const ImageListItemPlomien: TImageListItem; const ImageListItemSwiatlo: TImageListItem );virtual; procedure Move(const MoveCount: Single); override; destructor Destroy; end; //********************************* |
Proszę zauważyć, że do ciała klasy TPochodnia podpiąłem dwie zmienne żyjące na innej klasie:
swiatlo i plomien:TPlomien;
Snop światło i płomień zachowują się w pewnym sensie tak samo – snop światła zależny jest od płomienia…Więc dlaczego by nie zrobić jednej klasy na oba obiekty? Klasa ta ma postać
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
TPlomien=class(TSimpleAnimSprite) private procedure Draw;override; protected constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem );virtual; end; |
Efekt półprzezroczystego płomienia jak i efekt strumienia światła uzyskamy, jeśli podmienimy standardową funkcję rysującą Draw klasy bazowej TSimpleAnimSprite. Napiszemy ja jak poniżej
1 2 3 4 5 6 7 8 9 10 11 |
procedure TPlomien.Draw; begin // inherited Draw;//<-- jak to sie wlaczy to mozna zobaczyć roznicę - z czego się zrezygnowalo //swiatlo Image.Draw(Round(x),Round(y),0,0.5,0.5,1,1,OmegaColor(200,200,100,255),Round(AnimPos),bmSrcAdd); end; |
Funkcja nie jest trudna do zrozumienia, jedynie może zastanowić fakt, ze użyłem Round(AnimPos) dla strumienia światła. Przecież nie ma tu animacji klatek. Zrobiłem to tylko dla tego, że oba obiekty są do siebie podobne. Dla płomienia klatki się zmieniają a dla strumienia nie, AnimPos wynosi 0. Oczywiście można zrobić strumień na klasie bazowej TSprite – i to by było najwłaściwsze rozwiązanie. W tej chwili można powiedzieć, ze uzyskaliśmy fajny efekt ognia i światła jaki on daje…i to przy pomocy jednej linijki kodu. DELPHI może rzucić na kolana wiele razy mi to robi 🙂
Co dalej?
Należy teraz zająć się drzewcem pochodni. Utworzymy je przy pomocy tego konstruktora
1 2 3 4 5 6 7 8 9 |
constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem;//przechowa obraz klatek drzewca const ImageListItemPlomien: TImageListItem;//przechowa obraz klatek plomieni const ImageListItemSwiatlo: TimageListItem//przechowa obraz swiatla );virtual; |
Postać ciała konstruktora wygląda jak poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
constructor TPochodnia.Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem; const ImageListItemPlomien: TImageListItem; const ImageListItemSwiatlo: TImageListItem); begin //rob drzewce pochodni inherited Create(AParent); Image := ImageListItem; ImageIndex:=0; width:=Image.TileWidth; height:=Image.TileHeight; x:=200; y:=250; z:=1; //pzryjmij wartosc przesuniecia ruchu w osi X dVx:=40; //gdy nachylony w prawo czubek[0].x:=22; czubek[0].y:=12; //gdy nachylony w lewo czubek[1].x :=12; czubek[1].y :=10; //rob swiatlo Swiatlo:=TPlomien.Create(AParent,ImageListItemSwiatlo); Swiatlo.z:=z+1; //rob plomien pochodni Plomien:=TPlomien.Create(AParent,ImageListItemPlomien); Plomien.z:=z+1; end; |
Konstruktor pochodni wywołamy jak poniżej (pamiętając, aby w sekcji var projektu formatki zdefiniować zmienną: Pochodnia:TPochodnia;).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm1.OmegaScreen1Init(Sender: TObject); begin //Tworz pochodnie, // W liscie obrazow pod indeksem 1 jest drzewce pochodni, pod indeksem 2 jest plomień, // pod indeksem 3 jest obraz swiatla Pochodnia:=TPochodnia.Create(OmegaSprite1,OmegaImageList1.ImageList.Items[1],OmegaImageList1.ImageList.Items[2],OmegaImageList1.ImageList.Items[3]); OmegaTimer1.Enabled:=true; end; |
Jak widać za jednym pociągnięciem utworzyły się trzy obiekty: drzewce, ogień i światło. Na tej zasadzie można tak utworzyć kilka pochodni stosując wywołanie tej procedury w pętli.
Musze jeszcze wspomnieć o tym, aby zadbać aby te trzy obiekty się nie rozjeżdżały na przykład bohater naszej gry wziął do ręki pochodnię z nią wędruje. Pilnujemy tego ruchu w tej procedurze:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
procedure TPochodnia.Move(const MoveCount: Single); begin inherited Move(MoveCount); //uaktualnij polozenie drzewca X:= X+MoveCount*dVx; if x<0 then begin x:=0; //wyrownaj do lewej krawedzi ekranu dVx:=40; //zrob na przesuwanie w prawo ImageIndex:=0;//zmien klatke drzewca- nachylona w prawo end; if x>800 then begin x:=800; //wyrownaj do prawej krawedzi ekranu dVx:=-40;//zrob na przesuwanie w prawo ImageIndex:=1;//zmien klatke drzewca- nachylona w lewo end; //uaktualnij polozenie plomienia Plomien.x:=x+czubek[ImageIndex].x-Plomien.width div 2; Plomien.y:=y+czubek[ImageIndex].y-Plomien.Height; //a teraz swiatla Swiatlo.x:=Plomien.x-Swiatlo.width div 2; Swiatlo.y:=Plomien.y; end; |
W tym fragmencie kodu wyjaśni się pewien chwyt, jaki zastosowałem. Otóż mogło budzić zastanowienie dlaczego wprowadziłem taką zmienną: czubek:array[0..1]of TPoint; I to w dodatku jako tablica. Sprawa jest prosta – zmienna ta przechowuje współrzędne zaczepienia płomienia w zależności od nachylenia drzewca. Dodatkowo przełączanie indeksu w tej tablicy nie załatwiam jakimś tam specjalnym indeksem, ale wykorzystuje indeks zmiany klatki obrazu. Co widać poniżej:
1 2 3 |
Plomien.x:=x+czubek[ImageIndex].x-Plomien.width div 2; Plomien.y:=y+czubek[ImageIndex].y-Plomien.Height; |
Efekt nocy uzyskamy w najprostszy sposób
1 2 3 |
//udawaj noc OmegaImageList1.ImageList.Items[0].Draw(75,200,0,0,0,1,1,OmegaColor(50,75,70,255),0); |
W rzeczywistej planszy świata gry należy obsłużyć taką zmianę koloru dla każdego (lub wybranych) pola ewentualni całego ekranu. Zmianę kolorów dla wybranych pól świata można zobaczyć w tworzonym edytorze światów 2D, który można pobrać z www.delphi.ilion.pl pod strona narzędzia.
Pozostałych procedur nie będę tłumaczyć, są one w miarę proste wystarczy przeglądnąć kod programu. Mam nadzieję, że tym artykułem przybliżyłem zalety komponentu OMEGI no i oczywiście kompilatora DELPHI
Pozdrawiam oksal
Autor: oksal