Robimy grę platformową w Ruby Gosu!

Tematy różne, różniste.
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

Lekcja 2: Wprowadzenie do Scene. Gracz.
Jako, że chwilowo mam czasu więcej, postanowiłem szybko zabrać się za drugą część. Dzisiaj zrobimy podstawową SceneMap oraz naszego małego gracza ;) zaczynamy!

Otwórzcie edytor i oba nasze pliki. Stwórzcie kolejny plik pod nazwą "SceneMap.rb", który umieście w "/scripts", i importujcie go w naszym Run.rb

Kod: Zaznacz cały

require 'scripts/SceneMap.rb'
Dajcie naszemu SceneMap podstawowy układ metod, identyczny jak w naszym okienku:

Kod: Zaznacz cały

class SceneMap

	def initialize

	end

	def update

	end

	def draw

	end

	def button_down(id)

	end

	def button_up(id)

	end

end
W poszczególnych Scene będziemy mieli fizykę dotyczącą danego elementu gry, czyli np. w SceneTitle będziemy mieli wszystko co jest związane z ekranem tytułowym, a w SceneMap wszystko, co z mapą. Jednak żeby Scene działały, musimy je wywołać. Będziemy musieli je również update'ować czy rysować, innymi słowy: wszysto będziemy musieli odwołać do Scene. Dlatego też przełączcie na GameWindow.

W metodzie initialize, zaraz pod 'self.caption' umieśćcie ten oto kod:

Kod: Zaznacz cały

$scene = SceneMap.new
Jak już zapewne wiecie, to stworzy nową zmienną globalną '$scene', i da jej wartość SceneMap. Ale pozostaje jeszcze sprawa pozostałych metod. W każdej z tych metod umieście kod

Kod: Zaznacz cały

$scene.[nazwa_metody]
gdzie [nazwa_metody] jest dokładnie tym, co jest napisane. Tak więc w 'def update' dajcie '$scene.update', a w 'def button_down(id)': '$scene.button_down(id)'. Powinniście skończyć z czymś takim:

Kod: Zaznacz cały

# Main game window

class GameWindow < Window

	def initialize
		super(640,480,false)
		self.caption = "Fuzed" 
		$scene = SceneMap.new
	end

	def update
		$scene.update
	end

	def draw
		$scene.draw
	end

	def button_down(id)
		$scene.button_down(id)
	end

	def button_up(id)
		$scene.button_up(id)
	end

end
Krótkie wyjaśnienie co to będzie robiło: jako, że dane metody są automatycznie wywoływane tylko w oknie, bez wywoływania ich w innych klasach poprzez okno one nie będą działały. Tak więc teraz sprawiliśmy, że za każdym razem kiedy system wywoła Window#update, nasz obecny $scene (na ten moment - Map) również będzie odświeżany.
Jeśli teraz uruchomicie grę, nie powinno się nic zmienić (tj. powinniśmy mieć cały czas czarne okienko gry). Powód tego jest taki, że jeszcze nic nie wyświetliliśmy. Zróbmy więc próbę, czy wszystko działa, i używając wewnętrzną metodę Gosu::Window#draw_line narysujemy białą linię!
Przełączmy na nasze SceneMap i w metodzie draw wstawmy ten kod:

Kod: Zaznacz cały

$window.draw_line(0,0,Color.new(255,255,255),640,480,Color.new(255,255,255),0)
Szybkie wytłumaczenie co tu się stało. Otóż jak widzicie, odwołujemy się do $window.draw_line. Ale przecież $window to nasz GameWindow, a w nim nie ma takiej metody? Dokładnie, ale jest ona w Gosu::Window, i zachodzi tutaj rzecz taka sama jak z naszym 'self.caption'.
Metoda Gosu::Window#draw_line pobiera 7 argumentów: x1, y2, color1, x2, y2, color2, z. x i y to są współrzędne, a color potrzebuje wykorzystania klasy Color. z jest pozycją na ekranie, na której "warstwie" jest wyświetlane (rzeczy o wyższym z są wyświetlane ponad tymi, które mają mniejszą tę wartość). Z Wartościami jakie podaliśmy powinna nam się wyświetlić biała linia idąca po przekątnej przez nasze okienko. I rzeczywiście, jeśli wszystko zrobiliście okej, powinniście ujrzeć to:
Obrazek

Wiemy już, że nasz Scene jest wyświetlany prawidłowo, wykasujcie więc kod odpowiadający za rysowanie linii. Nie jest nam już potrzebny.

Znów stwórzcie nowy plik i nazwijcie go 'Player.rb'. Również musicie go importować ;) Nasza klasa Player potrzebuje tylko trzech podstawowych metod: initialize, update i draw. Jednak jest mała różnica w porównaniu do poprzednich skryptów: tym razem zarówno initialize jak i draw potrzebują parametrów. Dla 'initialize' będą to parametry x i y, bez żadnej wartości, a dla 'draw' będzie to parametr 'z' przyjmujący wartość 5.

Kod: Zaznacz cały

class Player

	def initialize(x,y)

	end

	def update

	end

	def draw(z=5)

	end

end
Widzicie różnicę między tym, jak przechwytujemy parametry w initialize a w draw? W "draw" automatycznie przyjmujemy wartość z=1, czyli jeśli odwołując się do tej metody nie podamy żadnego argumentu, nic się nie stanie. Za to tworząc objekt Player musimy podać jego położenie x i y. W 'initialize' będziemy musieli stworzyć kilka zmiennych lokalnych. Są to:
@x, @y, @sprite, @real_x, @real_y. Pierwsze dwie są bez problemu do zrozumienia. @sprite też nie powinien sprawić problemu, ale co z ostatnimi dwoma?
O tym za chwilę. Zacznijmy od przypisania pierwszym dwóm zmiennym wartości które przesyłamy poprzez 'initialize':

