Robimy grę platformową w Ruby Gosu!

Tematy różne, różniste.
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 »

No niestety mam praktycznie tak samo:

Kod: Zaznacz cały

if $gamewindow.button_down?(KbLeft) and !wall?(@player.middle_x, @player.y, :left) then
			@player.move_left
		end
		if $gamewindow.button_down?(KbRight) and !wall?(@player.middle_x, @player.y, :right) then
			@player.move_right
		end
(player.middle_x jest ustawiane na player.x + (player.width/2) w klasie Game_Object) no i nadal padaka jest. Tutaj jest najnowsza wersja: https://dl.dropboxusercontent.com/u/210 ... yjatka.rar, jakbyś ty, albo kto inny mógł zajrzeć...
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łaśnie to

Kod: Zaznacz cały

elsif
najwięcej zmienia. Bo teraz sprawdzasz dla obu na raz, a w drugim przypadku po prostu sprawdzasz, czy jeden jest, i jeśli nie, to dopiero sprawdzasz drugi.
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 »

No, to naprawiło padakę przy prawej ścianie, nie zauważyłem tego elsifa. Niestety teraz jak podejdę do lewej krawędzi ekranu i tam wcisnę obie strzałki na raz, to znowu jest padaka.
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 4: Edytor
Dzisiaj odejdziemy nieco od samego robienia gry. Niemniej jednak, to co będziemy robili, jest ważne. Mianowicie: zrobimy podstawę naszego edytora. Samą podstawę, tylko i wyłącznie edytor mapy. Potem, w miarę prac nad grą, będziemy również dodawać więcej funkcji w naszym edytorze.

Od razu na początek ostrzegam, że będzie trochę pisania kodu. Efekty również będą nieco nudne, ale nie można robić tylko ciekawych rzeczy :p

Jako, że edytor nie będzie częścią gry, musimy zrobić osobny folder: '/edytor'. Tam będziemy umieszcać wszystko co jest częścią samego edytora (więc wszystkie skrypty i grafiki). Także stwórzcie ten folder w folderze głównym gry:
Obrazek

Będziemy potrzebowali również stworzyć trzy nowe pliki: Edytor.rb (odpowiednik naszego Run.rb, zapisany w folderze głównym), EditorWindow.rb (odpowiednik naszego GameWindow.rb) i SceneEditor.rb (oba zapisane w '/edytor'). W tej chwili powinniście już wiedzieć jak je zrobić. Jedyną różnicą jest to, że nasze okno będzie większe niż okno gry - 1024x768. Jeśli wasza rozdzielczość ekranu jest taka sama, lub niewiele większa, możecie się zastanowić nad ustawieniem 'true' jako parametru fullscreen, ponieważ w przypadku gdy okno Gosu jest większe niż bodajże 90% rozmiaru ekranu, jest ono skalowane w dół. To nieco pogarsza precyzję (aczkolwiek wciąż jest możliwość używania takiego zeskalowanego okna). W dalszej części możecie porównać sobie działanie edytora jako okienko i fullscreen (jeśli oczywiście skalowanie was tyczy), i podjąć decyzję ;)

Kod: Zaznacz cały

$: << File.dirname(__FILE__)

require 'gosu'
require 'rubygems'
include Gosu

require 'edytor/EditorWindow.rb'
require 'edytor/SceneEditor.rb'

$window = EditorWindow.new
$window.show

Kod: Zaznacz cały

class EditorWindow < Gosu::Window

	def initialize
		super(1024,768,false)
		self.caption = "Fuzed: Map Editor"
		$window = self
		$scene = SceneEditor.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

Kod: Zaznacz cały

class SceneEditor

	def initialize

	end
	
	def update

	end

	def draw

	end

	def button_down(id)

	end

	def button_up(id)

	end

end
Będziemy również potrzebowali grafiki dla naszego edytora. Użyjemy tej:
https://dl.dropboxusercontent.com/u/189 ... editor.png
(jako link, ponieważ obraz za duży dla forum ;))
Wczytajmy ją w naszym SceneEditor jako '@bg', jako zwykły obrazek, oraz umieśćmy ją w draw ;)

Kod: Zaznacz cały

#initialize
@bg = Image.new($window,"edytor/editor.png", false)
# draw
@bg.draw(0,0,0)
Jako, że edytor będzie używał sterowania myszą, musimy go do niej dostosować. Obsługa myszy jest wbudowana w Gosu, ale jak zauważycie, kursor myszy się nie wyświetla gdy najedziecie na okno. Można to obejść w dwa sposoby. Pierwszym jest utworzenie grafiki kursora (Image) i wyświetlanie go na pozycji $window.mouse_x, $window.mouse_y. To pozwala na używanie dowolnych kursorów, ale w przypadku niskiego FPS kursor skacze.
Drugim jest zapewnienie naszego programu, że tak, potrzebujemy kursora myszy. Wtedy jest używany systemowy kursor. W poradniku użyjemy tego drugiego sposobu, ale jeśli ktoś chce, możecie spokojnie wykorzystać sposób #1 ;)
Żeby kursor systemowy wyświetlał się w oknie, musimy w naszej klasie EditorWindow dać tę oto metodę:

Kod: Zaznacz cały

	def needs_cursor?
		return true
	end
W ten sposób informujemy system, żeby wyświetlał nasz kursor myszy.
Obrazek

Kolejną rzeczą jaką musimy zrobić to utworzyć siatkę naszej mapy. Siatka będzie oczywiście trójwymiarową tablicą. Dlaczego trójwymiarową, a nie tak jak mieliśmy, dwuwymiarową?
Ponieważ dodamy warstwy! Na razie robimy mapę o stałym rozmiarze, ale to się zmieni z czasem :p

