Panel Logowania

Sterowanie & kamera rodem z RPG

napisał : gumen
22
września
2012
Dokładne omówienie skryptów kontrolujących sterowanie graczem i pracę kamery. Ślizganie się i przenikanie przez ściany nie będzie stanowić więcej problemów.
tagi : rpg camera collision kolizja kamery blender ge gumen kontroler postaci

1.Wstęp

Jak wiadomo, standardowe opcje odpowiedzialne za poruszanie się obiektów w BGE nie dają możliwości łatwego skonfigurowania gracza i kamery. Albo gracz będzie się dobrze poruszał i przenikał przez ściany, albo będzie kolidował z obiektami i ślizgał się jak na lodowisku. Konieczne jest zatem napisanie skryptu, zarówno dla gracza jak i dla kamery. Daję wam tutaj nowiutki plik blendera (2.62), w którym znajdziecie przykładową scenę demonstrującą działanie moich skryptów (link na dole). Tutorial poświęcony tylko kolizji kamery można znaleźć na moim kanale YouTube (http://youtu.be/2lZKWdp7IS8).

DEMO: http://www.youtube.com/watch?v=xX0a-FRpAp4 w opisie lista możliwości.

Na początku chciałem zwrócić uwagę na jeden istotny element. Dla całkowicie poprawnego działania skryptu kontrolującego gracza, konieczne jest ustawienie grawitacji w scenie na 0. Daje to duże możliwości swobodnego kontrolowania grawitacji każdego obiektu. Niezależnie jaką grawitację sobie wymyślimy (kąt nachylenia, moc) może ona działać dla każdego obiektu inaczej. Skrypt gracza przygotowany jest tak, że możemy obracać postacią i wszystko zawsze będzie działało. Przykładowo, obracamy postać o 180 stopni i od teraz chodzimy po suficie mapy. Można zastosować dowolny kąt obrotu, lub zmieniać go dynamicznie w trakcie gry. Takie rozwiązanie pojawiło się już w kilku grach np.: Prey. Niestety zmusza nas to do ciągłego, ręcznego kontrolowania 'sztucznej grawitacji' dla innych obiektów. Do tego posłuży trzeci skrypt. Jest on odpowiedzialny za operacje na grupach obiektów. Przykładowo, dla poprawnego działania algorytmu kolizji kamery jak i skryptu postaci, każdy obiekt, który ma kolidować z graczem i kamerą, musi posiadać parametr 'collision'. Dodanie go ręcznie może być bardzo czasochłonne. Nasz skrypt zrobi to za nas. Zastosowałem w nim dwie metody grupowania obiektów, które mogą się przydać w najróżniejszych sytuacjach.

2.World

Zaczniemy od skryptu world.py, który jest niezbędny dla poprawnego działania pozostałych dwóch skryptów. Jest on uruchamiany przez obiekt empty o nazwie 'world'. Tak naprawdę może to być dowolny obiekt np.: taki, który ma już sensor 'Always' z włączonym 'TRUE level triggering'.

Wszystkie obiekty, na których będziemy operować w skrypcie world.py muszą mieć zaznaczoną opcję Actor w zakładce Physics.  Niezależnie czy są to obiekty statyczne, czy dynamiczne. Bez tego nie będą wykrywane przez sensor Ray w obiekcie player.

Opiszę znaczenie każdej linii, w takiej kolejności, w jakiej są one wywoływane.

03. from bge import logic, types

Importujemy z bge tylko logic i types. Dobrym nawykiem jest importowanie tylko tego co naprawdę jest potrzebne, zamiast import bge.

30. def main(cont):
31.
32. own = cont.owner
33.
34. if 'init' not in own:
35. own['init'] = True
36. own = World(own)

Przechodzimy do funkcji main zgodnie z tym co napisaliśmy w kontrolerze. Podajemy w nawiasie nazwę zmiennej, do której automatycznie przypisane jest logic.getCurrentController(). Następnie używamy tego aby przypisać właściciela skryptu do zmiennej own. W kolejnych liniach kodu występuje warunek. Sprawdzamy czy nasz właściciel (own) posiada w sobie parametr init. Oczywiście nie posiada bo go mu nigdzie nie daliśmy, dlatego zostaną wykonane następne dwie operacje. W 35. linii dodajemy obiektowi parametr init i ustawiamy go na True. Co wrzucimy do init nie ma znaczenia. Może to być liczba, tekst; najważniejsze aby obiekt posiadał ten parametr. Dzięki temu te dwie instrukcje zostaną wykonane tylko raz przy pierwszym uruchomieniu skryptu. W następnej linii tworzymy nowy obiekt z naszego właściciela (own) wykorzystując klasę World. Teraz zmienna own prezentuje obiekt World, który dziedziczy klasę KX_GameObject (o ty za chwilę). W momencie tworzenia obiektu wykonywane są pierwszy raz instrukcje od 5. do 27. linii. 

05. class World(types.KX_GameObject):

Definiujemy nową klasę World. W nawiasie podajemy od jakiego obiektu dziedziczymy. Jeśli obiekt, na którym będziemy pracować, aha, no właśnie obiekt :) już jest obiektem i został stworzony z innej klasy to musimy dziedziczyć od tego obiektu. Jeśli nie wpisalibyśmy tego w nawiasy, to nasz empty (w tym przypadku, bo gdyby skryptu użyć w kamerze to wpisujemy types.KX_Camera) nie byłby dłużej obiektem empty. Nie posiadał by nazwy, położenia w przestrzeni 3D itd. Więc automatycznie posypałyby się błędy w konsoli.