Kod: Zaznacz cały

	def initialize(x,y)
		@x = x
		@y = y
	end
Stwórzmy również grafikę naszej postaci. Zapiszcie poniższy obrazek do folderu '/graphics/sprites' (musicie najpierw utworzyć podfolder sprites ;) ). To będzie grafika naszej postaci. Możecie oczywiście użyć własnej grafiki!
Obrazek
I w naszym initialize wpiszcie:

Kod: Zaznacz cały

@sprite = Image.new($window, "graphics/sprites/player_1_stand_right.png", false)
@sprite jest nową instancją klasy (Gosu::)Image.new. Image przyjmuje 3 parametry: okno (nasz $window), ścieżka do grafiki oraz boolean (true/false) dla tileable. Jako, że nasza postać nie będzie układana w tile'ach, ustawiamy tę wartośc na false ;). Możemy od razu przeskoczyć do metody draw i "narysować" nasz sprite. Image#draw potrzebuje trzech wartości: x, y i z. Wszystkie trzy mamy, więc wystarczy wpisać:

Kod: Zaznacz cały

@sprite.draw(@x, @y, z)
I viola, gotowe. Może więc stwórzmy naszego gracza na mapie?
W SceneMap#initialize stwórzcie taką oto zmienną:

Kod: Zaznacz cały

@player = Player.new(128,128)
Wykorzystajcie również tę samą zmienną '@player' wywołując ją w update i draw. Nie musicie tam podawać parametrów - update tego nie potrzebuje, a draw sam przypisze wartość. Odpalcie grę.
Jeśli pojawi wam się taki błąd:
Obrazek
(który niestety czasem się pojawia, jak np. mi teraz) musicie do GameWindow, pod 'self.caption' a nad '$scene' wkleić:

Kod: Zaznacz cały

$window = self
To upewni się, że zmienna $window jest rzeczywiście GameWindow.

Jeśli błąd nie wyskoczył (albo poprawiliście go w ten sposób) powinniście zobaczyć obrazek naszego gracza unoszący się blisko lewego górnego rogu.

Teraz zajmijmy się ostatnimi dwiema zmiennymi, których nasz gracz potrzebuje. Jak widzicie, mamy zarówno @x, @y, jak i @real_x i @real_y. Powodem tego jest to, że postanowiłem rozbić postać na dwa rodzaje współrzędnych, @x i @y będą wskazywały nam środkowy dół postaci ("środek ciężkości"), a @real_x/y będą punktem {0,0} grafiki. Tak więc musimy wprowadzić nieco zmian w naszym initialize:

Kod: Zaznacz cały

	def initialize(x,y)
		@real_x = x
		@real_y = y
		@sprite = Image.new($window, "graphics/sprites/player_1_stand_right.png", false)
		@x = @real_x + (@sprite.width / 2)
		@y = @real_y + @sprite.height
	end
Zmiany jakie zaszły: x i y które przesyłamy jest przypisane do @real_x/y. Następnie @x i @y jest obliczane na podstawie położenia i rozmiaru obrazka. Logika postaci będzie się opierała na @x i @y, więc będziemy musieli odświeżać wartości @real_x/y. Dlatego w update musicie wpisać taki oto kod:

Kod: Zaznacz cały

@real_x = @x - (@spirte.width / 2)
		@real_y = @y - @sprite.height
Jak widać, jest on odwrotnością tego, co w initialize. Zmieńcie jeszcze @x i @y w draw na @real_x i @real_y. Jak teraz odpalicie grę zobaczycie, że się postać nieco przesunęła.
Obrazek
Ale statyczna postać jest nieco nudna. Czas pozwolić jej się poruszać!

Do tego potrzebujemy dwóch zmiennych w initialize:

Kod: Zaznacz cały

@move_x = 0
@moving = false
Na jakiej zasadzie będziemy się poruszać? Otóż za każdym razem gdy będziemy wywoływać ruch postaci, wartość zmiennej @move_x się zmieni, oraz @moving zmienimy na true. Metoda update wtedy będzie odpowiednio reagowała. Ale najpierw musimy tę reakcję ustalić! W metodzie update musimy umieścić ten kod:

Kod: Zaznacz cały

		if @moving then
			if @move_x > 0 then
				@move_x -= 1
				@x += 1
			elsif @move_x < 0 then
				@move_x += 1
				@x -= 1
			elsif @move_x == 0 then
				@moving = false
			end
		end
Po kolei: warunek jeśli @moving (...jest true) wykona kod sprawdzający wartość @move_x. Jeśli większa od zera, odejmuje 1 i dodaje 1 do naszego @x. Jeśli mniejsza od zera, robi odwrotną rzecz. Jeśli równa zeru, postać przestaje się ruszać. Stwórzmy również dwie nowe metody, które będą nam zmieniały te wartości:

Kod: Zaznacz cały

	def move_left
		@move_x = -3
		@moving = true
	end

	def move_right
		@move_x = 3
		@moving = true
	end
Wróćmy do SceneMap. W update musimy mieć odwołanie do tych metod (w momencie, kiedy jest ono potrzebne). Czemu w update?
Ponieważ metoda button_down jest wywoływana tylko raz, przez co gracz musiałby bez przerwy klikać strzałkami w lewo i prawo, żeby postać się ruszała. Tak więc w update umieśćmy ten kod:

Kod: Zaznacz cały

@player.move_left if $window.button_down?(KbLeft)
		@player.move_right if $window.button_down?(KbRight)
Tutaj przy okazji widzicie nowy sposób tworzenia warunków. Jeśli coś potrzebujemy wywołać tylko gdy jeden konkretny warunek jest spełniony, możemy to umiećić w jednej linijce, zamiast w trzech:

Kod: Zaznacz cały

if $window.button_down?(KbRight) then
	@player.move_right
end
Gdy odpalicie grę zobaczycie, że nasz ludzik może się poruszać za pomocą strzałek! Ale wciąż jest dość statyczny. Wypadałoby go jakoś animować, czyż nie?
To za chwilkę. Najpierw jednak zrobimy, żeby się odwracał do kierunku w którym idziemy. Wracamy do klasy Player. Ściągnijcie kolejną graficzkę:
Obrazek
Zacznijmy od stworzenia dwóch zmiennych z Image (@stand_left i @stand_right, zawierających odpowiednie grafiki) oraz przypisania @sprite wartości @stand_right. Dajmy również zmienną @dir, odpowiadającą za kierunek postaci. W ten sposób:

Kod: Zaznacz cały

@stand_right = Image.new($window, "graphics/sprites/player_1_stand_right.png", false)
		@stand_left = Image.new($window, "graphics/sprites/player_1_stand_left.png", false)
		@sprite = @stand_right
		@dir = :right
Jak widzicie, pojawił się nowy rodzaj zmiennej: symbol (oznaczany dwukropkiem). Symbole są dobre w właśnie takich przypadkach, kiedy potrzebujemy definitywnych wartości. Symbol raz utworzony nie może być zmieniony (czyli nie możemy zmienić :right na :Right bez zajmowania dodatkowej pamięci), przez co zajmują mniej pamięci niż inne zmienne (oczywiście póki nie tworzymy ich wielu ;) ). Nasza zmienna @dir będzie miała tylko dwie możliwe wartości - :left i :right.
Musimy tylko dodać obracanie postaci i zmianę sprita. Najpierw w naszych metodach move_left i move_right przypisujemy odpowiednią wartość dla @dir. Następnie w update, ponad warunkiem 'if @moving then' zamieszczamy ten kod:

Kod: Zaznacz cały

		if @dir == :left then
			@sprite = @stand_left
		elsif @dir == :right then
			@sprite = @stand_right
		end
To jest chwilowe rozwiązanie, za chwilę nieco je zmienimy. Ale jak zobaczycie gdy odpalicie grę, postać się obraca! Teraz zajmijmy się animowaniem chodzenia. Przy okazji zrobimy drobną animację stania postaci. Ściągnijcie te cztery graficzki:
Obrazek
Obrazek
Obrazek
Obrazek
i umieśćcie wiecie gdzie ;). Musimy je dodać oraz dać naszemu programowi do zrozumienia, że to tak naprawdę nie jest jedna graficzka, a zbiór grafik w jednym pliku. Usuńcie wartości zmiennych @stand_right i @stand_left. Dodajcie również chwilowo puste zmienne @walk_left i @walk_right zaraz pod nimi. Żeby wczytało nam grafikę jako spritesheet, musimy użyć nieco innej metody: Image#load_tiles. Parametry jakie przyjmuje ta zmienna to: (Okno, ścieżka_dostępu, szerokość, wysokość, tileable?). Pierwsza dwa oraz ostatni znamy już, a szerokość i wysokość ustalają rozmiary jednej kratki. Można w nie przesłać wartości dwojako: w przypadku wartości dodatnich, program podzieli obrazek na kratki wielkości którą podamy (np. dając 32, 32 będziemy mieli x kratek o wielkości 32x32 piksele), a podając wartości ujemne, dzieli ten obrazek na tyle kratek (dając -4,-4 otrzymamy 16 kratek o rozmiarze zależnym od naszego spritesheeta). Pojedyncza kratka naszej postaci ma 32x32 piksele, więc podajemy te właśnie wartości. Wczytajcie wszystkie cztery grafiki:

Kod: Zaznacz cały

		@stand_right = Image.load_tiles($window, "graphics/sprites/player_1_standby_right.png", 32, 32, false)
		@stand_left = Image.load_tiles($window, "graphics/sprites/player_1_standby_left.png", 32, 32, false)
		@walk_left = Image.load_tiles($window, "graphics/sprites/player_1_run_left.png", 32, 32, false)
		@walk_right = Image.load_tiles($window, "graphics/sprites/player_1_run_right.png", 32, 32, false)
Kolejną rzeczą jest poprawienie metody draw. Otóż nasz @sprite jest w tej chwili tabelą. Próbowanie wyświetlenia jej wywoła błąd. Najprostrzą rzeczą jest zmienienie tego na:

Kod: Zaznacz cały

@sprite[0].draw(@real_x, @real_y, z)
Ale to będzie zawsze rysowało pierwszą klatkę, więc animacji jak nie było, tak nie ma ;). Dlatego zrobimy to nieco inaczej:

Kod: Zaznacz cały

frame = milliseconds / 100 % @sprite.size
		@sprite[frame].draw(@real_x, @real_y, z)
W ten sposób będzie się nam animowało. Jednak zanim odpalicie, żeby sprawdzić czy działa, musicie zarówno w initialize jak i update zmienić @sprite.width i @sprite.height na @sprite[0].width/height. Inaczej wystąpi błąd.
Uruchomcie grę. Nasza postać do was mruga ;) możecie zmieniać szybkość tego mrugania zmieniając liczbę 100 w obliczeniach frame. Im większa jest to wartość, tym wolniejsza animacja, ale pamiętajcie! Ta szybkość jest również do pozostałych animacji!

Dobrze, teraz czas na ostatnią animację: poruszanie się. Wbrew pozorom, nie będzie to trudne. Najpierw w metodach move_left i move_right musimy zmienić sprite postaci na odpowiedni:

Kod: Zaznacz cały

	def move_left
		@dir = :left
		@move_x = -3
		@sprite = @walk_left
		@moving = true
	end

	def move_right
		@dir = :right
		@move_x = 3
		@sprite = @walk_right
		@moving = true
	end
Następnie w update musimy zmienić kod tak, żeby zmieniało nam sprite na @stand_left/right tylko w przypadku, kiedy się nie poruszamy. Wytnijcie więc te pięć linijek zaczynających się od 'if @dir == :left then'. Na końcu warunku 'if @moving then' zmieńcie 'end' na 'else' i wklejcie ten kod który usunęliśmy, a potem dopiszcie jeszcze 'end' który zmieniliśmy. Nasz update powinien teraz wyglądać tak:

Kod: Zaznacz cały

	def update
		@real_x = @x - (@sprite[0].width / 2)
		@real_y = @y - @sprite[0].height

		if @moving then
			if @move_x > 0 then
				@move_x -= 1
				@x += 1
			elsif @move_x < 0 then
				@move_x += 1
				@x -= 1
			elsif @move_x == 0 then
				@moving = false
			end
		else
			if @dir == :left then
				@sprite = @stand_left
			elsif @dir == :right then
				@sprite = @stand_right
			end
		end
	end
Uruchomcie grę i zachwycćie się tym, jak nasz mały kosmita (?) się porusza!

Ostatnią rzeczą jaką dzisiaj zrobimy będzie spadanie i skakanie postaci. Zaczniemy od spadania. Najpierw musimy pozwolić SceneMap odczytać położenie postaci (ponieważ to ta klasa będzie sprawdzała, czy mamy spadać). Żeby to uczynić musimy w Player dać tzw. readery. Są dwa tego sposoby, pierwszy to funkcja

Kod: Zaznacz cały

attr_reader
przed initialize, która udostępnia oznaczone zmienne do odczytu. Druga, którą preferuję, to metody zwracające daną wartość. Także w naszej klasie Player tworzymy dwie metody:

Kod: Zaznacz cały

def get_x
		return @x
	end

	def get_y
		return @y
	end
Teraz przejdźmy do SceneMap. W updacie dajmy:

Kod: Zaznacz cały

@player.fall if no_ground?(@player.get_x, @player.get_y)
Jak widzicie, odwołujemy się tutaj do dwóch rzeczy. Pierwsza jest metodą SceneMap#no_ground?, druga to Player#fall. Żadna z tych metod nie istnieje, więc je stwórzmy. Najpierw no_ground?.
Jak widać, potrzebuje ona dwóch parametrów, x i y. Jako, że nasza mapa w tej chwili nie istnieje, metoda będzie tylko sprawdzała czy postać nie wypadnie poza okienko. Tak więc jeśli przesłane y jest mniejsze niż wielkość okna (480) zwracamy true, jeśli równe lub większe - zwracamy false.

Kod: Zaznacz cały

def no_ground?(x,y)
		return y < 480
	end
Oczywiście z czasem ta metoda będzie bardziej rozwinięta ;) Teraz zajmijmy się drugą metodą: Player#fall. Twórzmy ją w klasie Player. W niej zrobimy tylko dwie rzeczy: dodamy +2 do @y postaci oraz zmienimy grafikę.

Kod: Zaznacz cały

def fall
		@y += 2
		if @dir == :left then
			@sprite = @jump_left
		elsif @dir == :right then
			@sprite = @jump_right
		end
	end
Jak widzicie, dodaliśmy jeszcze zmianę grafiki przy spadaniu. Zapiszcie te dwa obrazki:
Obrazek
Obrazek
I wczytajcie tak samo jak poprzednie, jako @jump_left i @jump_right. Użyjcie metody Image#load_tiles mimo, że są to pojedyncze grafiki - to pozwoli nam uniknąć błędów ;) Szybkie odpalenie gry i:
Obrazek
Czyli wszystko działa. Zostało nam skaknie. Wróćmy do SceneMap, i tym razem dla odmiany udajcie się do metody 'button_down(id)'. Dlaczego tutaj a nie tak jak w przypadku ruchu, w update?
Otóż w tej metodzie, żeby gracz powtórzył skok, będzie musiał puścić przycisk i nacisnąć go jeszcze raz (jak wcześniej wspomniałem, ta metoda jest wywoływana tylko raz przy naciśnięciu!). Musimy sprawdzić więc, czy gracz naciśnie odpowiedni przycisk (strzałka do góry), oraz czy jest na ziemi (żeby nie mógł skakać odbijając się od powietrza). Najpierw tylko to sprawdzimy (pierwszy raz użyjemy printa). Nasz kod powinien wyglądać tak:

Kod: Zaznacz cały

		if id == KbUp then
			if !no_ground?(@player.get_x, @player.get_y) then
				p "skok!"
			end
		end
Co tu się dzieje? Otóż najpierw sprawdzamy, czy ID klawisza wskazuje na strzałkę w górę. Potem upewniamy się, czy gracz jest na stałym gruncie, czyli czy metoda no_ground? zwraca wartość false. Wykrzyknik przed nazwą metody (albo zmiennej) w warunkach oznacza "not", czyli "nie". Alternatywnie można by to napisać

Kod: Zaznacz cały

if no_ground?(@player.get_x, @player.get_y) == false then
Gdy te dwa warunki są spełnione, używamy printa, żeby wysłać wiadomość do wiersza poleceń. Teraz gdy uruchomimy grę i poczekamy aż nasza postać spadnie, a następnie wciśniemy strzałkę w górę dostaniemy taką oto wiadomość:
Obrazek
Czyli wszystko działa. Zastąpmy więc naszego printa wywołaniem metody skoku u gracza:

Kod: Zaznacz cały