Kod: Zaznacz cały

	width = 60
		height = 45
		@level = []
		for layer in 0...2
			@level[layer] = []
			for y in 0...height
				@level[layer][y] = []
				for x in 0...width
					@level[layer][y][x] = 0
				end
			end
		end
Tłumaczenie powyższego kodu: dla każdej z warstw (warstwy mamy 2 w tej chwili) tworzy tablicę rozmiarów 60x45 wypełnioną zerami. To jest nasza, na razie pusta, plansza. Potrzebujemy jeszcze wczytać nasz tileset:

Kod: Zaznacz cały

@tileset = Image.load_tiles($window, "graphics/tiles/area02_level_tiles.png", 16, 16, true)
Zacznijmy może od narysowania naszej mapy w obszarze "Map", żebyśmy mogli upewnić się, że nasza siatka @level działa. W metodzie draw musimy ponownie stworzyć potrójną pętle for.

Kod: Zaznacz cały

		for l in 0...@level.size
			for y in 0...@level[l].size
				for x in 0...@level[l][y].size

				end
			end
		end
Zanim zaczniemy rysować obszar mapy, musimy wiedzieć jaki x i y mają poszczególne tile. Zauważcie, że obszar "Map" nie zaczyna się w 0:0, a w 368:160. Tak więc obliczamy nasze położenie:

Kod: Zaznacz cały

				tx = 368 + (x*16)
					ty = 160 + (y*16)
					i = @level[l][y][x]
					@tileset[i].draw(tx,ty,1+l)
To nam dla każdego tila obliczy jego położenie i wyświetli, na z 1 + warstwa. Uruchomienie teraz edytora nie pokaże nic, ponieważ wypełniamy naszą mapę przeźroczystymi grafikami. Dlatego też dla sprawdzenia zmieńmy na chwilę wszystkie wartości z 0 na 1 w initialize ('@level[layer][y][x] = 1'). I rzeczywiście, wyświetla się grafika, ale jest pewien problem:
Obrazek
Grafika wychodzi poza krawędzie! Co z tym zrobić?
Ograniczyć pętlę ;) przy rozmiarze obszaru "Map" (które równe jest okienku naszej gry) zmieścimy 40x30 kratek. Więc:

Kod: Zaznacz cały

				if x < 40 and y < 30 then
						tx = 368 + (x*16)	
						ty = 160 + (y*16)
						i = @level[l][y][x]
						@tileset[i].draw(tx,ty,1+l)
					end
I rezultat:
Obrazek
To przy okazji nieco zmniejsza zużycie procesora, bo wyświetla mniejszą ilość grafik. Żeby jeszcze bardziej przyśpieszyć, możemy do linijki '@tileset.draw(tx,ty,1+l)' dodać:

Kod: Zaznacz cały

if i != 0
To nie będzie wyświetlało grafiki jeśli index danego tila jest 0 (czyli grafika jest w 100% przeźroczysta). Dobra, możecie ustawić tile znów na 0, sprawdziliśmy to, co chcieliśmy. Teraz trzeba wyświetlić nasz tileset. Co może być nieco trudne, ze względu na zapętlenie. Grafika tilesetu zaczyna się w pozycji 16:112.

Kod: Zaznacz cały

for i in 0...@tileset.size
			tx = 16 + (i*16)
			ty = 112
			@tileset[i].draw(tx,ty,1)
		end
Powyższy kod narysuje wszystko w prostej, ciągłej linii, a ten kod:

Kod: Zaznacz cały

for i in 0...@tileset.size
			tx = 16 + (i*16)
			ty = 112 + (i*16)
			@tileset[i].draw(tx,ty,1)
		end
Narysuje całość po skosie. Więc jak zrobić, żeby grafika narysowała się 20 razy a potem przeskoczyła do kolejnej linijki?
Otóż, logicznie biorąc, musimy wyświetlić 20 tili, następnie przejść do kolejnej linijki, wyświetlić kolejne 20 tili i tak dalej. Zrobić to możemy tym oto kodem:

Kod: Zaznacz cały

		for i in 0...@tileset.size
			tx = 16 + ((i%20)*16)
			ty = 112 + ((i/20)*16)
			@tileset[i].draw(tx,ty,1)
		end
W oczy powinien wam się od razu rzucić nowy znak: %. Znak %, lub mod, modulo, reszta wykonuje dzielenie, ale zwraca nie wynik tego dzielenia, a resztę (np. 5 % 2 zwróci nam 1). Jak więc nasz kod działa?
Obliczając tx bierzemy index w zasięgu 1 - 20 (i%20) i mnożymy przez 16 dla dokładnej pozycji. Dla obliczenia ty po prostu dzielimy obecny indeks i dzielimy przez 20. Takie obliczanie pozwoli nam osiągnąć to:
Obrazek

Teraz musimy zrobić obsługę kliknięcia myszą. Zaczniemy od samego wykrycia kliknięcia, potem gdzie to kliknięcie nastąpiło, a na końcu wybieranie tila i stawianie go na mapie :) W SceneEditor#button_down(id) dajcie warunek sprawdzający, czy wykryło kliknięcie lewym przyciskiem myszy. Jeśli tak, powinniśmy zostać odesłani do metody 'click'.

Kod: Zaznacz cały

		if id == MsLeft then
			click
		end
Oczywiście musimy również utworzyć tę metodę. W niej na razie będziemy mieli dwa warunki. Jeden sprawdzi, czy kliknęliśmy nad obszarem tilesetu, drugi: nad obszarem mapy. Do tego możemy użyć metody #between().