07.     def __init__(self, own):

Następnie definiujemy pierwszą metodę. __init__ to jedna ze standardowych metod (tak zwany konstruktor klasy, chociaż z tego co wiem, nim nie jest), która wywoływana jest tylko raz podczas tworzenia obiektu. Tak więc zrobimy w niej wszystko co nie wymaga powtarzania. Pierwszy argument metody to zawsze self. (odwołanie do obiektu klasy), a drugi own, jeśli dziedziczymy z KX_GameObject.

09.         scene = logic.getCurrentScene()
10.         collision_objects = scene.objects['collision_objects']
11. 
12.         self.rigid_body_objects = []
13.         self.gravity_force = 500

Następnie tworzymy 4 zmienne. Pierwsze dwie nie będą potem wykorzystywane w klasie, ani modyfikowane, więc nie dodajemy ich do obiektu tak jak to zrobiłem z kolejnymi dwoma zmiennymi. Czyli wrzucamy sobie scenę do zmiennej scene, żeby pobrać obiekt o nazwie collision_objects. Jest to obiekt empty, do którego przyparentowałem wszystkie statyczne elementy mapy. Specjalnie podzieliłem mapę na tak wiele elementów aby pokazać, że można łatwo pracować na wszystkich jednocześnie. Ale o ty za chwilę. W linii 12. tworzę pustą listę, do które potem wrzucę wszystkie obiekty dynamiczne, które pogrupowałem w inny sposób. Następnie zapisujemy w self.gravity_force wartość, którą użyjemy jako mocy grawitacji dla obiektów dynamicznych.

15.         for i in collision_objects.children:
16.             i['collision'] = True

Pierwsza pętla for. Wykona się dokładnie tyle razy ile mamy przyparentowanych obiektów do empty collision_objects. Jednocześnie za każdym razem gdy pętla się wykona dostaniemy w zmiennej i jeden z przyparentowanych obiektów. Dodajemy do każdego obiektu parametr collision przechowujący True. Może to być cokolwiek, liczba, tekst. Gdy pętla się zakończy będziemy mieli parametr collision w każdym elemencie mapy.

18.         for i in scene.objects:
19.             if i.name[0] == 'r':
20.                 i['collision'] = True
21.                 self.rigid_body_objects.append(i)

Kolejna pętla przeszukuje każdy obiekt na scenie. Jeśli pierwsza litera nazwy tego obiektu to r, wtedy do tego obiektu dodawany jest parametr collision oraz sam obiekt jest dodawany do wcześniej utworzonej (wtedy pustej) listy obiektów self.rigid_body_objects[]. Bardzo dobra metoda, ale sugeruję używania wyrazów zamiast liter.

38.     own.gravity()

To koniec pierwszej metody. Już nie będziemy jej wywoływać. Teraz wracamy do 38. linii. Mamy utworzony obiekt, który wykonał już pewne operacje i posiada pewne dane. Wychodzimy z if'a, do którego nie będziemy już wracać. Wywołujemy drugą metodę obiektu, która będzie już wykonywana cały czas.

24.     def gravity(self):
25. 
26.         for i in self.rigid_body_objects:
27.             i.applyForce((0,0,-self.gravity_force), False)

Definiujemy metodę gravity, której pierwszym parametrem jest self. Sama metoda wykonuje tylko jedną pętlę. Tyle razy ile jest obiektów w wcześniej utworzonej liście. Za każdym razem, kolejnemu obiektowi dodaje moc grawitacji. I tak nieustannie.

3.Player

Teraz gdy nasz ul jest gotowy, możemy nauczyć Gucia latać. Skrypt gracza jest znacznie dłuższy i trochę bardziej skomplikowany. Zaczniemy od ustawień obiektów, które składają się na gracza.

Obiekt bee to nasz gracz, w którym uruchamiamy skrypt player.py. Do obiektu bee przyparentowany jest obiekt empty cameraTarget. Znajduje się on dokładnie nad graczem. Dodatkowo przyparentowałem do gracza obiekt wings. Nie ma on jednak większego znaczenia dla skryptu. Chce podkreślić, że w scenie znajduje się kamera, ale nie jest ona przyparentowana do żadnego obiektu.

Obiekt cameraTarget jest po części używany przez skrypt gracza i przez skrypt kamery. Dla poprawnego działania skryptu player.py nie ma potrzeby dodawać czegokolwiek do obiektu empty. Jedyne co zmieniłem w obiekcie empty to typ wyświetlania (Display) na Arrows. Takie ustawienie bardzo ułatwia programowanie.