@player.jump
Przejdźmy do klasy Player i stwórzmy tę metodę. Jeszcze wróćmy do initialize i zróbmy tę zmienną:

Kod: Zaznacz cały

@jump = 0
Przejdźmy do metody Player#fall. Dlaczego tutaj? Otóż musimy zmienić nieco kod, żeby nasza postać nie spadała podczas wyskoku, bo wtedy niemal się nie ruszymy ;) tak więc całą zawartość metody 'fall' umieśćcie w warunku 'if @jump == 0 then':

Kod: Zaznacz cały

if @jump == 0 then
			@y += 2
			if @dir == :left then
				@sprite = @jump_left
			elsif @dir == :right then
				@sprite = @jump_right
			end
		end
Teraz do metody 'jump'. Poniższy kod:

Kod: Zaznacz cały

@jump = 15 if @jump == 0
Ustawi wartość @jump na 15 tylko i wyłącznie, gdy w czasie jej wywołania @jump równe jest zero.
Dobrze, mamy więc zmienianie @jump oraz powstrzymywanie upadania postaci jeśli tylko @jump jest różne od zera. Teraz w update musimy dać ten skok.

Kod: Zaznacz cały

if @jump > 0 then
			@y -= 5
			if @dir == :left then
				@sprite = @jump_left
			elsif @dir == :right then
				@sprite = @jump_right
			end
			@jump -= 1
		end
Co ten kod robi? Jeśli skaczemy, co kratkę zmniejsza @y postaci o 5, zmienia sprite i zmnniejsza @jump o 1. To daje nam skoki na wysokość 75 pikseli. Potem potać po prostu zacznie spadać. Jak teraz uruchomicie grę zobaczycie, że działa wszystko, ale gdy w trakcie skoku poruszycie się, postać "idzie" w powietrzu. Nie do końca chcemy, żeby to tak wyglądało, prawda?
W metodach move_left i move_right musicie dać prosty warunek, żeby @sprite było zmieniane tylko gdy @jump równy jest 0: 'if @jump == 0'.

Kod: Zaznacz cały

	def move_left
		@dir = :left
		@move_x = -3
		@sprite = @walk_left if @jump == 0
		@moving = true
	end

	def move_right
		@dir = :right
		@move_x = 3
		@sprite = @walk_right if @jump == 0
		@moving = true
	end
Uruchomcie grę ponownie. Jak widać, wszystko działa!


Na dzisiaj to wszystko. Całkiem sporo udało nam się zrobić :) Do następnego!

Archiwum winrara z efektem dzisiejszej pracy.
Ostatnio zmieniony 07 sty 2014, 17:32 przez Ekhart, łącznie zmieniany 1 raz.
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
Rave

Golden Forki 2010 - Dema (miejsce 2)
Posty: 2041
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Rave »

GameBoy pisze:Animacje nie są trudne wcale:
SNIP
Jedną metodą wczytujesz plik, który będzie non-stop się animował. Można by jeszcze dopisać tu prostą metodę (3 linijki kodu) pozwalającą na wyświetlanie konkretnych klatek i tryb, w którym animacja skończy się po iluś klatkach.
Tylko po co jak są gotowe rozwiązania? Po co na nowo wynajdywać koło? No proszę, po paru googlaniach znalazłem dokumentację chingu
Awatar użytkownika
GameBoy

Golden Forki 2009 - Pełne Wersje (miejsce 1); Puchar Ligi Mapperów II (zwycięstwo); TA Sprite Contest 6 (miejsce 3)(miejsce 3)
Posty: 1769
Rejestracja: 11 lip 2009, 13:47
Lokalizacja: Wieluń

Re: Robimy grę platformową w Ruby Gosu!

Post autor: GameBoy »

Po prostu czasem gotowe rozwiązania niekoniecznie muszą nam przypasować, abo chcemy nauczyć się jak działa taka animacja.
Jak pójdziesz na studia to praktycznie wszędzie będą cię uczyć takich rzeczy od podstaw, a nie korzystania z gotowych bibliotek (co w sumie bardzo mnie denerwuje, bo wolałbym uczyć się korzystania z narzędzi, a nie ich pisania).
Awatar użytkownika
Jazzwhisky
Posty: 4332
Rejestracja: 13 kwie 2006, 21:45
Kontakt:

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Jazzwhisky »

GameBoy pisze:Jak pójdziesz na studia to praktycznie wszędzie będą cię uczyć takich rzeczy od podstaw, a nie korzystania z gotowych bibliotek (co w sumie bardzo mnie denerwuje, bo wolałbym uczyć się korzystania z narzędzi, a nie ich pisania).
To już się na niektórych uczelniach/kierunkach zmienia. =)

@Rave - wyraźnie nie rozumiesz założeń tutoriala, jeśli chcesz - napisz po prostu własny, wszyscy będą szczęśliwi. Mnie np. interesują możliwości biblioteki, czysty obraz Ruby/Gosu, który pokaże podstawy pracy w środowisku. Wplatanie w to wybranego frameworka spowodowałoby, że tut miałby inny charakter i wartości dydaktyczne niż zdecydował Ekhart, te spory o teorię i bezwzględną wyższość jednego rozwiązania nad drugim są naprawdę niepoważne.
Nasz discordowy czat, 24h/d - https://discord.gg/4GG85kr
Awatar użytkownika
Hubertov
Posty: 253
Rejestracja: 28 paź 2013, 16:07

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Hubertov »

Jeśli błąd nie wyskoczył (albo poprawiliście go w ten sposób) powinniście zobaczyć obrazek naszego gracza unoszący się blisko lewego górnego rogu.
Znowu mam coś nie tak:
http://imgur.com/AqQsTYM