Kod: Zaznacz cały

	def click
		if $window.mouse_x.between?(16, 336) and $window.mouse_y.between?(112, 304) then
			p "tileset"
		elsif $window.mouse_x.between?(368, 1008) and $window.mouse_y.between?(160, 640) then
			p "mapa"
		end
	end
Teraz jeśli klikniecie nad tilesetem albo mapą powinniście otrzymać odpowiednią informację w naszym wierszu poleceń. Skoro to już działa, czas zrobić zaznaczanie tile. Przyda nam się do tego poniższa grafika, którą będziemy określać obecnie zaznaczonego tila:
Obrazek
Wczytajcie ją jako '@sel_16'. Zróbcie też dwie zmienne, @sel_16_x i @sel_16_y, ich wartości ustawcie na 16:112, przez co zostanie wyświetlona na pierwszym zaznaczonym tile. Zróbcie też zmienną '@selected_tile = 0'

Kod: Zaznacz cały

#Initialize
		@sel_16 = Image.new($window, "edytor/sel16.png", false)
		@sel_16_x = 16
		@sel_16_y = 112
		@selected_tile = 0
		
		# Draw
		@sel_16.draw(@sel_16_x, @sel_16_y, 5)
To łatwiejsze mamy z głowy. Teraz musimy zrobić zaznaczanie tile. W naszej metodzie 'click' , zastąpcie 'p "tileset"' wywołaniem metody: 'select_tile($window.mouse_x, $window.mouse_y)'. Metodę oczywiście trzeba stworzyć, z dwoma parametrami: x i y.

Kod: Zaznacz cały

	def select_tile(x,y)
		tx = ((x - 16) / 16).floor
		ty = ((y - 112) / 16).floor
		i = tx + (ty*20)
		@sel_16_x = (tx * 16) + 16
		@sel_16_y = (ty * 16) + 112
		@select_tile = i
	end
Co robi ten kod? Po kolei: Sprawdza x i y danego tila, na którym kliknęliśmy. .floor sprawia, że zawsze jest zaokrąglona wartość w dół (więc w przypadku, gdy klikniemy za połową tila nie zostanie zaznaczony następny tile). Następnie obliczamy index danego tila (pamiętajcie, że nasz tileset jest tablicą jednowymiarową!). Kolejną rzeczą jest ustalenie pozycji x i y dla naszego zaznaczenia. Dlaczego zrobiłem to tak, a nie po prostu przypisałem wartości x i y które przesłałem jako parametry? Ponieważ nie byłyby wyrównane do siatki 16x16, i zaznaczenie nie byłoby dokładne. Ostatnią rzeczą jest zmienienie wartości @selected_tile na index który obliczyliśmy. Teraz zróbmy stawianie tila na mapie. Zróbmy metodę 'place_tile(x,y)', i prześlijmy do niej dane identycznie jak przed chwilą. W initialize zróbmy jeszcze jedną zmienną: @current_layer = 0. Będziemy jej teraz potrzebowali.
Metoda stawiania tila jest bardzo prosta. Znajdujemy tx i ty (czyli x i y tila nad którym kliknęliśmy) i zmieniamy wartość @level[warstwa][y][x] na @selected_tile. Myślę, że sami dacie sobie z tym radę ;)

Kod: Zaznacz cały

	def place_tile(x,y)
		tx = ((x - 368) / 16).floor
		ty = ((y - 160) / 16).floor
		@level[@current_layer][ty][tx] = @select_tile
	end
Jak zauważycie, możecie spokojnie stawiać tile, ale pojedynczo. Skoczmy więc szybko do update'a i dodajmy taki kod:

Kod: Zaznacz cały

		if $window.button_down?(MsLeft) and $window.mouse_x.between?(368, 1008) and $window.mouse_y.between?(160, 640) then
			place_tile($window.mouse_x, $window.mouse_y)
		end
To pozwoli nam rysować danym tilem po mapie. Jednak jest problem, jeśli nie zaznaczymy nic, tylko od razu spróbujemy rysować po mapie, czeka na nas (nie)miła niespodzianka:
Obrazek
Jak możemy wyczytać z błędu, skrypt ma problem z odczytaniem wartości Integer z podanej wartości nil (czyli nie potrafi odczytać Liczby z wartości nic). Nie powinno tak być, więc szybko znajdźmy linijkę w której to się dzieje:

Kod: Zaznacz cały

						@tileset[i].draw(tx,ty,1+l) if i != 0 
I dodajmy jeszcze jeden warunek:

Kod: Zaznacz cały

						@tileset[i].draw(tx,ty,1+l) if i != 0 and i != nil
Uruchomcie jeszcze raz i sprawdźcie, czy błąd wyskakuje. Nie powinien ;) do tego możemy już robić nieco szybciej nasze mapy:
Obrazek
(to nie jest szczyt moich mappingowych umiejętności, ale cóż :P). Wygląda jakbyśmy byli blisko końca?
No niestety, musimy zrobić jeszcze kilka rzeczy. Najpierw ściągnijcie ten obrazek:
Obrazek
I wczytajcie go w edytorze. Wyświetlimy go nad mapą, żeby dokładniej widzieć krawędzie naszych tili. Sami już potraficie to zrobić. Wyświetlcie na współrzędnych 368:160:5
Poza tym mamy do zrobienia jeszcze kilka rzeczy. Pierwszą będzie przesuwanie mapy. Następnie zrobimy przełączanie między warstwą 1 a 2, stawianie postaci gracza na pozycji startowej i, oczywiście, zapisywanie mapy do pliku.

