Jest to blog poświęcony nowoczelnym technologiom ułatwiającym tworzenie aplikacji internetowych. Znajdziesz tutaj porady na temat CSS3, JavaScript, designu, web-usability, standardów W3C.
Cześć! Nazywam się Michał Środek. Z zawodu programista php, z zamiłowania gitarzysta oraz fanatyk GNU/Linuksa(openSUSE® w laptopie). W branży aplikacji internetowych od 9 lat. Prywatnie bez dzieci i kota.
Pracuję wciąż nad własnym elastycznym i wydajnym frameworkiem MVC, kilkoma portalami internetowymi oraz mniejszymi bibliotekami php. Czekam na wasze opinie, zgłoszenia błędów oraz pomysły na dalszy rozwój.
Ta część strony jest w trakcie budowy a moje prace tymczasowo niedostępne.
W przypadku pytań, ofert pracy oraz ciekawych pomysłów proszę się ze mną kontaktować. Możesz mnie znaleźć i wysłać PW na php.pl(SHiP), jamendo.com(michalsrodek), goldenLine.pl, facebook.com lub nk.pl
W dzisiejszym artykule pokażę prosty sposób na wychwycenie zdarzenia przytrzymania klawiszy klawiatury oraz rozpoczniemy mierzenie prędkości naszej gry tj. współczynnika FPS. Umieścimy na naszej mapie czołg oraz umożliwimy sterowanie nim za pomocą klawiszy strzałek.
UWAGA: Artykuł jest kontynuacją poprzedniego: Canvas — Rysujemy mapę
Przede wszystkim potrzebujemy czołgu. Na potrzeby artykułu oraz mojej gry stworzyłem prosty szkic w GIMP-ie:
![]()
Zanim umieścimy go na planszy, przeanalizujmy kilka innych ważnych aspektów.
Aby sterować naszym czołgiem musimy stworzyć obiekt przechwytujący zdarzenia klawiatury. Najbardziej problematyczny jest jednak sposób ich działania w JavaScript. Dla przeciętnego klawisza są wywoływane 4 zdarzenia:
Zdarzenie textInput zdefiniowane niedawno przez W3C obsługują, bodajże, tylko przeglądarki w oparciu o silnik WebKit. Z pozostałymi zdarzeniami jest różnie. Keypress jest pomijane w przypadku klawiszy specjalnych takich jak shift, alt i ctrl. Niestety nie ma jasno zdefiniowanej listy klawiszy specjalnych. Przeglądarki w oparciu o WebKit zaliczają do nich np. klawisze strzałek, FireFox już nie. Co więcej identyfikacja klawiszy w różnych przeglądarkach jest realizowana w oparciu o inne pola(event.keyCode, event.which oraz event.charCode). Na szczęście wszystkie całkiem sprawnie obsługują pole keyCode, więc nie jest to aż tak ogromny problem(pomijając fakt, że czasami keyCode dla tego samego klawisza jest inny w różnych przeglądarkach). Podsumowując. Obsługa klawiatury w JavaScript to istny koszmar i moim zdaniem powinna zostać przebudowana. Wystarczy pozostawić keydown, textInput oraz keyup oraz ujednolicić kody klawiszy. Jest jednak lista klawiszy bezpiecznych(działających identycznie lub bardzo podobnie w różnych środowiskach). Znalazłem w sieci ciekawe zestawienie na ten temat.
Warto wiedzieć w jaki sposób działa zdarzenie przytrzymania klawisza. Przeglądarki realizują to bardzo dziwnie. W przypadku wciśnięcia klawisza jest wywoływana akcja keydown, keypress oraz keyup. Następnie wywoływane jest ponownie zdarzenie keydown, po nim keypress oraz znów keyup. I tak, aż do momentu puszczenia klawisza. W naszym przypadku zdarzenie keypress pominiemy(tym bardziej, że w przypadku strzałek nie zawsze jest ono wywoływane) więc zaobserwujemy:
keydown
keyup
keydown
keyup
(…)
keydown
keyup
Oczywiście w przypadku naszej gry akcja utrzymywania klawisza musi być ciągła dlatego musimy opóźnić zwolnienie klawisza. Napiszmy prostą klasę.
var keyHandler = { keyLastEvent: [], //true = keyDown, false = keyUp keyStatus: [], keyDown: function(e){ // ustawienie statusu na jeden(klawisz wcisniety po raz pierwszy) if(keyHandler.keyStatus[e.keyCode]===undefined) { keyHandler.keyStatus[e.keyCode] = 1; keyHandler.keyLastEvent[e.keyCode] = 1; } else { if(!keyHandler.keyLastEvent[e.keyCode]) // ostatnia akcja byl keyUp => powtarzane down, up; down, up keyHandler.keyStatus[e.keyCode]++; keyHandler.keyLastEvent[e.keyCode] = 1; } }, keyUp: function(e){ keyHandler.keyLastEvent[e.keyCode] = 0; // odcisniecie klawisza z 10ms opóźnieniem window.setTimeout(keyHandler.decreaseKeyStatus(e.keyCode), 20); }, decreaseKeyStatus: function(keyCode){ this.keyStatus[keyCode]--; }, isHolded: function(key){ return (this.keyStatus[key]>0); } };
Klasa zawiera tablicę klawiszy. W przypadku wciśnięcia np. strzałki w lewo(keyCode = 37) następuje zwiększenie o jeden wartości keyStatus[37]. Jeżeli ta wartość jest większa od zera metoda isHolded() zwraca true co informuje nas, że klawisz jest przytrzymywany. W przypadku systemów z rodziny Windows zdarzenie keyUp nie jest powtarzane. Zatem zwiększenie wartości naszego licznika powinno nastąpić tylko raz w przypadku pierwszego pojawienia się zdarzenia keyDown. Aby wiedzieć jakiego rodzaju zdarzenie zostało ostatnio wywołane(up czy down) stworzyłem zmienną keyLastEvent i modyfikuję ją wg. potrzeb. Dzięki takiej budowie naszej klasy(tj. bez warunkowych lini kodu zależnych od systemu operacyjnego lub przeglądarki) akcja przytrzymania klawisza zostanie poprawnie rozpoznana nawet w przypadku poprawienia błędów w jednej z przeglądarek.
Aby kod ten zadziałał potrzebujemy dodatkowo określić, które z naszych funkcji nasłuchują. Zrobimy to w metodzie init() obiektu GameBoard tuż pod kodem ładującym tekstury.
this.preloadTextures(1); document.addEventListener('keydown', keyHandler.keyDown, false); document.addEventListener('keyup', keyHandler.keyUp, false);
W przypadku gier ważne jest utworzenie pętli głównej programu. Będzie ona za każdym razem sprawdzała stan zdarzeń pochodzących z klawiatury, sieci oraz sztucznej inteligencji. W przypadku małych gier(np. kółko i krzyżyk, sudoku, saper) jest to nieistotne, u nas — w grze pełnej animacji — po prostu konieczne.
Stwórzmy dodatkowy obiekt będący centralną sterownią naszej gry.
var GameMain = { img: {}, x:0, y:480, direction: 1, // 0= LEFT 1 =UP, 2 = RIGHT, 3=DOWN mainLoop: function(){ GameBoard.drawBoard(); if(keyHandler.isHolded(37)) // LEFT { GameMain.direction = 0; if(GameMain.x>0) GameMain.x-=1; } else if(keyHandler.isHolded(38)) // UP { GameMain.direction = 1; if(GameMain.y>0) GameMain.y-=1; } else if(keyHandler.isHolded(39)) // RIGHT { GameMain.direction = 2; if(GameMain.x<480) GameMain.x+=1; } else if(keyHandler.isHolded(40)) // DOWN { GameMain.direction = 3; if(GameMain.y<480) GameMain.y+=1; } for(var i=37; i<41; i++) { if(keyHandler.isHolded(i)) document.getElementById('key-'+i).style.background = 'red'; else document.getElementById('key-'+i).style.background = 'white'; } GameBoard.ctx.save(); GameBoard.ctx.translate(GameMain.x,GameMain.y); switch(GameMain.direction) { case 2: GameBoard.ctx.rotate(90 * Math.PI / 180); GameBoard.ctx.drawImage(GameMain.img.tank,0,-32); break; case 3: GameBoard.ctx.rotate(180 * Math.PI / 180); GameBoard.ctx.drawImage(GameMain.img.tank,-32,-32); break; case 0: GameBoard.ctx.rotate(270 * Math.PI / 180); GameBoard.ctx.drawImage(GameMain.img.tank,-32,0); break; default: GameBoard.ctx.drawImage(GameMain.img.tank,0,0); } GameBoard.ctx.restore(); FrameCounter.updateFPS(); }, init: function(){ this.img.tank = new Image(); this.img.tank.src = 'images/btank.png'; this.img.tank.onload = function(){ GameBoard.init(); } } }
Metoda mainLoop() będzie wywoływana co określony, bardzo krótki czas. Dzięki temu będziemy w stanie na bierząco wychwytywać nadchodzące zdarzenia.
Podzielona jest ona na kilka akcji. Pierwszą z nich jest rysowanie mapy. Następnie sprawdzamy poszczególne klawisze strzałek. W przypadku przytrzymania jedngo z nich, zmieniamy współrzędne naszego czołgu — pola x oraz y klasy GameMain. Kolejnym krokiem jest zapisanie stanu obiektu canvas. Translacja o wektor, obrót w odpowiednim kierunku oraz rysowanie czołgu. Następnie przywracamy stan obszaru canvas i wywołujemy licznik FPS
Odczyt grafiki czołgu umieściłem w metodzie init(). Jest to nasza nowa metoda inicjalizująca, którą musimy wywołać aby uruchomić grę.
Ważnym elementem podczas testowania gry jest licznik FPS. Dzięki niemu będziemy mogli określić, które elementy gry są napisane nieoptymalnie oraz, który kod należy wymienić. Oko ludzkie jest w stanie zaobserwować jedynie 25 klatek filmu w każdej sekundzie. Dla mnie jest to jednak mało. Spróbujmy sprawdzić aby gra posiadała przynajmniej 40fps. Moja klasa licząca ilość klatek wygląda następująco:
var FrameCounter = { lastFrameTime: new Date().getTime(), updateFPS: function(){ var currentTime = new Date().getTime(); document.getElementById('fps').innerHTML = Math.floor(1000/(currentTime-this.lastFrameTime),2)+'fps'; this.lastFrameTime = currentTime; } };
Jak ten kod działa? W każdej klatce obliczana jest różnica pomiędzy aktualnym czasem oraz czasem wyświetlania poprzedniej klatki. W ten sposób możemy obliczyć czas(w milisekundach) wykonywania się jednej klatki. Dzieląc przez tą wartość 1000 otrzymujemy ilość klatek na sekundę.
Zmieniona została klasa inicjująca naszą grę więc w metodzie GameBoard.preloadTextures() musi zajść lekka zmiana:
preloadTextures: function(i){ var img = new Image(); img.src = this.textures[i]; img.onload = function() { GameBoard.textures[i] = GameBoard.ctx.createPattern(img, 'repeat'); if(GameBoard.textures[i+1]) GameBoard.preloadTextures(i+1); else window.setInterval(GameMain.mainLoop,10); } },
Gotowe! Pełny kod źródłowy z dzisiejszego artykułu jest dostępny również online, jako działająca całość. Zapraszam do testów.
Po uruchomieniu gry zapewne zauważyłeś, że działa ona bardzo wolno. Jedynie przeglądarki działające w oparciu o WebKit potrafią osiągnąć oczekiwane 40 klatek. Poniżej dołączam małe zestawienie przeglądarek(aktualne ustawienia — odświeżanie klatki co 10ms — powoduje, że maksymalna ilość klatek na sekundę to 100).
| Przeglądarka | fps |
|---|---|
| Safari (OS X) | 50–60 |
| Google Chrome(5.0.375.38 beta, GNU/Linux) | 58–60 |
| Mozilla Firefox(3.5.9, GNU/Linux) | 31–33 |
| Konqueror(4.3.5–0.1.1, GNU/Linux) | 15–17 |
| Opera(10.10, GNU/Linux) | 12–16 |
Oczywiście jest to zbyt mało aby gra była w pełni grywalna, ponieważ na mapie poruszać będzie się kilka czołgów otoczonych szeregiem animacji. Pamiętajmy jednak, że kod, który umieściłem na serwerze jest niezoptymalizowany. W kolejnych artykułach przedstawię sztuczki jak przyspieszyć rysowanie elementów w obiekcie canvas oraz jak skrócić czas wykonywania się zwykłego kodu JavaScript.
Może dodałbyś jakieś demka do artykułów, można by od razu przetestować czy działa. ;]
Komentarz by Tomasz Kowalczyk — 23 maja 2010, 0:28
Przecież w akapicie „Uruchomimenie“ jest link do działającej gry — http://srodek.info/examples/game-0.0.3/index.html
Komentarz by Michał Środek — 23 maja 2010, 11:15
Super artykuł myślę ze brakuje artykułów o Canvas w polskiej sieci, apropo zestawienia to jeżeli chodzi o Opere 10.5X to fps jest ok.68–75 wiec nawet wyższe niż w Chrome. Przydałby się jeszcze artykuł jak można by zrobić śledzenie obiektu jak opuści mapę.
Pozdrawiam
Komentarz by maxx — 23 maja 2010, 11:38
Dzięki. Czy się mylę, czy piszesz o Operze w systemie Windows?
O rysowaniu można poczytać sporo pod tym adresem. Nie ma sensu abym to duplikował. Mogę napisać co nieco na temat rysowania obrazków, ścieżek i bardziej zaawansowanych rzeczy.
Co do opuszczania mapy. Co masz na myśli? Czołg powinien przenosić się tj. gdy znika z prawej strony, powinien pojawiać się w lewej? Ogólnie obserwować wystarczy zmienne x oraz y z obiektu GameMain.
Tak swoją drogą. Napiszcie na jaki temat chcielibyście poczytać. Ja chętnie przeanalizuje wasze propozycje
.
Komentarz by Michał Środek — 23 maja 2010, 11:51
Chodzi mi o opere w systemie Windows, nie dokonca chodziło mi bardziej o śledzeniu obiektu za pomocą tak jakby kamera, mapa ma np 1000 x 1000 wyświetla nam się 200 x 200 i ze śledzimy obiekt jak opuści ten obszar początkowy.
Apropo artykuł to z chęcią bym własnie przeczytał na ten temat który przed chwila podałem.
Komentarz by maxx — 23 maja 2010, 12:35
Ok, i tak miałem pisać na ten temat
.
Komentarz by Michał Środek — 23 maja 2010, 12:55
U mnie w najnowszym FF(windows), FPS to 66–71
Komentarz by Oskar Wójcicki — 27 maja 2010, 22:23
Safari (mac) 111 FPS
Chrome (Mac) 90 — 100 FPS
Mozilla Firefox (Mac) — 52 FPS (dużo zakładek miałem)
Opera (Mac) 60 FPS (dużo zakładek miałem)
Zauważyłem jeszcze jedną rzecz do której się przyczepię, mianowicie jak trzymam strzałkę do dołu to mogę jechać w lewo, prawo oraz do góry nie puszczając strzałki w dół. Trzymając strzałkę do góry mogę jechać tylko w lewo. Trzymając w prawo mogę jechać w lewo i do góry. Trzymając w lewo w żadną stronę nie mogę poza lewo.
Gdybyśmy jechali w lewo — wiadomo nie potrzeba nam w prawo, ale przydałoby się nagle zmienić kierunek do dołu lub do góry nie puszczając klawisza do jazdy w lewo.
Pozdrawiam
PS. Proszę o skasowanie postu poprzedniego postu!
Komentarz by geshu — 8 czerwca 2010, 13:28
To poruszanie się wynika z prymitywnej obsługi trzymanych klawiszy. Kod wymienię wkrótce. Teraz po prostu mam urwanie głowy w związku z innym projektem.
Pozdrawiam
PS: rozumiem, że chodziło o usunięcie poprzedniego komentarza?
Komentarz by Michał Środek — 8 czerwca 2010, 19:16
Dokładnie tak — komentarza
Komentarz by geshu — 9 czerwca 2010, 7:10