Spoiler:
SceneMap
Spoiler:
Player
Spoiler:
Run.rb
Spoiler:
Jeszcze pytanie dotyczące poradnika:
Czy im dłużej wciśniemy przycisk, tym wyżej nasza postać skoczy? Jeśli nie, to może zróbmy tak, fajniejsze to o wiele i realistyczniejsze :-D
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

Hubertov pisze:
Jeśli błąd nie wyskoczył (albo poprawiliście go w ten sposób) powinniście zobaczyć obrazek naszego gracza unoszący się blisko lewego górnego rogu.
Znowu mam coś nie tak:
http://imgur.com/AqQsTYM

Jeszcze pytanie dotyczące poradnika:
Czy im dłużej wciśniemy przycisk, tym wyżej nasza postać skoczy? Jeśli nie, to może zróbmy tak, fajniejsze to o wiele i realistyczniejsze :-D
W Run.rb:

Kod: Zaznacz cały

require 'scripts/SceneMap'
Potrzebujesz rozszerzenia pliku (.rb). Spróbuj poprawić i sprawdź, czy błąd dalej wyskakuje?

I odnośnie pytania: niestety, nie dzieje się tak, póki co skok jest stały. I raczej tego też nie zrobię. Ale możliwe, że zrobimy podwójny skok ;)
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
Hubertov
Posty: 253
Rejestracja: 28 paź 2013, 16:07

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Hubertov »

Pomogło, ale błąd jest dalej:
http://imgur.com/0Nr7uX0