Zacznijmy od ustawienia trzech nowych zmiennych:

Kod: Zaznacz cały

		@offset_x = 0
		@offset_y = 0
		@ctrl_held = false
Pierwsze dwie mają nasz offset, czyli o ile tili przesunięta mapa jest. Ostatnia będzie służyła do przełączania między przewijaniem poziomo a pionowo. Przejdźmy do metody 'button_down(id)' i dajmy trzy warunki:

Kod: Zaznacz cały

		if id == MsWheelDown then
			increase_offset
		end
		if id == MsWheelUp then
			decrease_offset
		end
		if id == KbLeftControl then
			@ctrl_held = true
		end
Ostatni warunek również dajmy do metody 'button_up(id)', z tą różnicą, że tam zmieniamy wartość na false. Zróbmy również dwie brakujące metody.

Kod: Zaznacz cały

	def increase_offset
		if @ctrl_held then
			@offset_x += 1
		else
			@offset_y += 1
		end
	end

	def decrease_offset
		if @ctrl_held then
			@offset_x -= 1
		else
			@offset_y -= 1
		end
	end
Co tu zrobiliśmy, i jak to działa. Za każdym razem gdy użyjemy pokrętła w rolce, nasz @offset_y się zwiększa (rolka w dół) albo zmniejsza (rolka w górę). Jednak gdy w międzyczasie trzymamy wciśnięty lewy control, zmienia się nasz @offset_x (zmiany są analogiczne). Zróbmy jeszcze kilka warunków: zabrońmy offsetów mniejszych niż 0, i większych niż szerokość/wysokość mapy - obszar który widzimy. Przez to będziemy mieli pewność, że nasza mapa nie zginie gdzieś tak poza granicami ;) Potrzebujemy jeszcze, żeby nasze offsety miały jakieś odzwierciedlenie na mapie. W metodzie 'draw' znajdźcie linijkę:

Kod: Zaznacz cały

i = @level[l][y][x]
i dodajcie w niej nasze offsety:

Kod: Zaznacz cały

i = @level[l][y + @offset_y][x + @offset_x]
Odpalcie grę, postawcie jakiś tile i zobaczcie jak zmienia pozycję. Jeśli nie posiadacie w myszce rolki, albo używacie laptopa, musimy zmienić nieco metodę i dodać możliwość użycia klawiatury. Do obu metod odpowiedzialnych za offset dajcie parametr 'forced = false' i zmieńcie je w taki sposób:

Kod: Zaznacz cały

	def increase_offset(forced = false)
		if @ctrl_held or forced then
			@offset_x += 1 if (@offset_x < @level[0][0].size - 40)
		else
			@offset_y += 1 if (@offset_y < @level[0].size - 30)
		end
	end

	def decrease_offset(forced = false)
		if @ctrl_held or forced then
			@offset_x -= 1 if @offset_x > 0
		else
			@offset_y -= 1 if @offset_y > 0
		end
	end
A do 'button_down(id)' dajcie warunki dla każdej ze strzałek:

Kod: Zaznacz cały

		if id == KbUp then
			decrease_offset
		end
		if id == KbDown then
			increase_offset
		end
		if id == KbLeft then
			decrease_offset(true)
		end
		if id == KbRight then
			increase_offset(true)
		end
Parametr 'forced', jak mam nadzieję, że zauważyliście, jest wysyłany tylko przy poruszaniu się strzałkami w prawo i lewo. Dzięki temu @offset_x może zostać zmieniany za pomocą odpowiednich strzałek, a nie musimy trzymać lewego controla i używać strzałek w górę i w dół (aczkolwiek ten sposób również działa, możecie sprawdzić ;) ).
Możliwe również, że zauważyliście kolejny mały błąd. Otóż offsety nie wpływają na to, gdzie kładziemy kolejne tile. Nie działają, ponieważ stawiając tila nie bierzemy ich pod uwagę ;) w metodzie 'place_tile(x,y)' musicie dodać nasze offsety do tx i ty. Gdy to zrobicie, wszystko będzie działało tak, jak powinno.

Teraz zajmijmy się przełączaniem warstw. Potrzebna nam będzie ta grafika:
Obrazek
zapisana jako @sel_32. Do niej również zróbcie zmienne _x/_y, jak robiliśmy to wcześniej, i ustawcie współrzędne na 672:112. Od razu ustawcie rysowanie tego w metodzie draw, na z=5.
Zmieńcie funkcję click żeby brała pod uwagę kliknięcia nad dwoma przyciskami wartstw:

Kod: Zaznacz cały

		elsif $window.mouse_x.between?(672,704) and $window.mouse_y.between?(112,144) then
			@current_layer = 0
			@sel_32_x = 672
		elsif $window.mouse_x.between?(720,752) and $window.mouse_y.between?(112,144) then
			@current_layer = 1
			@sel_32_x = 720
Dodatkowo przejdźmy do draw i ustawmy, żeby aktywna warstwa różniła się nieco od nieaktywnej. Najprostrzym będzie zmienienie przeźroczystości nieaktywnej warstwy na 160. Nasz kod:

Kod: Zaznacz cały

@tileset[i].draw(tx,ty,1+l) if i != 0 and i != nil
musimy nieco rozbudować. Pierwsza rzecz, którą musimy zrobić, to sprawdzić, czy warstwa l jest aktywna czy nie:

Kod: Zaznacz cały

if l == @current_layer then

						else

						end
I rozbudować kod rysowania. Metoda Image#draw może pobrać więcej argumentów niż x,y,z:

Kod: Zaznacz cały

draw(x, y, z, scale_x, scale_y, color, mode)
Scale_x i _y są proste do zrozumienia. Po naszym z musimy dać wartości 1.0 dla obu skali (ponieważ nie chcemy zmieniać rozmiaru naszego obrazka). To wartość color nas obchodzi. Dla aktywnej warstwy nie zmieniamy nic, a dla warstwy nieaktywnej (w części 'else' warunku) musimy ustawić kolor: 'Color.new(160,255,255,255)'.

Kod: Zaznacz cały

@tileset[i].draw(tx,ty,1+l,1.0,1.0,Color.new(160,255,255,255)) if i != 0 and i != nil
Sprawdźcie sami, ustawcie kilka bloków na pierwszej warstwie a następnie przełączcie na warstwę drugą. Pierwsza warstwa jest nieco przeźroczysta. Możecie dopasować przeźroczystość do własnych potrzeb, zmieniając '160' na inną wartość.

Teraz zajmiemy się stawianiem naszej postaci. Do tego potrzebujemy kilku zmiennych:

Kod: Zaznacz cały

		@objects = []
		@player_graphic = Image.load_tiles($window, "graphics/sprites/player_1_run_left.png", 32, 32, false)
		@active_mode = :map
		@object_held = nil
@active_mode jest ważną zmienną. Nią ustalamy, czy w danym momencie stawiamy objekty (gracz, później więcej elementów) czy robimy mapę.
Narysujmy jeszcze naszą postać gracza:

Kod: Zaznacz cały

frame = milliseconds / 150 % @player_graphic.size
		@player_graphic[frame].draw(32,352,1)
Następnie sprawdźmy, czy kliknęliśmy w tym miejscu (32:352, rozmiar 32x32 piksele). Jeśli tak, odeślijmy do metody 'select_object(:player)'. W tej metodzie przypiszemy zmiennej '@object_held' wartość ':player' i zmienimy mode na ':objects'. Następnie zejdźmy do 'draw' i dodajmy ten oto kod:

Kod: Zaznacz cały

		case @object_held
			when nil

			when :player
				@player_graphic[0].draw($window.mouse_x, $window.mouse_y,10)
		end
Tak wygląda switch statement. Czasami będziemy go używać. Ten oto rysuje grafikę postaci (jeśli mamy ją zaznaczoną) pod kursorem myszy. Mamy zaznaczanie postaci, teraz czas na stawianie jej na mapie. Wróćmy do metody 'click', a dokładnie do części odsyłającej do 'place_tile' i zmieńmy ją na:

Kod: Zaznacz cały

if @active_mode == :map then
				place_tile($window.mouse_x, $window.mouse_y)
			elsif @active_mode == :objects then
				place_object($window.mouse_x, $window.mouse_y)
			end
Dodajmy również tę metodę:

Kod: Zaznacz cały

	def place_object(x,y)
		rx = x + (@offset_x*16) - 368
		ry = y + (@offset_y*16) - 160
		@objects << [@object_held, rx, ry]
		if @object_held == :player then
			@object_held = nil
		end
	end
Ten kod ustawi informacje o pozycji gracza, oraz (w tym przypadku) zmieni trzymany objekt na pustą wartość. W przypadku innych elementów nie będziemy tego robić (będzie można stawiać elementy masowo). Musimy jeszcze wyświetlać nasze elementy na mapie:

Kod: Zaznacz cały

		for i in 0...@objects.size
			case @objects[i][0]
				when :player
					frame = milliseconds / 150 % @player_graphic.size
					rx = @objects[i][1] - (@offset_x * 16) + 368
					ry = @objects[i][2] - (@offset_y * 16) + 160
					@player_graphic[frame].draw(rx,ry,6)
			end
		end
Teraz gdy postawicie naszego gracza, zauważycie, że pojawił się na naszej mapce. Ale gdy postawicie drugiego, są dwie postaci. Musimy coś z tym zrobić. Wróćmy do metody 'place_object' i na samym jej początku dajmy:

Kod: Zaznacz cały

if @object_held == :player then
			for i in 0...@objects.size
				if @objects[i][0] == :player
					@objects.delete_at(i)
				end
			end
		end
To sprawdzi, czy mamy już gracza w naszej zmiennej, i jeśli tak jest - usuwa go przed postawieniem w nowej pozycji.
Zanim przejdziemy do ostatniej już części, dajcie w metodzie 'select_tile' zmianę na mapę:

Kod: Zaznacz cały

@active_mode = :map
Gdy to zrobiliśmy, czas na końcówkę dzisiejszej lekcji, czyli zapisywanie naszej mapy do pliku!
Najpierw wykrywajmy kliknięcie myszą nad pozycją 560:112 (ikonka zapisu). Jeśli tak, niech nas kliknięcie tam odeśle do metody 'save'.

W naszym pliku mapy musimy zachować trzy informacje: naszą mapę, tileset użyty i objekty. Na ten moment będziemy zapisywać jako "Map000.map". To oczywiście z czasem dopracujemy ;)
Nasza metoda save:

Kod: Zaznacz cały

	def save
		f = File.new("Map000.map", "w+")
		Marshal.dump(@tileset, f)
		Marshal.dump(@level, f)
		Marshal.dump(@objects, f)
		f.close
	end
Jednak użycie jej wyświetli nam taki oto błąd:
Obrazek
Powód tego błędu jest taki, że Marshal nie potrafi zapisać iformacji z Gosu::Image. Możemy jedynie zapisać ścieżkę/nazwę tileseta w postaci stringa. Dlatego w naszym initialize dokonajmy drobnej zmiany:

Kod: Zaznacz cały

@used_tileset = "area02_level_tiles.png"
		@tileset = Image.load_tiles($window, "graphics/tiles/#{@used_tileset}", 16, 16, true)
I w metodzie 'save' zmieńmy '@tileset' na '@used_tileset'. Teraz po kliknięciu ikonki zapisu dzieje się magia, której wynikiem jest:
Obrazek.

Na dzisiaj to koniec, w następnej części nauczymy się wczytywania mapy, zrobimy też obsługę mapy większej, niż wielkość ekranu. A potem... zobaczymy ;)

Archiwum winrara z efektem dzisiejszej pracy.
No matter how tender, how exquisite… A lie will remain a lie.
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 »

Zamierzony doublepost #2!

Nie wiem, czy znacie taki program jak Tiled, ale jest dość przydatny i łatwy w obsłudze. Prawdopodobnie jutro dam tutorial specjalny (4.5 :P) w którym poruszę nieco temat tego programu oraz zrobimy do naszego edytora mały parser, pozwalający importować dane mapy z tego programu. To powinno nieco przyśpieszyć tworzenie map (przynajmniej ich części graficznej) :p
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 »

Dobry pomysł. Ja już rozkminiam gema tmx, no ale poczekam na tutka.
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 Specjalna 4.5: Parsowanie map z Tiled
Jak zapowiedziałem wczoraj, dzisiejszy poradnik nie jest obowiązkowy dla naszej gry, ale może być całkiem przydatny. Nauczymy się w nim importować dane naszych map z programu Tiled.

Zaczniemy od eksportowania potrzebnych nam informacji. Zróbcie mapkę w Tiled i uzywając opcji "Eksportuj jako...", zapiszcie naszą mapkę jako plik mapy 'Flare' (.txt). Jest to plik tekstowy, więc łatwiej będzie nam go otworzyć i wyciągnąć dane jakich potrzebujemy.
Obrazek

Gdy otworzycie ten plik zobaczycie informacje podzielone na kilka sekcji:
- [header] ma informacje o rozmiarze mapy i pojedynczych tili
- [tilesets] ma informacje o położeniu tileseta, rozmiarze tili w tym tilesecie i ich offsecie.
- [layer] ma informacje których potrzebujemy. A konkretnie 'data'. Zauważcie duży zbiór cyfr. Każda warstwa ma ten zbiór cyfr, i właśnie on jest nam potrzebny. Zaznaczcie wszystkie cyfry danej warstwy i zapiszcie w osobnym pliku: 'layerX.txt' (za X podłóżcie 0 albo 1, w zależności od warstwy). Te dwa pliki umieścćcie w folderze edytora.
Jako, że na ten moment robimy wszystko w edytorze na podstawowym poziomie, ładowanie tych danych będzie automatyczne. Zapewne domyśliliście się już, że te liczby to indexy tili ;) ale musimy je zaimportować odpowiednio. Otwórzmy nasz SceneEditor i w initialize, zaraz nad '@level = []' umieśćmy ten kod:

Kod: Zaznacz cały

		if FileTest.exists?('edytor/layer0.txt') and FileTest.exists?('edytor/layer1.txt') then
			
		else

		end
Nasz obecny kod wczytywania planszy (wypełniający zmienną pustymi wartościami) umieśćmy w części 'else' tego warunku. To sprawi, że tylko jeśli oba pliki (layer0 i layer1) istnieją, zostaną wczytane, a w innym przypadku - mapa zostanie wypełniona pustymi wartościami. W pustej części warunku (tam gdzie warunek jest spełniony) umieśćcie:

Kod: Zaznacz cały

parser = Parser.new
			@level = parser.parse_data(width,height)
Stwórzcie i importujcie nową klasę: Parser. Dajcie w niej tylko metodę 'parse_data(width,height)' ;) W niej potrzebujemy paru zmiennych:

Kod: Zaznacz cały

level = []
layer0_raw = File.read('edytor/layer0.txt')
layer1_raw = File.read('edytor/layer1.txt')
Pierwsza zmienna będzie zawierała (i zwracała) naszą planszę. Pozostałe dwie na ten moment wczytały surowe dane z pliku. Te surowe dane są stringiem, wszystkimi liczbami w jednym ciągu. To się nam na dużo nie przyda, więc musimy to w jakiś sposób przerobić. Ale jak?

Kod: Zaznacz cały

		layer0_data = layer0_raw.scan(/\d+/)
		layer1_data = layer1_raw.scan(/\d+/)

		layer0_data.collect! &:to_i
		layer1_data.collect! &:to_i
Pierwsza metoda skanuje naszą zmienną w poszukiwaniu liczb i zwraca je do osobnej zmiennej. Druga tworzy z nich tablicę. Dodanie '&:to_i' sprawia, że każda wartość w danej tablicy jest przy okazji przekształcana na Integer. Więc mamy większość tego, czego potrzebujemy: obie warstwy są zapisane jako tablice liczb (indeksów). Problem jest jedynie z tym, że są jednowymiarowe, a my potrzebujemy dwuwymiarowych tablic. Zróbmy więc coś takiego:

Kod: Zaznacz cały

level[0] = []
		level[1] = []
		for y in 0...height
			level[0][y] = []
			level[1][y] = []			
			for x in 0...width
				# Layer 0
				level[0][y][x] = layer0_data[y*width + x]

				# Layer 1
				level[1][y][x] = layer1_data[y*width + x]
			end
		end

		return level
