Robimy grę platformową w Ruby Gosu!

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

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

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Rave » 10 sty 2014, 14:45

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: 405
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Carrigtwohill, IE

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 10 sty 2014, 15:26

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: 1942
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Rave » 10 sty 2014, 15:33

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: 405
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Carrigtwohill, IE

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 10 sty 2014, 17:00

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/18909657/Tsukuru_TutorialeRubyGosu/Tut4_Edytor/res/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[i].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: 405
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Carrigtwohill, IE

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 11 sty 2014, 19:29

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: 1942
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Rave » 12 sty 2014, 11:27

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: 405
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Carrigtwohill, IE

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 12 sty 2014, 18:04

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: 150
Rejestracja: 26 sie 2009, 08:15
Lokalizacja: kraina sera

Re: Robimy grę platformową w Ruby Gosu!

Postautor: pakitos » 12 sty 2014, 18:52

Ś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: 405
Rejestracja: 28 maja 2010, 10:12
Lokalizacja: Carrigtwohill, IE

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 13 sty 2014, 22:43

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: 1942
Rejestracja: 15 kwie 2009, 21:33
Lokalizacja: '; DROP TABLE 'Messages'

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Rave » 14 sty 2014, 16:48

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: 3112
Rejestracja: 22 lut 2008, 14:15

Re: Robimy grę platformową w Ruby Gosu!

Postautor: X-Tech » 14 sty 2014, 17:43

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

http://gafferongames.com/2009/01/11/rub ... velopment/
Niepotrzebne sztuczne cukry, hejty, zazdrości, wredności. Liczy się pochwała za dobrze wykonaną pracę - grę.

Czat ssie.

RM zawsze spoko.

Jestem tu dla siebie.Nie jaram się RPG itp. Szkoda czasu.
Liczy się akcja i intensywna rozgrywka. Inne gry mnie nie jarały i nie jarają. Prawda wylana.
Awatar użytkownika
Ekhart

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

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 14 sty 2014, 18:21

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)
Posty: 1666
Rejestracja: 11 lip 2009, 13:47
Lokalizacja: Wieluń

Re: Robimy grę platformową w Ruby Gosu!

Postautor: GameBoy » 14 sty 2014, 18:53

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. :)
http://www.rmxp.vot.pl -> kopia RMXP

To Twój projekt, więc, do jasnej cholery, weź wreszcie za niego choć odrobinę odpowiedzialności. - Kleo =)
Ponoć w jaskiniach koło rysunków ściennych przedstawiających mamuty można było zobaczyć "scena umiera". - Noruj =)
Awatar użytkownika
X-Tech

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

Re: Robimy grę platformową w Ruby Gosu!

Postautor: X-Tech » 14 sty 2014, 20:23

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 ?
Niepotrzebne sztuczne cukry, hejty, zazdrości, wredności. Liczy się pochwała za dobrze wykonaną pracę - grę.

Czat ssie.

RM zawsze spoko.

Jestem tu dla siebie.Nie jaram się RPG itp. Szkoda czasu.
Liczy się akcja i intensywna rozgrywka. Inne gry mnie nie jarały i nie jarają. Prawda wylana.
Awatar użytkownika
Ekhart

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

Re: Robimy grę platformową w Ruby Gosu!

Postautor: Ekhart » 14 sty 2014, 20:42

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.

Wróć do „Offtopic”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 6 gości