Ooooo, wielka szkoda, że nie będzie takiego skoku :-(
Wiele legendarnych platformówek z niego korzysta np. nieśmiertelny "Kapitan Pazur".
Trudno taki skok wykonać?
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

Hubertov pisze:Pomogło, ale błąd jest dalej:
http://imgur.com/0Nr7uX0

Ooooo, wielka szkoda, że nie będzie takiego skoku :-(
Wiele legendarnych platformówek z niego korzysta np. nieśmiertelny "Kapitan Pazur".
Trudno taki skok wykonać?
def initialize(x,z)
@x = x
@y = y
Błąd leży tutaj :p musisz zwracać uwagę na takie małe rzeczy. Staraj się najpierw sam znaleźć błąd (treść błędu pokazuje Ci w którym pliku i w której linijce się on znajduje).
A co do skoku, to w sumie nie jest taki trudny. W sumie, w następnej lekcji coś takiego dodam.
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
Hubertov
Posty: 253
Rejestracja: 28 paź 2013, 16:07

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Hubertov »

Taki błąd lol :lol:
Ekhart pisze: A co do skoku, to w sumie nie jest taki trudny. W sumie, w następnej lekcji coś takiego dodam.
Yeah 8)
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

W sumie dzisiaj mi się mało roboty trafiło, to wrzucę z wyprzedzeniem.
Lekcja 3: Prosty poziom.
I czas na lekcję 3. Na ten moment mamy zrobioną postać, którą możemy się poruszać, podskakiwać, postać również spada gdy nie ma pod sobą żadnego wsparcia. Ale sama postać na czarnym tle to nie jest dużo. Wypadałoby zrobić jakąś planszę po której nasza postać mogłaby się poruszać, prawda?

Do tego przyda nam się tileset. Zapiszcie poniższą grafikę do 'graphics/tiles'
Obrazek
Ten tileset ma rozmiary 16x16 dla każdej kratki. Wczytujemy go za pomocą znanej już nam komendy Image#load_tiles. Zróbmy to w SceneMap:

Kod: Zaznacz cały

@tileset = Image.load_tiles($window, "graphics/tiles/area02_level_tiles.png", 16, 16, true)
Co powinno wam się od razu rzucić w oczy to fakt, że tym razem ostatni parametr jest "true". Jako, że tilesety są 'tileable' (ustawiane jeden obok drugiego, zapętlane), użycie tego jest jak najbardziej na miejscu. Tileable rozmywa odrobinę krawędzie obrazka, przez co łączy się on z innymi w nieco przyjemniejszy dla oka sposób.
Do tej pory używaliśmy Image#load_tiles dla jednowymiarowych grafik (gracza). Tilesety są jednak dwuwymiarowe - kratki nie idą tylko w prawo, ale i w dół. Rezultat wciąż jednak jest jednowymiarową tabelą, a indeksy kolejnych grafik po prostu rosną. Gdy skrypt dochodzi do końca jednej linijki grafik, przeskakuje do kolejnej. Na tej grafice:
Obrazek
macie pokazane jak numeruje grafiki.
Teraz jest nieco pisania. Musimy stworzyć dwuwymiarową tabelę '@level[y][x]' zawierającą informację który tile jest w którym miejscu. Nasze okienko pomieści tych grafik 1200 (40x30). Póki co nie mamy żadnego edytora, czegoś, co nam pomoże, także trzeba pisać ręcznie. Dlatego też dla przykładu ograniczę nieco rozmiar mapy. Możecie skopiować kod poniżej do initialize:

Kod: Zaznacz cały

		@level = []
		@level[0] = [14,14,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,0,0,0,0,0,0,0,0]
		@level[1] = [14,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
		@level[2] = [10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
		@level[3] = [10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
		@level[4] = [14,2,2,2,2,2,2,5,0,0,0,0,0,1,2,2,2,2,2,2,2,2,2,2,2]
		@level[5] = [14,14,14,14,23,0,0,0,0,0,0,0,0,0,21,22,22,22,22,14,14,14,14,14,14]
		@level[6] = [14,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,14,14]
		@level[7] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,14]
		@level[8] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[9] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[10] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[11] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[12] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[13] = [14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14]
		@level[14] = [14,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,14]
Od razu przepraszam, że ta plansza będzie taka pusta, ale pisanie tylu indexów nie jest proste :p Mamy już layout mapy, teraz trzeba to wszystko wyświetlić. W metodzie draw będziemy potrzebowali dwóch pętli 'for', które będą przechodziły przez każdą pozycję w dwuwymiarowej tabeli '@level'. Dlatego piszcie:

Kod: Zaznacz cały

for y in 0...@level.size
			for x in 0...@level[y].size
				
			end
		end
Dobrze. Teraz pod zmiennymi x i y mamy index grafiki którą chcemy wyświetlić. W środku naszych pętli (tam gdzie zostawiłem puste miejsce) dajcie:

Kod: Zaznacz cały

@tileset[@level[y][x]].draw(x*16,y*16,1)
Co ten kod robi? Powoduje, że odpowiedni tile z '@tileset' (o indeksie podanym przez '@level[y][x]') jest rysowany na pozycji x*16, y*16. Teraz gdy odpalicie grę pokaże wam się nasza, bardzo mała i prosta, plansza:
Obrazek
Nasza postać przeleci przez nią i spadnie na sam dół. Musimy więc zmienić nieco naszą metodę 'no_ground?', żeby upewnić się, że zatrzymamy się na platformie ;)
Jak widzicie w tabeli @array puste miejsca są oznaczone liczbą 0. Ta liczba określa powietrze, i tak dokładnie ją będziemy traktować - jeśli pod naszą postacią jest tile o ID 0, spadamy. Żeby wiedzieć jaki tile jest pod postacią, musimy wiedzieć najpierw jakie jest położenie tile_x i tile_y naszego gracza. Można to zrobić w bardzo prosty sposób - dzieląc na 16 (rozmiar tila) i zaokrąglając. Dokładnie to zrobimy:

Kod: Zaznacz cały

def no_ground?(x,y)
		tile_x = (x/16).to_i
		tile_y = (y/16).to_i
	end
Użycie '.to_i' wymusza, żeby liczba była integerem (liczbą całkowitą). Zachodzi podstawowe zaokrąglanie (gdy po przecinku liczba jest między 0 a 4, zaokrąga w dół, 5-9 zaokrągla w górę). Terazm musimy tylko sprawdzić, czy tile pod graczem ma ID 0 czy nie.

Kod: Zaznacz cały

return @level[tile_y][tile_x] == 0
Dlaczego '@level[tile_y]' a nie '[tile_y+1]'? Ponieważ w wyniku zaokrąglania automatycznie będzie nam sprawdzało tile poniżej gracza ;) jak teraz uruchomicie zobaczycie, że zatrzymujemy się na gruncie. Zmieńcie jeszcze pozycję startową gracza na 96,16. W ten sposób zatrzymamy się na pierwszej platformie:
Obrazek
Gdy przejdziecie kawałek w prawo zobaczycie, że spadamy nieco niżej. Czyli wszystko działa jak powinno. Zauważcie jednak, że gdy poruszamy się na boki możemy wejść w ścianę. Tego też nie chcemy raczej, prawda?
Zróbmy więc nową metodę w SceneMap. Nazwijmy ją 'wall?'. Przyjmować będzie trzy parametry: x, y i direction:

Kod: Zaznacz cały

def wall?(x,y,direction)

	end
Jak będzie działała ta metoda? Otóż będzie sprawdzała, czy obok postaci (w zależności od kierunku) jest tile o ID innym niż 0. Więc piszemy:

Kod: Zaznacz cały

if direction == :left then
			return @level[tile_y-1][tile_x-1] != 0
		elsif direction == :right then
			return @level[tile_y-1][tile_x+1] != 0
		end
Dodatkowo musimy zrobić, żeby ta metoda była wywoływana przy poruszaniu się postaci. Dlatego też w update wprowadźmy drobną zmianę:

Kod: Zaznacz cały

		if $window.button_down?(KbLeft) and !wall?(@player.get_x, @player.get_y, :left) then
			@player.move_left
		end
		if $window.button_down?(KbRight) and !wall?(@player.get_x, @player.get_y, :right) then
			@player.move_right 
		end
Teraz podejście do ściany zablokuje naszą postać. Jeśli przeniesiecie postać nieco w prawo zobaczycie, że gdy dojdziemy do krawędzi mapy nie możemy przejść dalej, czyli jest tak, jak być powinno. Ale to też tworzy jeden drobny problem. Spróbujcie skoczyć stojąc na platformie na której zaczynamy. Postać podskoczyła i zniknęła. Powód tego jest taki, że znaleźliśmy się poza obszarem planszy, i nasz skrypt przyjmuje, że nie jesteśmy na powietrzu (wartość nil, czyli nic, nie jest równa 0, więc nie jest powietrzem). Do tego i tak wylądujemy na suficie. Najlepszym, co możemy zrobić będzie sprawdzenie, czy nad głową postaci jest jakiś grunt. Jeśli jest - nie możemy skoczyć wyżej. Niestety, sprawdzanie tego przed skokiem jest nieopłacalne, więc trzeba będzie to sprawdzać aktywnie. W klasie Player zróbmy dwie metody: 'is_jumping?' oraz 'reset_jump'

Kod: Zaznacz cały

	def is_jumping?
		return @jump > 0
	end

	def reset_jump
		@jump = 0
	end
Pierwsza sprawdzi nam, czy postać skacze. Druga anuluje skok postaci. Wróćmy do SceneMap. W update dajmy:

Kod: Zaznacz cały

		if @player.is_jumping? then
			if solid_overhead?(@player.get_x, @player.get_y) then
				@player.reset_jump
			end
		end
To najpierw sprawdzi, czy postać skacze. Jeśli skacze, wywołujemy metodę 'solid_overhead?' która sprawdza, czy nad głową postaci znajduje się coś innego niż powietrze. Jeśli tak jest, resetujemy skok naszej postaci. Metoda 'solid_overhead?' wygląda tak:

Kod: Zaznacz cały

	def solid_overhead?(x,y)
		tile_x = (x/16).to_i
		tile_y = (y/16).to_i
		return @level[tile_y-2][tile_x] != 0
	end
Jak sprawdzicie, wszystko powinno działać. Wyjście poza planszę również jest niemożliwe.
Na koniec dodamy jeszcze jedną małą rzecz, proponowaną przez Hubertova, a mianowicie, żeby po puszczeniu strzałki w górę postać zakończyła skok i zaczęła spadać. Dzięki naszej metodzie 'reset_jump', jest to banalnie proste. W SceneMap#button_up dodajcie:

Kod: Zaznacz cały

if id == KbUp then
			@player.reset_jump if @player.is_jumping?
		end
Ten kod sprawdzi, czy puściliśmy przycisk. Jeśli tak, skok postaci się kończy.


Na dzisiaj to już wszystko. Temat kolejnej lekcji wybieracie wy! W komentarzu napiszcie, co byście woleli zrobić pierwsze:
1) Ekran Tytułowy
2) Muzyka i efekty dźwiękowe
3) Przedmioty na mapie
Możecie również dać swoją propozycję!