Teraz powinno działać, ale jest duża szansa, że wystąpi nam błąd: 'undefined method `draw` for nil:NilClass (NoMethodError)'. Problem leży w naszych danych. Otwórzcie plik z naszą warstwą 0 i przejrzyjcie dokładnie liczby. W pewnym momencie możecie trafić na coś takiego:

Kod: Zaznacz cały

-1610612682
Albo podobna, równie duża liczba. Niestety nie wiem, co powoduje pojawienie się takich wartości, ale na pewno to nie jest poprawne. Dodatkowym problemem jest to, że przy przetwarzaniu danych ta wartość staje się wartością dodatnią. Możemy jednak przyjąć, że nasz tileset nie będzie większy niż 999 kratek. Ten kod:

Kod: Zaznacz cały

level[0][y][x] = 0 if level[0][y][x] > 999
(i analogiczny dla warstwy wyżej) zamieni daną wartość na 0 jeśli jest za duża. Teraz gdy odpalicie mapa powinna zostać wyświetlona, ale...
Obrazek
Wciąż jest coś nie tak. Ale co? Przyjrzyjcie się dobrze. Nasze tile są o jeden ID dalej, niż powinny być. To dlatego, że Tiled numeruje je od 1, a my od 0. Ale prosto można to naprawić:

Kod: Zaznacz cały

# Layer 0
				level[0][y][x] = layer0_data[y*width + x] - 1
				level[0][y][x] = 0 if level[0][y][x] > 999
				level[0][y][x] = 0 if level[0][y][x] < 0

				# Layer 1
				level[1][y][x] = layer1_data[y*width + x] - 1
				level[1][y][x] = 0 if level[0][y][x] > 999
				level[1][y][x] = 0 if level[0][y][x] < 0
Teraz będziemy przyjmować wartości o jeden mniejsze. A gdy któraś z wartości zejdzie poniżej 0, automatycznie ustawiamy ją na zero (tiled puste miejsca zapisuje jako 0, przy naszym zmniejszeniu indexów te wartości byłyby ujemne). Teraz gdy uruchomicie mapę wszystko powinno być tak, jak powinno.

Na dzisiaj to wszystko. To był tylko krótki, specjalny odcinek. W następnym wracamy do gry :)

Archiwum winrara z postępem dzisiejszej lekcji.
No matter how tender, how exquisite… A lie will remain a lie.
Awatar użytkownika
pakitos
Posty: 151
Rejestracja: 26 sie 2009, 08:15
Lokalizacja: kraina sera

Re: Robimy grę platformową w Ruby Gosu!

Post autor: pakitos »

Świetny poradnik, już dawno chciałem się nauczyć programować gry ale jakoś mi niespecjalnie wychodziło :) Czekam na kolejne części.
co
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 5: Kamera
Tak jak obiecałem, dzisiaj zajmiemy się dalszą pracą nad grą. Jeśli zrobiliście własną mapę, to możecie jej teraz używać. Jeśli nie, możecie użyć mojej:
Mapa
Plik mapy (wasz czy mój) wrzućcie do folderu '/data', nazwijcie go 'Map001.map'. Pierwszą rzeczą jaką musimy zrobić jest wczytanie informacji z mapy. Usuńcie wszystko z naszego 'SceneMap#initialize'. Pierwszą rzeczą jaką musimy zrobić to wczytać nasz plik. Posłuży nam do tego metoda File.open:

Kod: Zaznacz cały

file = File.open('data/Map001.map')
Plik od razu będzie miał przypisany atrybut 'r', czyli 'read'. Więcej nie potrzebujemy.
Kolejnym krokiem będzie wyciągnięcie informacji z pliku. Jak pamiętamy (a jak nie, możemy sprawdzić ;) ), dane zapisywaliśmy w kolejności '@used_tileset, @level i @objects'. W tej samej kolejności je odczytujemy. Po odczytaniu tych informacji wystarczy zamknąć plik.

Kod: Zaznacz cały

		file = File.open('data/Map001.map')
		@used_tileset = Marshal.load(file)
		@level = Marshal.load(file)
		@objects = Marshal.load(file)
		file.close
Mamy teraz trzy zmienne, z których tak naprawdę możemy użyć tylko '@level'. Najpierw wczytajmy tileset:

Kod: Zaznacz cały

@tileset = Image.load_tiles($window, "graphics/tiles/#{@used_tileset}", 16, 16, true)
Teraz musimy wczytać nasze objekty. Stwórzcie zmienną '@player', a następnie wywołajcie metodę 'load_entities'.

Kod: Zaznacz cały

		@player = nil
		load_entities
W naszej metodzie 'load_entities' będziemy musieli przejść przez całą zmienną '@objects' i dodawać odpowiednie rodzaje Istot. Do tego wykorzystamy pętlę 'for' i warunki 'switch'.

Kod: Zaznacz cały

	def load_entities
		for i in 0...@objects.size
			case @objects[i][0]
			when :player
				@player = Player.new(@objects[i][1], @objects[i][2])
			end
		end
	end
Nie jest to duża metoda, bo na razie mamy tylko Gracza ;) ale z czasem będzie się rozrastała. Kolejną różnicą jaką mamy jest fakt, że do '@level' doszedł trzeci wymiar, zawierający warstwy. Musimy więc nieco zmienić naszą metodę 'draw', a konkretnie część wyświetlającą poziom:

Kod: Zaznacz cały

		for l in 0...@level.size
			for y in 0...@level[l].size
				for x in 0...@level[l][y].size
					@tileset[@level[l][y][x]].draw(x*16,y*16,1)
				end
			end
		end
