Difference between revisions of "Tutorial:Cameras (Polski)"

(Część 2. Wielowarstwowe przewijanie (parallax scrolling))
m (test change)
 
(5 intermediate revisions by the same user not shown)
Line 23: Line 23:
  
 
Na [b]informację o przekształceniu[/b], składają się trzy elementy: przesunięcie, skalowanie i obrót. W trakcie każdej operacji rysowania:
 
Na [b]informację o przekształceniu[/b], składają się trzy elementy: przesunięcie, skalowanie i obrót. W trakcie każdej operacji rysowania:
* argumenty x i y przekazywane do funkcji translate są dodawane do współrzędnych x i y,  
+
* argumenty x i y przekazywane do funkcji [[love.graphics.translate (Polski)|love.graphics.translate]]są dodawane do współrzędnych x i y,  
* obie współrzędne są mnożone przez argument funkcji scale
+
* obie współrzędne są mnożone przez argument funkcji [[love.graphics.scale (Polski)|love.graphics.scale]]
* każdy punkt jest obracany względem lewego górnego rogu o kąt podany jako argument do funkcji rotate (w radianach).
+
* każdy punkt jest obracany względem lewego górnego rogu o kąt podany jako argument do funkcji [[love.graphics.rotate (Polski)|love.graphics.rotate]](w radianach).
  
 
Mając te wiedzę możemy stworzyć naszą pierwszą kamerę
 
Mając te wiedzę możemy stworzyć naszą pierwszą kamerę
Line 47: Line 47:
 
</source>
 
</source>
  
zmienne ''-obrot'' i ''-x'' oraz ''-y'', będą mieć takie wartości, że obiekty, które nie poruszają się wraz z kamerą, ''zostaną w tyle''.
+
zmienne <code>-obrot</code> i <code>-x</code> oraz <code>-y</code>, będą mieć takie wartości, że obiekty, które nie poruszają się wraz z kamerą, ''zostaną w tyle''.
 
(Najczęściej kamera będzie podążać za graczem).
 
(Najczęściej kamera będzie podążać za graczem).
 
Wartości zmiennej skala powyżej 1 będą oddalały kamerę, podczas gdy mniejsze od 1, będą ją przybliżały.
 
Wartości zmiennej skala powyżej 1 będą oddalały kamerę, podczas gdy mniejsze od 1, będą ją przybliżały.
Line 55: Line 55:
 
Uporządkujmy kod do obsługi kamery w moduł
 
Uporządkujmy kod do obsługi kamery w moduł
 
<source lang="lua">
 
<source lang="lua">
camera = {}
+
local camera = {}
 
camera.x = 0
 
camera.x = 0
 
camera.y = 0
 
camera.y = 0
Line 97: Line 97:
 
   self.scaleY = sy or self.scaleY
 
   self.scaleY = sy or self.scaleY
 
end
 
end
 +
 +
return camera
 
</source>
 
</source>
  