Archiwum winrara z efektem dzisiejszej pracy.
No matter how tender, how exquisite… A lie will remain a lie.
lorak

Golden Forki Special - Premiery (zwycięstwo); Liga Mapperów Sezon VI (miejsce 1); Liga Mapperów Sezon V (miejsce 3)
Posty: 241
Rejestracja: 18 lip 2011, 15:11

Re: Robimy grę platformową w Ruby Gosu!

Post autor: lorak »

Fajnie, że coś takiego robisz. Kiedyś chciałem stworzyć od zera platformówkę, ale nie dałem rady przebrnąć przez naukę tak niskopoziomowych bibliotek jak OGL czy DirectX, choć cały proces tworzenia platformówki jest opisany ładnie tu: http://informatyka.wroc.pl/node/387

Mimo, że nie mam zamiaru tworzyć w gosu to korzystam i tak, bo techniki i triki mają zastosowanie ogólne i można się tego szybko nauczyć. Zwłaszcza, że nieźle piszesz :D

Co do pomysłów: fajnie by było zrobić mechanizm wyświetlania mapy większej niż obszar ekranu.
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

lorak pisze:Fajnie, że coś takiego robisz. Kiedyś chciałem stworzyć od zera platformówkę, ale nie dałem rady przebrnąć przez naukę tak niskopoziomowych bibliotek jak OGL czy DirectX, choć cały proces tworzenia platformówki jest opisany ładnie tu: http://informatyka.wroc.pl/node/387

Mimo, że nie mam zamiaru tworzyć w gosu to korzystam i tak, bo techniki i triki mają zastosowanie ogólne i można się tego szybko nauczyć. Zwłaszcza, że nieźle piszesz :D

Co do pomysłów: fajnie by było zrobić mechanizm wyświetlania mapy większej niż obszar ekranu.
Cieszę się, że jest przydatne nawet, jeśli nie dla Twojego środowiska. Sam tak zrobiłem system cząsteczek do swojej gry - na podstawie tutoriala XNA (próbowałem na podstawie cząsteczek Areva i nie potrafiłem, a na obcym środowisku rozkminiłem! :P).

A sam mechanizm wyświetlania większej mapy będzie w swoim czasie. Łącznie z jakimś w miarę prostym parallax scrollingiem, dla nieco ciekawszego efektu.
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
Rave

Golden Forki 2010 - Dema (miejsce 2)
Posty: 2041
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Rave »

Ruby jest o tyle fajnym językiem, że pozwala na użycie polskich liter w nazwach zmiennych, o ile kodowanie jest poprawne, więc kod:

Kod: Zaznacz cały

gżegżółka = "To nie powinno działać!"
puts gżegżółka
będzie działać. Spacje jednak dalej nie działają ;).
Awatar użytkownika
Ekhart

Golden Forki 2015 - Zapowiedzi (zwycięstwo); Golden Forki 2011 - Dema (miejsce 1)
Posty: 447
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Midleton

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Ekhart »

Owszem, polskie znaki możesz umieścić jako nazwę zmiennej. Jednak są pewne podstawowe zasady, paradygmaty, których się lepiej trzymać, bo po coś one są. Jednym z nich jest używanie tylko podstawowych znaków w nazwach zmiennych. Ja np. Twojego programu z taką zmienną jaką podałeś już nie odpalę. Inne osoby o systemie nie-polskim również. Nawet nie jestem pewien, czy skompilowanie takiego projektu będzie możliwe. Więc staraj się trzymać z góry ustalonych zasad. Komplikowanie sobie wszystkiego nie jest tego warte :p
A spacja w nazwie zmiennej owszem, nie będzie działała, bo jeśli po jakiejś nazwie nie dasz znaku '=', ruby traktuje to jako wywołanie metody.
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
Rave

Golden Forki 2010 - Dema (miejsce 2)
Posty: 2041
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Post autor: Rave »

Wiem, głupi nie jestem. Chciałem się podzielić tym jako ciekawostką.

Anyway, co powiesz jakby w następnej części zająć się obsługą autotilesów, na kształt tych z makera XP? Takie coś się by przydało, nawet w twojej mapce, bo "ziemię" wraz z trawą można by przedstawić jako autotiles.

W każdym razie, czekam na kolejną część. Prosiłbym też o pomoc w moim problemie z Graphics Gale jak ktoś zna ten program (temat w offtopie).
ODPOWIEDZ