jak i metody 'wall?', 'solid_overhead?' i 'no_ground?', które to również wykorzystują (póki co) naszą zmienną '@level'. W przypadku tych trzech zamieńcie '@level[tile_y][tile_x]' na @level[0][tile_y][@tile_x]'. Sprawdzanie blokowania będzie się działo na jednej warstwie (na razie). Teraz powinniśmy bez problemu odpalić grę z naszą nową mapą:
Obrazek
Teraz musimy sprawić, żeby kamera podążała za naszą postacią. Będziemy potrzebowali dwóch zmiennych: '@camera_x' i '@camera_y'

Kod: Zaznacz cały

		@camera_x = 0
		@camera_y = 0
Na końcu update'a ustawimy dwa obliczenia zmiennych, które będą ustawiały naszą kamerę:

Kod: Zaznacz cały

		@camera_x = [[@player.get_x - 320, 0].max, @level[0][0].size * 16 - 640].min
    	@camera_y = [[@player.get_y - 240, 0].max, @level[0].size * 16 - 480].min
Przyznam się, że te obliczenia są podkradzione z jednego z przykładowych projektów w Gosu: CptnRuby. Funkcja którą ja zrobiłem sprawiała, że postać nieco skakała przy przesuwaniu się kamery, a ta jest bardzo płynna.

Kod: Zaznacz cały

@tileset[@level[l][y][x]].draw((x*16)-@camera_x,(y*16)-@camera_y,1)
Nasza postać też musi być rysowana w odpowiednich miejscach, więc przesyłamy wartości kamer za pomocą '@player.draw':

Kod: Zaznacz cały

@player.draw(@camera_x, @camera_y)
A samo 'Player#draw' zmieniamy na:

Kod: Zaznacz cały

	def draw(camera_x, camera_y, z=5)
		frame = milliseconds / 150 % @sprite.size
		@sprite[frame].draw(@real_x - camera_x, @real_y - camera_y, z)
	end
Teraz powinno wszystko działać. Dzisiaj nieco krótko, ale w sumie nie ma dużo więcej do zrobienia. W następnej lekcji zrobimy trochę poprawek w poruszaniu się postaci i prawdopodobnie podrasujemy nieco blokowanie. Może coś więcej ;)

Archiwum winrara z efektami dzisiejszej pracy
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 »

Rave pisze:No, to naprawiło padakę przy prawej ścianie, nie zauważyłem tego elsifa. Niestety teraz jak podejdę do lewej krawędzi ekranu i tam wcisnę obie strzałki na raz, to znowu jest padaka.
Możesz mi z tym pomóc?

Anyway, zabieram się za edytor i kamerkę.
Awatar użytkownika
X-Tech

Golden Forki 2009 - Pełne Wersje (miejsce 3)
Posty: 3268
Rejestracja: 22 lut 2008, 14:15

Re: Robimy grę platformową w Ruby Gosu!

Post autor: X-Tech »

Mnie tu bardziej interesują przykłady gier zrobionych w ruby, bo :

http://gafferongames.com/2009/01/11/rub ... velopment/
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 »

X-Tech pisze:Mnie tu bardziej interesują przykłady gier zrobionych w ruby, bo :

http://gafferongames.com/2009/01/11/rub ... velopment/
Platformówki nie mają jakichś dużych wymagań, więc Ruby spokojnie uciągnie. Większe projekty też mogą działać, ale raczej trzeba uważać. Np. mój projekt (ponad 5k linijek czystego kodu, ponad 6.8k łącznie i rośnie) cały czas utrzymuje się przy 58-60FPS, przynajmniej na moim komputerze. Oczywiście gdy silnik będzie ukończony, zrobię małe tech demo które wyślę kilku osobom żeby sprawdzić na różnych komputerach jak działa, ale to nie temat od tego ;)

W każdym bądź razie nasza gra nie będzie mocno rozbudowana, z dużą fizyką, 150 update'ami objektów na frame i renderowaniem pseudo-3D, więc fakt użycia tego języka nie będzie problemem.
No matter how tender, how exquisite… A lie will remain a lie.
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 »

Artykuł z 2009 roku, co w dziedzinie komputerów i programowania to szmat czasu.
Nawet w C++ możesz napisać skopany kod, który użre ci połowę FPSów.
Wszystko zależy głównie od twoich umiejętności, a programowanie w języku, w którym nie możesz sobie pozwolić na tak leniwe olewanie prostej optymalizacji "bo c++ mi załatwi sprawę wydajności" jest bardziej kształcące. :)
Awatar użytkownika
X-Tech

Golden Forki 2009 - Pełne Wersje (miejsce 3)
Posty: 3268
Rejestracja: 22 lut 2008, 14:15

Re: Robimy grę platformową w Ruby Gosu!

Post autor: X-Tech »

Nie zmienia to faktu, że przykładów gier nie daliście. W C++ można podać setki. Czy ktoś widział jakieś przyzwoite platformery w ruby ?
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 »

Prawda, nie daliśmy żadnego przykładu. Jest trochę ich na Gosu Showcase, zarówno platformówek jak i innego rodzaju gier, ale konkretnych przykładów Ci nie podam. I fakt, w C++ można dać ich setki. W Lua też. I w Javie, czy zrobione w Unity 3D. Z podaniem chociażby 50 platformówek w Ruby może być troszkę większy problem, bo, cytuję:
Ruby is *not* at all suitable for game development!
http://gafferongames.com/2009/01/11/rub ... velopment/
No matter how tender, how exquisite… A lie will remain a lie.
ODPOWIEDZ