Gracz posiada jeden parametr jump typu Time. Pierwszy sensor Always z TRUE level triggering. Drugi Mouse, w którym zostawiamy standardowe ustawienia. Trzeci sensor, Ray został tak ustawiany aby swoim zasięgiem (Distance) sięgam trochę pod gracza. Oś ustawiona na -Z kieruje sensor w stronę ziemi. Będzie on poszukiwał parametru collision. Ustawienie opcji Angle, w praktyce, ma wpływ na to z jak stromego zbocza gracz będzie się ześlizgiwał. Wszystkie sensory połączone są z kontrolerem Python, w którym uruchamiamy funkcję main skryptu player.py. Physic Type obiektu bee ustawiony na Dynamic z zaznaczonym Actor. Mass na 70, ale to jest zależne od późniejszych ustawień mocy grawitacji (w tym przypadku będzie to -10000). Reszta opcji w tej zakładce jest nieistotna. Zaznaczamy Collision Bounds i wybieramy Capsule. To wszystkie ustawienia potrzebne do poprawnego działania skryptu.

03. from bge import types, logic, events, render
04. from mathutils import Vector, Matrix
05. from math import radians

Z bge importujemy types (żeby móc dziedziczyć z obiektów), logic (do pobierania sceny), events (klawiatura) i rander (do pobierania informacji o naszym wyświetlaczu). Następnie z mathutils importujemy Vector i Matrix do operacji na Blenderowych typach zmiennych. Na koniec z math importujemy radians do zamiany stopni na radiany.

84. def main(cont):
85. 
86. own = cont.owner
87. 
88. if "init" not in own:
89. own['init'] = True
90. own = Player(own)
91.
92. own.movement()
93. own.mouse_look()
94. own.jumping()

Funkcja main jest niemal identyczna jak w skrypcie world.py. Warunek jest wykonywany tylko raz, gdzie tworzony jest nowy obiekt z klasy Player. A następnie wykonywane są trzy metody obiektu i tak nieustannie. Przejdźmy więc do klasy Player.

14. class Player(types.KX_GameObject):
15.
16.     def __init__(self, own):
17.
18.         self.cameraTarget = self.children['cameraTarget']
19.         self.mouse = self.sensors["Mouse"]
20.         self.radar = self.sensors["Radar"]
21.
22.         self.speed = 15
23.         self.gravity = -10000
24.         self.rotSpeed = (0.005, 0.005)
25.         self.jump = False
26.         self.jump_speed = 20
27.         self.jump_time = 0.15

Zaczynamy od zdefiniowania klasy Player dziedziczą z obiektu types.KX_GameObject. Następnie definiujemy metodę __init__ (konstruktor klasy), w którym zdefiniujemy wszystkie niezbędne zmienne. W atrybutach podajemy, jak zawsze w klasach, self i own (bo dziedziczyliśmy z obiektu KX_GameObject). Dalej mamy same zmienne, których będziemy używać później. Nie ma tu nic odkrywczego, ale żeby nie było problemów opisze krótko każda z nich. Mamy self.cameraTarget czyli nasz obiekt empty przyparentowany do gracza. Właśnie na tym przykładzie widać, że dziedziczyliśmy z obiektu KX_GameObject. Dalej self.mouse czyli sensor Mouse i self.rada czyli senor Radar. Następne linie kodu przedstawiają prędkość poruszania się gracza, moc grawitacji jaka na niego działa, prędkość obrotu (prawo-lewo, góra-dół). Trzy ostatnie zmienne wykorzystywane są w metodzie jumping. pierwsza zawiera informacje o tym czy gracz jest w trakcie skoku, druga to moc skoku, a trzecia to czas trwania wybicia się. Na tym kończy się metoda __init__.

29.     def movement(self):
30. 
31.         k = keyDown

Następnie wywołujemy metodę movement, która odpowiedzialna jest za poruszanie się graczem. Zaczynamy jak zwykle od zdefiniowania metody i podania w pierwszym argumencie self. Następnie przypisuję zmiennej k funkcję keyDown.

07. def keyDown(key, status = logic.KX_INPUT_ACTIVE):
08.
09. if logic.keyboard.events[key] == status:
10. return True
11. return False