Moduł kamery  jest tablicą, która zawiera informację gdzie ma być umiejscowiona kamera. Kluczowe są funkcje set i unset. <code>set</code> zmienia ''aktualne przekształcenie'', na informacje zawarte w module kamery (ale wcześniej zapisuje bieżący stan, dzięki czemu moduł kamery będzie w stanie obsługiwać kilka ''widokow'' w jednej ramce. <code>unset</code> po prostu przywraca poprzednie ustawienie, korzystając z wspomnianej funkcji pop. Pozostałe funkcje stanowią pomoc przy przemieszczaniu kamery.
+
Moduł kamery  jest tablicą, która zawiera informację gdzie ma być umiejscowiona kamera. Kluczowe są funkcje <code>set</code> i <code>unset</code>. <code>set</code> zmienia ''aktualne przekształcenie'', na informacje zawarte w module kamery (ale wcześniej zapisuje bieżący stan, dzięki czemu moduł kamery będzie w stanie obsługiwać kilka ''widokow'' w jednej ramce. <code>unset</code> po prostu przywraca poprzednie ustawienie, korzystając z wspomnianej funkcji pop. Pozostałe funkcje stanowią pomoc przy przemieszczaniu kamery.
  
 
Kamery można użyć poprzez następujący kod w callbacku [[love.draw (Polski)]]:
 
Kamery można użyć poprzez następujący kod w callbacku [[love.draw (Polski)]]:
Line 128: Line 130:
 
</source>
 
</source>
  
Funkcja ta koryguje pozycję myszy '''tylko''' względem przesunięcia i skalowania kamery. W przypadku obrotu, należy uwzględnić przemnożenie przez odpowiedni vector. Zobacz implementacje wektora bibliotece [http://vrld.github.io/hump/|HUMP], by zobaczyć jak to zrobić.
+
Funkcja ta koryguje pozycję myszy '''tylko''' względem przesunięcia i skalowania kamery. W przypadku obrotu, należy uwzględnić przemnożenie przez odpowiedni vector. Zobacz implementacje wektora bibliotece [http://vrld.github.io/hump/ HUMP], by zobaczyć jak to zrobić.
  
  
Line 140: Line 142:
  
 
Technika którą wykorzystamy by osiągnąć ten efekt nazywana jest ''metodą warstwową'' (tłum: stąd bardziej właściwy tytuł części: Przewijanie wielowarstwowe). To co musimy zrobić, to wyrenderować kilka warstw grafik, na które ruch kamery ma wpływ w różnym stopniu. Normalnie na ''pole gry'' (tam gdzie znajduje się gracz i gdzie będzie odbywać się cała rozgrywka) ruch kamery będzie miał wpływ w stosunku 1:1 (czyli teren PO którym gracz chodzi i sam gracz będzie się poruszać z taką prędkością z jaką porusza się kamera). Warstwy ''bliżej'' kamery niż ''pola gracza'', będą poruszać się szybciej niż kamera, natomias warstwy ''dalej'' niż kamera, będą poruszać się welniej (góry w tle, chmury itp.)
 
Technika którą wykorzystamy by osiągnąć ten efekt nazywana jest ''metodą warstwową'' (tłum: stąd bardziej właściwy tytuł części: Przewijanie wielowarstwowe). To co musimy zrobić, to wyrenderować kilka warstw grafik, na które ruch kamery ma wpływ w różnym stopniu. Normalnie na ''pole gry'' (tam gdzie znajduje się gracz i gdzie będzie odbywać się cała rozgrywka) ruch kamery będzie miał wpływ w stosunku 1:1 (czyli teren PO którym gracz chodzi i sam gracz będzie się poruszać z taką prędkością z jaką porusza się kamera). Warstwy ''bliżej'' kamery niż ''pola gracza'', będą poruszać się szybciej niż kamera, natomias warstwy ''dalej'' niż kamera, będą poruszać się welniej (góry w tle, chmury itp.)
 +
 +
=== Implementacja ===
 +
 +
Dosyć gadania, czas przystąpić do dzieła. By zaimplementować przewijanie stworzymy dość surowy system warstw, który będzie zarządzany przez kamerę. Oto modyfikacje oryginalnego modułu kamery, jakie musimy zrobić. Na początek, musimy dodać tablicę, która będzie przechowywać warstwy.
 +
 +
<source lang="lua">
 +
camera.layers = {}
 +
</source>
 +
Następnie musimy dodać metodę, która pozwoli na dodawanie nowych warstw:
 +
 +
<source lang="lua">
 +
function camera:newLayer(skala, funkcjaRysujaca)
 +
  table.insert(self.layers, { draw = funkcjaRysujaca, scale = skala })
 +
  table.sort(self.layers, function(a, b) return a.scale < b.scale end)
 +
end
 +
 +
function camera:resetLayers()
 +
  camera.layers = {}
 +
end
 +
</source>
 +
<code>skala</code> to wartość przez którą zostaną przemnożone współrzędne kamery przed narysowaniem warstwy. <code>funkcjaRysujaca</code> to funkcja, która będzie odpowiedzialna za rysowanie  obiektów w danej warstwie. Musimy również trzymać warstwy posortowane względem skali, tak by te położone dalej (w tle) rysować zanim narysujemy te położone bliżej.
 +
 +
(tłum: niektóre frameworki obsługują 'układanie', w jakiej kolejności ma następować rysowanie, w LÖVE istotna jest kolejność rysowania)
 +
 +
Następnie dodamy taką metodę:
 +
<source lang="lua">
 +
function camera:draw()
 +
  local bx, by = self.x, self.y
 +
 
 +
  for _, v in ipairs(self.layers) do
 +
    self.x = bx * v.scale
 +
    self.y = by * v.scale
 +
    camera:set()
 +
    v.draw()
 +
    camera:unset()
 +
  end
 +
end
 +
</source>
 +
 +
Funkcja ta zajmuje się rysowaniem wszystkich warstw. <code>bx</code>,<code>by</code> to "bazowe" współrzędne oryginalnej pozycji kamery. Zwróć uwagę, że przed każdym wywołaniem <code>camera:set</code> i funkcji rysującej (<code>v.draw()</code>), mnożymy bazowe współrzędne kamery przez skalę danej warstwy, dopiero potem przystępujemy do rysowania.
 +
 +
Nasz kod w callbacku LÖVE  odpowiedzialnym za rysowanie, będzie bardzo prosty:
 +
<source lang="lua">
 +
function love.draw()
 +
  camera:draw()
 +
end
 +
</source>
 +
 +
=== Szybki przykład ===
 +
Przygotowałem szybki przykład, by zaprezentować działanie '''przewijania paralektycznego''' w praktyce.
 +
 +
Pełny kod modułu kamery znajdziesz [[Tutorial:Cameras:camera (Polski)|tutaj]].
 +
 +
<source lang="lua">
 +
local camera = require('camera')
 +
 +
math.randomseed(os.time())
 +
math.random()
 +
math.random()
 +
math.random()
 +
 +
function love.load(args)
 +
  -- stworzymy 6 warstw
 +
  -- indeks i, będzie użyty jako skala warstwy
 +
  for i = .5, 3, .5 do
 +
    local prostokaty = {}
 +
   
 +
    -- w każdej warstwie będą prostokąty o losowym położeniu
 +
    -- i losowym kolorze
 +
    for j = 1, math.random(2, 15) do
 +
      table.insert(prostokaty, {
 +
        math.random(0, 1600),
 +
        math.random(0, 1600),
 +
        math.random(50, 400),
 +
        math.random(50, 400),
 +
        color = { math.random(0, 255), math.random(0, 255), math.random(0, 255) }
 +
      })
 +
    end
 +
   
 +
    -- dodajemy warstwę
 +
    camera:newLayer(i, function()
 +
      -- funkcja rysująca warstwę jest bardzo prosta
 +
      -- rysuje ona wszystkie prostokąty należące do danej warstwy
 +
      for _, v in ipairs(prostokaty) do
 +
        love.graphics.setColor(v.color)
 +
        love.graphics.rectangle('fill', unpack(v))
 +
        love.graphics.setColor(255, 255, 255)
 +
      end
 +
    end)
 +
  end
 +
end
 +
 +
function love.update(dt)
 +
  camera:setPosition(love.mouse.getX() * 2, love.mouse.getY() * 2)
 +
end
 +
 +
function love.draw()
 +
  camera:draw()
 +
  love.graphics.print("FPS: " .. love.timer.getFPS(), 2, 2)
 +
end
 +
 +
function love.keypressed(klawisz, unicode)
 +
  if klawisz == " " then
 +
    camera:resetLayers()
 +
    love.load()
 +
  elseif klawisz == 'escape' then
 +
    love.event.push('q')
 +
  end
 +
end
 +
</source>
 +
 +
Powyższy kod tworzy 6 warstw, zaczynając od skali 0.5 do 3. Tablica <code>prostokaty</code> zawiera informacje o losowych prostokątach należących do danej warstwy. Nie potrzebujemy zmiennej glogalnej, ponieważ funkcja korzystająca z tej tablicy jest w tym samym bloku. Funkcja rysująca daną warstwę, po prostu rysuje prostokąty należące do niej.
 +
 +
Poruszanie myszą będzie zmieniać pozycję kamery. Możesz zresetować wszystko naciskając spację. (dlatego wywołujemy <code>camera:resetLayers</code> przed ręcznym wywołaniem <code>love.load</code>. Wciśnięcie Escape, zakończy aplikację.
 +
 +
Uwaga na koniec, na samym początku, wołamy <code>math.random</code> trzy razy, ponieważ w pierwszych wywołaniach, math.random, może zwracać nie do końca losowe liczby. Sam tego nigdy nie doświadczyłem, ale te dodatkowe wywołania, nic w tym przypadku nie kosztują.
  
 
== Część 3. Ograniczenia ruchu ==
 
== Część 3. Ograniczenia ruchu ==
Line 148: Line 266:
 
{{i18n|Tutorial:Cameras}}
 
{{i18n|Tutorial:Cameras}}
  
[[Category:Tutorials (Polski)]]
+
[[Category:Tutorials_(Polski)]]

Latest revision as of 19:01, 9 December 2013

Ten poradnik jestłumaczeniem artykułów "Cameras in Love2D" napisanym przez BlackBulletIV.

Oryginalne artykuły (po angielsku):


Część 1. Podstawy

This part will deal with the fundamentals of creating a camera. Part two will deal with parallax scrolling and creating layers. So, let's get to it!

W pierwszej części zajmiemy się podstawami tworzenia kamer.

Funkcje z których będziemy korzystać

LÖVE posiada stos przekształceń (geometrycznych), a dokładniej stos [b]informacji o danym przekształczeniu[/b]. love.graphics.push pozwala odłożyć aktualne przekształcenie na stos (pozwala to zapamiętać przekształcenie na później), natomiast love.graphics.pop zdejmuje przekształcenie ze stosu (nadpisując tym samym aktualne przewształceni, tym zdjętym przed chwilą ze stosu).

Na [b]informację o przekształceniu[/b], składają się trzy elementy: przesunięcie, skalowanie i obrót. W trakcie każdej operacji rysowania:

Mając te wiedzę możemy stworzyć naszą pierwszą kamerę

function love.draw()
  love.graphics.translate(-x, -y)
  -- właściwe rysowanie poniżej
end

Tutaj funkcja translate modyfikuje aktualne przekształcenie. Stos intormacji o przekształceniach (jak również aktualne przekształcenie) jest resetowany z każdą ramką, więc w każdej ramce, będziemy ustawiać kamerę.

Bardziej kompletna implementacja kamery wygląda następująco:

function love.draw()
  love.graphics.rotate(-obrot) -- w radianach
  love.graphics.scale(1 / skala, 1 / skala)
  love.graphics.translate(-x, -y)
end

zmienne -obrot i -x oraz -y, będą mieć takie wartości, że obiekty, które nie poruszają się wraz z kamerą, zostaną w tyle. (Najczęściej kamera będzie podążać za graczem). Wartości zmiennej skala powyżej 1 będą oddalały kamerę, podczas gdy mniejsze od 1, będą ją przybliżały.

moduł kamery

Uporządkujmy kod do obsługi kamery w moduł

local camera = {}
camera.x = 0
camera.y = 0
camera.scaleX = 1 
camera.scaleY = 1
camera.rotation = 0 

function camera:set()
  love.graphics.push()
  love.graphics.rotate(-self.rotation)
  love.graphics.scale(1 / self.scaleX, 1 / self.scaleY)
  love.graphics.translate(-self.x, -self.y)
end

function camera:unset()
  love.graphics.pop()
end

function camera:move(dx, dy) -- przesuniecie kamery o deltę
  self.x = self.x + (dx or 0)
  self.y = self.y + (dy or 0)
end

function camera:rotate(dr) -- obrot kamery o deltę
  self.rotation = self.rotation + dr
end

function camera:scale(sx, sy) -- zmiana skali (względna)
  sx = sx or 1
  self.scaleX = self.scaleX * sx
  self.scaleY = self.scaleY * (sy or sx)
end

function camera:setPosition(x, y) -- zmiana pozycji absolutnej kamery
  self.x = x or self.x
  self.y = y or self.y
end

function camera:setScale(sx, sy) -- zmiana skali (bezwzględnda)
  self.scaleX = sx or self.scaleX
  self.scaleY = sy or self.scaleY
end

return camera

Moduł kamery jest tablicą, która zawiera informację gdzie ma być umiejscowiona kamera. Kluczowe są funkcje set i unset. set zmienia aktualne przekształcenie, na informacje zawarte w module kamery (ale wcześniej zapisuje bieżący stan, dzięki czemu moduł kamery będzie w stanie obsługiwać kilka widokow w jednej ramce. unset po prostu przywraca poprzednie ustawienie, korzystając z wspomnianej funkcji pop. Pozostałe funkcje stanowią pomoc przy przemieszczaniu kamery.

Kamery można użyć poprzez następujący kod w callbacku love.draw (Polski):

function love.draw()
  camera:set() -- zaapliwowanie ustawień kamery

  -- właściwe rysowanie

  camera:unset()
end

Natomiast w callbacku love.update (Polski), można zmieniać położenie kamery.

camera.x = 50
camera:scale(3)

mysz

Jeśli chcesz znać pozycję myszy w świecie (czyli względem kamery), będzie trzeba odpowiednio przekształcić współrzędne zwracane przez love.mouse.getX/love.mouse.getY. Mając gotowy moduł kamery, można to prosto zrobić dodając do niego następującą funkcję.

function camera:mousePosition()
  -- przelicza pozycję myszy na ekranie, na współrzędne 'w świecie gry'
  return love.mouse.getX() * self.scaleX + self.x, love.mouse.getY() * self.scaleY + self.y
end

Funkcja ta koryguje pozycję myszy tylko względem przesunięcia i skalowania kamery. W przypadku obrotu, należy uwzględnić przemnożenie przez odpowiedni vector. Zobacz implementacje wektora bibliotece HUMP, by zobaczyć jak to zrobić.


Część 2. Wielowarstwowe przewijanie (parallax scrolling)

"W szczególności paralaksa odnosi się do jednoczesnego obserwowania obiektów leżących w różnych odległościach od obserwatora lub urządzenia obserwującego," - Wikipedia, paralaksa

W efekcie czego, przy poruszaniu się, odnosi się wrażenie, że obiekty położone dalej, poruszają się wolniej.

W tej części zajmiemy się przewijaniem paralakatycznym. Użyta metoda, nie jest najpiękniejsza, ma jedynie stanowić podstawę, dla prób na własną rękę.

Czym jest przewijanie paralakatyczne? To sposób na uzyskanie efektu pseudo-3d, podczas gay cała gra i grafiki są oparet na 2d. To popularna metoda w grach 2d, na dodanie wrażenia głębi, chociaż de facto jej tam nie ma.

Technika którą wykorzystamy by osiągnąć ten efekt nazywana jest metodą warstwową (tłum: stąd bardziej właściwy tytuł części: Przewijanie wielowarstwowe). To co musimy zrobić, to wyrenderować kilka warstw grafik, na które ruch kamery ma wpływ w różnym stopniu. Normalnie na pole gry (tam gdzie znajduje się gracz i gdzie będzie odbywać się cała rozgrywka) ruch kamery będzie miał wpływ w stosunku 1:1 (czyli teren PO którym gracz chodzi i sam gracz będzie się poruszać z taką prędkością z jaką porusza się kamera). Warstwy bliżej kamery niż pola gracza, będą poruszać się szybciej niż kamera, natomias warstwy dalej niż kamera, będą poruszać się welniej (góry w tle, chmury itp.)

Implementacja

Dosyć gadania, czas przystąpić do dzieła. By zaimplementować przewijanie stworzymy dość surowy system warstw, który będzie zarządzany przez kamerę. Oto modyfikacje oryginalnego modułu kamery, jakie musimy zrobić. Na początek, musimy dodać tablicę, która będzie przechowywać warstwy.

camera.layers = {}

Następnie musimy dodać metodę, która pozwoli na dodawanie nowych warstw:

function camera:newLayer(skala, funkcjaRysujaca)
  table.insert(self.layers, { draw = funkcjaRysujaca, scale = skala })
  table.sort(self.layers, function(a, b) return a.scale < b.scale end)
end

function camera:resetLayers()
  camera.layers = {}
end

skala to wartość przez którą zostaną przemnożone współrzędne kamery przed narysowaniem warstwy. funkcjaRysujaca to funkcja, która będzie odpowiedzialna za rysowanie obiektów w danej warstwie. Musimy również trzymać warstwy posortowane względem skali, tak by te położone dalej (w tle) rysować zanim narysujemy te położone bliżej.

(tłum: niektóre frameworki obsługują 'układanie', w jakiej kolejności ma następować rysowanie, w LÖVE istotna jest kolejność rysowania)

Następnie dodamy taką metodę:

function camera:draw()
  local bx, by = self.x, self.y
  
  for _, v in ipairs(self.layers) do
    self.x = bx * v.scale
    self.y = by * v.scale
    camera:set()
    v.draw()
    camera:unset()
  end
end

Funkcja ta zajmuje się rysowaniem wszystkich warstw. bx,by to "bazowe" współrzędne oryginalnej pozycji kamery. Zwróć uwagę, że przed każdym wywołaniem camera:set i funkcji rysującej (v.draw()), mnożymy bazowe współrzędne kamery przez skalę danej warstwy, dopiero potem przystępujemy do rysowania.

Nasz kod w callbacku LÖVE odpowiedzialnym za rysowanie, będzie bardzo prosty:

function love.draw()
  camera:draw()
end

Szybki przykład

Przygotowałem szybki przykład, by zaprezentować działanie przewijania paralektycznego w praktyce.

Pełny kod modułu kamery znajdziesz tutaj.

local camera = require('camera')

math.randomseed(os.time())
math.random()
math.random()
math.random()

function love.load(args)
  -- stworzymy 6 warstw
  -- indeks i, będzie użyty jako skala warstwy
  for i = .5, 3, .5 do
    local prostokaty = {}
    
    -- w każdej warstwie będą prostokąty o losowym położeniu
    -- i losowym kolorze
    for j = 1, math.random(2, 15) do
      table.insert(prostokaty, {
        math.random(0, 1600),
        math.random(0, 1600),
        math.random(50, 400),
        math.random(50, 400),
        color = { math.random(0, 255), math.random(0, 255), math.random(0, 255) }
      })
    end
    
    -- dodajemy warstwę
    camera:newLayer(i, function()
      -- funkcja rysująca warstwę jest bardzo prosta
      -- rysuje ona wszystkie prostokąty należące do danej warstwy
      for _, v in ipairs(prostokaty) do
        love.graphics.setColor(v.color)
        love.graphics.rectangle('fill', unpack(v))
        love.graphics.setColor(255, 255, 255)
      end
    end)
  end
end

function love.update(dt)
  camera:setPosition(love.mouse.getX() * 2, love.mouse.getY() * 2)
end

function love.draw()
  camera:draw()
  love.graphics.print("FPS: " .. love.timer.getFPS(), 2, 2)
end

function love.keypressed(klawisz, unicode)
  if klawisz == " " then
    camera:resetLayers()
    love.load()
  elseif klawisz == 'escape' then
    love.event.push('q')
  end
end

Powyższy kod tworzy 6 warstw, zaczynając od skali 0.5 do 3. Tablica prostokaty zawiera informacje o losowych prostokątach należących do danej warstwy. Nie potrzebujemy zmiennej glogalnej, ponieważ funkcja korzystająca z tej tablicy jest w tym samym bloku. Funkcja rysująca daną warstwę, po prostu rysuje prostokąty należące do niej.

Poruszanie myszą będzie zmieniać pozycję kamery. Możesz zresetować wszystko naciskając spację. (dlatego wywołujemy camera:resetLayers przed ręcznym wywołaniem love.load. Wciśnięcie Escape, zakończy aplikację.

Uwaga na koniec, na samym początku, wołamy math.random trzy razy, ponieważ w pierwszych wywołaniach, math.random, może zwracać nie do końca losowe liczby. Sam tego nigdy nie doświadczyłem, ale te dodatkowe wywołania, nic w tym przypadku nie kosztują.

Część 3. Ograniczenia ruchu