Ta funkcja jest w całości przepisana z pewnego skryptu napisanego przez Gorana (http://nilunder.com). Pobiera ona dwa parametry. Pierwszy jest wymagany i jest to wciskany klawisz. Drugi to status, który ustawiony jest na logic.KX_INPUT_ACTIVE. Możemy oczywiście zmienić status podając drugi parametr w momencie wywołania funkcji. Działanie funkcji polega na sprawdzaniu czy podany klawisz (w przypadku KX_INPUT_ACTIVE) jest wciśnięty. Jeśli tak to funkcja zwraca prawdę czyli 1, jeśli nie to zwraca fałsz czyli 0.

35.         back_forward = k(events.WKEY) - k(events.SKEY)
36.         left_right = k(events.DKEY) - k(events.AKEY)
37.         speed = self.speed

Z powrotem w metodzie movement. Zmienna back_forward zawiera różnicę wyników funkcji keyDown. Pierwsza wartość przyjmuje 1 jeśli wciśnięty jest klawisz W, jeśli nie to 0. Druga tak samo tylko dla klawisza S. Czyli wynikiem operacji może być albo 1, albo 0, albo -1. Podsumowując, zmienna back_forward przyjmuje 1(jeśli został wciśnięty tylko klawisz W), -1(jeśli został wciśnięty tylko klawisz S) lub 0 (jeśli nie został wciśnięty żaden klawisz, lub zostały wciśnięte oba na raz). Dokładnie tak samo działa następna zmienna, tylko dla klawiszy A i D. Następnie mamy speed. Zawiera ona wartość self.speed. Po co taka kopia?! Bo będę zmieniał speed w zależności od tego czy został wciśnięty klawisz SHIFT. W ten sposób wartość początkowa zawsze będzie taka sama.

37.         if back_forward == 0 and left_right == 0 and self.radar.positive:
38.             self.setLinearVelocity((Vector((0,0,0))), False)
39.         elif self.radar.positive or self.jump:
40.             if k(events.LEFTSHIFTKEY):
41.                 speed /= 2
42.             delta = Vector((left_right, back_forward, 0))
43.             delta.magnitude = speed
44.             self.setLinearVelocity((delta), True)
45.             self.applyForce((0,0, self.gravity/2), True)
46.         else:
47.             self.applyForce((0,0, self.gravity), True)

Teraz główna część metody movement. W pierwszym warunku sprawdzamy czy żaden z klawiszy (AWSD) nie został wciśnięty i czy bohater stoi na ziemi (radar zwraca prawdę). Jeśli tak to ustawiamy przyspieszenie gracza w każdej osi na 0. Powoduje to zamrożenie postaci w miejscu, w którym się znajduje. Dzięki poprawnie ustawionemu radarowi może to nastąpić tylko wtedy gdy postać stoi na ziemi. Następnie jeśli jeden z wcześniejszych warunków nie jest spełniony przechodzimy dalej. Mamy drugi warunek, który jest spełniony w dwóch przypadkach, albo stoimy na ziemi, albo jesteśmy w trakcie skoku (o tym później). Załóżmy, że poruszamy się teraz bohaterem po ziemi więc wchodzimy w ten warunek. W pierwszej kolejności sprawdzane jest czy wraz w wciśniętymi klawiszami poruszania się trzymamy wciśnięty klawisz SHIFT. Jeśli tak to zmniejszamy wartość zmiennej speed  o połowę. Następnie tworzymy nową zmienną delta. Jest ona typu Vector o wartościach dla osi X left_right, dal Y back_forward i dla Z zawsze 0. Dzięki temu uzyskujemy w jednej zmiennej informacje o tym w którą stronę porusza się gracz. Następnie używając wyrażenia delta.magnitude = speed zmieniamy długość wektora w zależności od wartości prędkości. W 44. linii ustawiamy przyspieszenie gracza zgodnie z informacjami zawartymi w zmiennej delta. Na koniec dodajemy moc grawitacji dla osi Z. Ale, jeżeli nie wcisnęliśmy, żadnego z klawiszy (AWSD) i nie jesteśmy w trakcie skoku, oraz nasz radar nie zwraca nam prawdy; to oznacza, że bohater jest w powietrzu. W takim wypadku dodajemy mu tylko moc grawitacji. I to cała magiczna formuła.

50.     def mouse_look(self):
51. 
52. x = (render.getWindowWidth() / 2 - self.mouse.position[0])
53. y = (render.getWindowHeight() / 2 - self.mouse.position[1])

Nadeszła kolej na metodę mouse_look. Zaczynamy od zdefiniowania metody i (jak zawsze) w pierwszym parametrze wpisujemy self. Następnie definiujemy dwie zmienne x i y. Obie działają tak samo. Pobierają wysokość/szerokość ekranu, dzielą przez 2 (żeby dostać współrzędne środka) i odejmują aktualną pozycję kursora. Ostatecznie w zmiennej x mamy informacje o przesunięciu myszy lewo-prawo, a w y góra-dół.

55.         self.applyRotation((0 , 0, int(x) * self.rotSpeed[0]), True)

Teraz gdy mamy potrzebne informacje możemy ustawić obrót gracza względem osi Z na podstawie przesunięcia myszki prawo-lewo. Dodatkowo mnożymy wartość przesunięcia przez prędkość obrotu zawarta w liście self.rotSpeed. Użyłem listy, bo w ten sposób możemy łatwo kontrolowac prędkość obrotu kamery góra-dół i prawo-lewo osobno. Pierwsza wartość w liście jest dla osi Z, druga dla osi X.

57.         cameraRotY = Matrix.to_euler(self.cameraTarget.localOrientation)[0]

Teraz tworzymy zmienną cameraRotY. Do tej zmiennej trafia informacja o aktualnym obrocie punktu empty cameraTarge względem osi Y. Za pomoca Matrix konwertuję informacje o rotacji z matrix do euler. Na tym typie znacznie łatwiej operować, gdyż przyjmuje on bardziej czytelną formę. Tylko, że nie w stopniach, a w radianach. Dodatkowo wybieram tylko jedną oś dopisując [0].

59.         if cameraRotY < radians(-89) and y < 0:
60.             self.cameraTarget.localOrientation = Matrix.Rotation(radians(-90), 3, 'X')
61.         elif cameraRotY > radians(74)and y > 0:
62.             self.cameraTarget.localOrientation = Matrix.Rotation(radians(75), 3, 'X')
63.         else:
64.             self.cameraTarget.applyRotation((int(y) * self.rotSpeed[1], 0, 0), True)

Teraz gdy mamy potrzebne informacje możemy ustalić granice (góra-dół) dla naszej kamery. W pierwszym warunku sprawdzamy czy empty przekroczył górną barierę. Jeśli przekroczył i y < 0 co oznacza, że nadal poruszamy myszką w dół, to ustawiamy maksymalny kąt obiektowi cameraTarget. Robimy to podając kąt nachylenia w stopniach, konwertujemy na radiany, następnie wybieramy rozmiar tablicy typu matrix i ustawianą oś. Tak samo w następnym warunku dla dolnej granicy. Ale jeśli, żadna z granic nie została przekroczona, wykonywana jest ostatnia instrukcja. Identyczna jak ta z 55. linii, tylko dla obiektu empty i osi X. To wszystko jest skomplikowane, dlatego zachęcam do zapoznania się z dokumentacją:
http://www.blender.org/documentation/blender_python_api_2_63_17/mathutils.html?highlight=mathutils#mathutils

66.         render.setMousePosition(int(render.getWindowWidth() / 2), 
int(render.getWindowHeight() / 2))

Ostatnia linijka kodu w metodzie mouse_look ustawia kursor myszy na środek ekranu..

69.     def jumping(self):
70.
71.         k = keyDown
72.         move = self.localLinearVelocity

Ostatnia metoda w skrypcie player.py pozwala graczowi wznieść się w powietrze. Tworzymy zmienną k tak samo jak w metodzie movement i przypisujemy zmiennej move przyspieszenie gracza.

74.         if k(events.SPACEKEY, logic.KX_INPUT_JUST_ACTIVATED) and self.radar.positive and 
self.jump == False:

75. self.jump = True
76. self['jump'] = 0
77.
78. if self.jump == True and self['jump'] < self.jump_time:
79. move.z = self.jump_speed
80. else:
81. self.jump = False

Zaczynamy od warunku, który może uruchomić tylko ustawiony klawisz; pod warunkiem, że gracz stoi na ziemi i nie jest w trakcie skoku. Załóżmy, że spełniamy te warunki. Zmieniamy wtedy self.jump na prawdę, co będzie nam mówiło, że jesteśmy w trakcie skoku i zerujemy zegar (self['jump]). Od teraz, puki zegar nie osiągnie granicy ustawionej w self.jump_time, zmienna self.jump będzie ustawiona na prawdę. Możemy zatem przejść do 78. linii i widzimy, że wszystkie warunki są spełnione. Dzięki temu w następnej linii dodajemy graczowi przyspieszenie. Będziemy je dodawać tak długo od puki self['jump'] nie dojdzie do ustawionej granicy. Gdy osiągnie granicę dostaniemy dostęp do 80. linii kodu gdzie zakończymy operację skoku. W trakcie skoku możemy się poruszać, mimo nie jesteśmy na ziemi. Dzieje się tak ponieważ w 39. linii ustawiliśmy, że w trakcie skoku poruszanie się też jest możliwe.

4.Camera

Ostatni skrypt odpowiedzialny jest za pracę kamery. Ustawia on kamerę w odpowiedniej odległości za obiektem cameraTarget. Wywołuje algorytm kolizji kamery oraz pozwala na przybliżanie i oddalanie kamery od gracza. Skrypt jest niemal tak długi jak skrypt player.py, ale nie ma w nim nic skomplikowanego. Zaczniemy od ustawień kamery w Logic Editor.

Sama kamera nie jest do niczego przyparentowana itp. Posiada jeden sensor Always (z włączonym TRUE level triggering), kontroler Python (ustawiony na Module z wpisanym camera.main) i actuator Scene, któremu zmieniłem nazwę na SetCamera (ustawiony na Set Camera, a wybrany obiekt to camera). Skrypt kamery będzie wykorzystywał sensor Ray znajdującym się w obiekcie empty cameraTarget. Zaznaczamy w nim TRUE level triggering oraz X-Ray Mode. Następnie wybieramy oś -Y. Musi to być zawsze oś skierowana w stronę kamery, czyli w tym przypadku do tyłu, bo kamera będzie zawsze z tyłu, za obiektem empty. Wpisujemy parametr collision, który dzięki skryptowi world.py będzie posiadać każdy element mapy. Na koniec ustawiamy odległość, np.: 10 (jest to odległość kamery od obiektu cameraTarget). Łączymy wszystko ze sobą i gotowe.

03. from bge import logic, events, types
04. from math import radians
05. from mathutils import Vector

Przechodzimy do skryptu. Importujemy tylko potrzebne rzeczy i przenosimy sie do funkcji main.

69. def main(cont):
70. 
71. own = cont.owner
72. 
73. if 'init' not in own:
74. own['init'] = True
75. own = Camera(own)
76. 
77. own.collision()

Wszystko tak samo jak w przypadku poprzednich skryptów. Dlatego przejdziemy od razu do klasy.

14. class Camera(types.KX_Camera):
15. 
16. def __init__(self, own):
17. 
18. setCamera = self.actuators["SetCamera"]
19. scene = logic.getCurrentScene()
20. 
21. self.target = scene.objects["cameraTarget"]
22. self.ray = self.target.sensors['Ray']

Definiujemy klasę i dziedziczymy z obiektu KX_Camera. Pobieramy actuator SetCamera. Za pomocą zmiennej scene pobieramy obiekt cameraTarget. Do zmiennej self.ray wrzucamy sensor Ray obiektu cameraTarget.

24.         self.margin = 1
25.         self.positionY = self.ray.range - self.margin
26.         self.positionZ = 0.0 # better leave hear 0.0
27.         self.rotation = 90
28.         self.lens = 35
29.         self.zoom_speed = 2
30.         self.zoom_max_out = 30
31.
32.         self.setPosition()
33.
34.         self.setParent(self.target)
35.
36.         self.controllers[0].activate(setCamera)

Pierwsza zmienna to odległość kamery od obiektu, z którym koliduje. Następna zmienna prezentuje odległość kamery od obiektu empty. Jak widać jest to odległość ustawiona w sensorze Ray zmniejszona o wartość ustawioną w self.margin. Trzecia zmienna, zgodnie z ostrzeżeniem, powinna wynosić 0 (ciekawscy mogą sobie wpisać np.: -1,2,5). Kolejna wartość to kąt nachylenia kamery. Można wpisać np.: 80 i wtedy kamera będzie skierowana bardziej w dół, na gracza. self.lens bierze się z obiektu KX_Camera i w tym przypadku nie jest to tworzenie zmiennej a ustawianie Focal Lenght kamery (powinienem umieścić to w innym miejscu, wśród zmiennych może wprowadzać w błąd). Następnie dwie zmienne wykorzystywane są podczas zmiany odległości kamery od gracza. Teraz wywoływana jest metoda setPosition(), która ustawia kamerę w odpowiednim miejscu i pod odpowiednim kątem, ale o tym za chwilę. Następnie ustawiamy obiekt cameraTarget jako rodzica kamery i na koniec aktywujemy kontroler setCamera. Robimy to wykorzystując pierwszy kontroler obiektu, którym jest python. Posiada on metodę activate gdzie w parametrze podajemy actuator setCamera.

39.     def setPosition(self):
40.
41.         self.worldTransform = self.target.worldTransform
42.         self.localPosition.y -= self.positionY
43.         self.localPosition.z += self.positionZ
44.         self.localOrientation = Vector((radians(self.rotation), 0, 0))

Metoda setPosition jest wykorzystywana a każdej metodzie klasy. Ustawia kamerze położenie i rotację takie jakie posiada obiekt empty. Następnie przesuwa ją do tyłu i ostatecznie obraca o 90 stopni.

47.     def zoom(self):
48. 
79.         m = mouseKey

Definiujemy metodę zoom i tworzymy zmienną, która będzie przechowywać funkcję mouseKey.

07. def mouseKey(key, status = logic.KX_INPUT_JUST_ACTIVATED):
08.
09.         if logic.mouse.events[key] == status:
10.             return True
11.         return False

Działa dokładnie tak samo jak keyDown, z ta różnicą, że w 09. linii mamy logic.mouse.events zamiast logic.keyboard.events.

50.         zoom = m(events.WHEELDOWNMOUSE) - m(events.WHEELUPMOUSE)
51.
52.         if zoom < 0 or self.ray.range < self.zoom_max_out:
53.         self.ray.range += zoom * self.zoom_speed
54.
55.         self.positionY = self.ray.range - self.margin
56.         self.setPosition()

Teraz tworzymy zmienną zoom, która działa dokładnie tak samo jak back_forward z player.py, tylko dla WHEELDOWNMOUSEWHEELUPMOUSE. W warunku sprawdzamy, czy robimy zbliżenie, czy oddalamy. Jeśli zmienna zoom jest większa od 0 to znaczy, że oddalamy kamerę, wtedy ważny jest drugi warunek, który sprawdza czy nie przekroczyliśmy wyznaczonego zakresu. Jeśli wszystko jest ok, to zmieniamy odległość ustawioną w sensorze Ray. W zależności od tego jaką wartość przyjęła zmienna zoom (-1,1,0) przybliżamy, oddalamy, lub nie robimy nic. Potem zaktualizujemy zmienną self.positionY i uruchamiamy metodę setPostion, aby odświerzyć pozycję kamery.

59.     def collision(self):
60.
61.         if self.ray.positive:
62.             self.worldPosition = self.ray.hitPosition
63.             self.localPosition.y += self.margin
64.         else:
65.             self.setPosition()
66.             self.zoom()

Ta metoda jest odpowiedzialna za kolizję kamery. Sprawdza czy sensor Ray zwraca prawdę, co oznacza, że wykrył obiekt. Wtedy ustawia pozycję kamery w miejsce, w którym została wykryta kolizja oraz oddala kamerę od obiektu uwzględniając ustawiony margines. Jeśli kolizja nie jest wykrywana to przywracana jest pozycja kamery oraz pojawia się możliwość użycia metody zoom.

To wszystko. Plik blendera do pobrania: http://troman.pl/files/users/61/player_control.blend 

Dzięki za uwagę.

	
24 komentarzy
pawianek napisał :
godz. 13:28, 5 stycznia 2013
Nie znam się za bardzo ale rozumiem że te wartości przypisywane do parametrów to tak naprawdę słowniki ?
pawianek napisał :
godz. 13:58, 5 stycznia 2013
"Trzeci sensor, Ray został tak ustawiany aby swoim zasięgiem (Distance) sięgam trochę pod gracza." Czy chodzi o sensor Radar ?
gumen napisał :
godz. 20:32, 5 stycznia 2013
Tak, wkradła się pomyłka.
pawianek napisał :
godz. 14:25, 5 stycznia 2013
W klasie Player linijka 18 jest taki kod "self.cameraTarget = self.children['cameraTarget']" czy zamiast tego można by było napisać tak: self.cameraTarget = own.children['cameraTarget'] ? i jak to się stało że childern klasy player to 'camera Target' ? Wiem ze to mogą być głupie pytania ale dopiero się uczę :]
gumen napisał :
godz. 20:31, 5 stycznia 2013
Dobrze, że pytasz. Ten drugi zapis nie jest poprawny, chociaż w tym wypadku own i self to ten sam obiekt. Zmienna own nie będzie rozpoznawalna przez program tam gdzie znajduje się ta linijka kodu, o która pytasz.

Obiekt empty 'cameraTarget' jest sparentowany z obiektem 'bee' (graczem). Dlatego znajduje się w liście obiektów children. Czyli wybieramy sobie obiekt 'cameraTarget z listy wszystkich obiektów przyparentowanych do gracza.
pawianek napisał :
godz. 14:05, 6 stycznia 2013
Jeśli dobrze rozumiem to jeśli klasa dziedziczy klasę types.KX_GameObject to "konstruktor" tej klasy musi wyglądać tak: def __init__(nazwa1, nazwa2): - czyli musi przyjmować tylko dwie zmienne z których nazwa1 to odwołanie do obiektu klasy, a nazwa2 też potem jakoś jest przypisywana do obiektu klasy dzięki czemu możemy wywołać np. taką komendę self.children['cameraTarget']. czy to rozumowanie jest poprawne ?
gumen napisał :
godz. 15:52, 6 stycznia 2013
Zgadza się. Dzięki temu, że dziedziczymy z klasy KX_GameObject (a musimy to zrobić w bge, bo inaczej obiekt nie będzie posiadał takich informacji jak położenie skala itp) mamy wszystkie inne informacje jakie posiada taki obiekt jak np.: children, sensors, localPosition itd...
pawianek napisał :
godz. 14:51, 6 stycznia 2013
Czy klasa World musi dziedziczyć z klasy types.KX_GameObject, jeśli tak to dlaczego ?, czy można by było jakoś ominąć to dziedziczenie ?
Anonymous napisał :
godz. 15:59, 6 stycznia 2013
chm... właściwie to chyba nie musi, ale osobiście tego nie próbowałem. Chyba powinno dać się zrobić tak, że zamiast 'own = World(own)' to np.: 'world = World()' tylko wtedy trzeba wyrzucić dziedziczenie: 'class World():' i w metodzie __init__ wywalić 'own'. Nie próbowałem tego ale chyba powinno działać. Potem normalnie cały czas wywoływać world.gravity()
pawianek napisał :
godz. 17:10, 6 stycznia 2013
Sprawdziłem i działa , ale trzeba dać 'world = World()' żeby nie był w tym if-ie, nie wiem dlaczego
gumen napisał :
godz. 17:34, 6 stycznia 2013
if musi być, bo inaczej będziesz od nowa tworzyć klasę w każdej klatce na sekundę. Zrób takiego ifa.

def main(cont):
own = cont.owner

if 'init' not in own:
own['init'] = True
world = World()

world.gravity()

Używasz obiektu, żeby mieć pewność, że ta operacja wykona się tylko raz ale robisz sobie obiekt world tak ja to ustaliliśmy.
pawianek napisał :
godz. 18:03, 7 stycznia 2013
zrobiłem taki kod jak wyżej ale wyskoczył mi błąd, nie mam pojęcia dlaczego za mało jeszcze umiem :( , ale czy w powyższym kodzie nie jest tak że ten if za każdym razem się wykona bo "own" jest jakby zerowany linijkę wyżej, czyli w tej linijce : own = cont.owner ?
gumen napisał :
godz. 19:31, 7 stycznia 2013
nie, to jest tylko pobieranie obiektu do zmiennej, a w ifie dodajemy mu parametr, który nie znika (znika odpiero po zakończeniu gry), dzięki temu unikamy ponownego utworzenia obiektu z klasy, dzieje się to tylko raz, bo gdy następnym razem blender wywoła ten skrypt to parametr będzie już w obiekcie own i if zostanie pominięty. A z błędami musisz się sam uporać, kombinuj, wywalaj, zmieniaj, sprawdzaj, na pewno jakoś do tego dojdziesz.
pawianek napisał :
godz. 17:45, 8 stycznia 2013
Już chyba wiem dlaczego nie działa :-). takiego rozwiązania nie można zastosować:

def main(cont):
own = cont.owner

if 'init' not in own:
own['init'] = True
world = World()

world.gravity()

ponieważ za każdym razem kiedy kończy się funkcja main() wszystko co jest w jej środku też jest usuwane, oczywiście oprócz tych obiektów które są w scenie. Powyższa funkcja world.gravity() wykona się tylko raz, za pierwszym razem kiedy wykonywane jest to co w if-ie potem ta funkcja nie będzie już rozpoznawalna bo obiekt world został usunięty po zakończeniu funkcji main(). Dlatego też jeśli nie chemy utracić jakichś danych dobrze jest wszystko wykonywać na obiektach znajdujących się w scenie a tu niezbędne jest dziedziczenie z tej klasy "types.KX_GameObject".

Czy to jest poprawne ?

Jeśli tak to zniosę jajo :D.
gumen napisał :
godz. 17:34, 6 stycznia 2013
if musi być, bo inaczej będziesz od nowa tworzyć klasę w każdej klatce na sekundę. Zrób takiego ifa.

def main(cont):
own = cont.owner

if 'init' not in own:
own['init'] = True
world = World()

world.gravity()

Używasz obiektu, żeby mieć pewność, że ta operacja wykona się tylko raz ale robisz sobie obiekt world tak ja to ustaliliśmy.
gumen napisał :
godz. 17:34, 6 stycznia 2013
if musi być, bo inaczej będziesz od nowa tworzyć klasę w każdej klatce na sekundę. Zrób takiego ifa.

def main(cont):
own = cont.owner

if 'init' not in own:
own['init'] = True
world = World()

world.gravity()

Używasz obiektu, żeby mieć pewność, że ta operacja wykona się tylko raz ale robisz sobie obiekt world tak ja to ustaliliśmy.
gumen napisał :
godz. 17:35, 6 stycznia 2013
o kurde coś cię zacięło w przeglądarce :D
zrób taki kod (jak wyżej) ale pamiętaj o tabulatorach.
Anonymous napisał :
godz. 13:02, 6 stycznia 2013
Super, dzięki za odpowiedź, bardzo chce zrozumieć tego tutka, bo mam zrobić grę przygodową :]
pawianek napisał :
godz. 14:10, 6 stycznia 2013
hehe , dlaczego mnie podpisało jako Anonymous? :]
maniek napisał :
godz. 14:43, 6 stycznia 2013
Jest jakiś błąd w skrypcie, którego nie mogę jeszcze namierzyć ;/
pawianek napisał :
godz. 20:20, 14 stycznia 2013
Bardzo dobry tutek, bardzo dużo się przy nim nauczyłem, a nie doszedłem jeszcze do końca :) (mało czasu).

Pytanko odnośnie funkcji mouse_look
linijka 55 self.applyRotation((0 , 0, int(x) * self.rotSpeed[0]), True)
jeśli dobrze się orientuję to chyba nie ma potrzeby konwertować zmiennej x na int, wykasowałem to konwertowanie i niby wszystko działa dobrze, czy może jest to konieczne ?
szymon5596 napisał :
godz. 12:05, 3 stycznia 2014
Można prosić o wrzucenie pliczku jeszcze raz, bo link wygasł?
maniek napisał :
godz. 18:28, 5 stycznia 2014
Wrzuciłem na serwer tromana.. Link został podmnieniony lub masz go tu http://troman.pl/files/users/61/player_control.blend
szymon5596 napisał :
godz. 23:26, 5 stycznia 2014
Spasiba
Dodaj komentarz
Aby dodać komentarz do newsa, musisz być zalogowany w Serwisie.. Zaloguj