Michał Środek

Po prostu devBlog

Witaj na srodek.info

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

22 Maj 2010

Obsługa klawiatury oraz licznik FPS

Filed under: Gry,JavaScript,Moje projekty — Michał Środek @ 23:58

W dzi­siej­szym arty­kule pokażę pro­sty spo­sób na wychwy­ce­nie zda­rze­nia przy­trzy­ma­nia kla­wi­szy kla­wia­tury oraz roz­pocz­niemy mie­rze­nie pręd­ko­ści naszej gry tj. współ­czyn­nika FPS. Umie­ścimy na naszej mapie czołg oraz umoż­li­wimy ste­ro­wa­nie nim za pomocą kla­wi­szy strzałek.

UWAGA: Arty­kuł jest kon­ty­nu­acją poprzed­niego: Canvas — Rysu­jemy mapę

Przede wszyst­kim potrze­bu­jemy czołgu. Na potrzeby arty­kułu oraz mojej gry stwo­rzy­łem pro­sty szkic w GIMP-ie:

Czołg

Zanim umie­ścimy go na plan­szy, prze­ana­li­zujmy kilka innych waż­nych aspektów.

Obsługa kla­wia­tury

Aby ste­ro­wać naszym czoł­giem musimy stwo­rzyć obiekt prze­chwy­tu­jący zda­rze­nia kla­wia­tury. Naj­bar­dziej pro­ble­ma­tyczny jest jed­nak spo­sób ich dzia­ła­nia w Java­Script. Dla prze­cięt­nego kla­wi­sza są wywo­ły­wane 4 zdarzenia:

  1. key­down
  2. key­press
  3. textIn­put
  4. keyup

Zda­rze­nie textIn­put zde­fi­nio­wane nie­dawno przez W3C obsłu­gują, bodajże, tylko prze­glą­darki w opar­ciu o sil­nik WebKit. Z pozo­sta­łymi zda­rze­niami jest róż­nie. Key­press jest pomi­jane w przy­padku kla­wi­szy spe­cjal­nych takich jak shift, alt i ctrl. Nie­stety nie ma jasno zde­fi­nio­wa­nej listy kla­wi­szy spe­cjal­nych. Prze­glą­darki w opar­ciu o WebKit zali­czają do nich np. kla­wi­sze strza­łek, Fire­Fox już nie. Co wię­cej iden­ty­fi­ka­cja kla­wi­szy w róż­nych prze­glą­dar­kach jest reali­zo­wana w opar­ciu o inne pola(event.keyCode, event.which oraz event.charCode). Na szczę­ście wszyst­kie cał­kiem spraw­nie obsłu­gują pole key­Code, więc nie jest to aż tak ogromny problem(pomijając fakt, że cza­sami key­Code dla tego samego kla­wi­sza jest inny w róż­nych prze­glą­dar­kach). Pod­su­mo­wu­jąc. Obsługa kla­wia­tury w Java­Script to istny kosz­mar i moim zda­niem powinna zostać prze­bu­do­wana. Wystar­czy pozo­sta­wić key­down, textIn­put oraz keyup oraz ujed­no­li­cić kody kla­wi­szy. Jest jed­nak lista kla­wi­szy bezpiecznych(działających iden­tycz­nie lub bar­dzo podob­nie w róż­nych środo­wi­skach). Zna­la­złem w sieci cie­kawe zesta­wie­nie na ten temat.

Warto wie­dzieć w jaki spo­sób działa zda­rze­nie przy­trzy­ma­nia kla­wi­sza. Prze­glą­darki reali­zują to bar­dzo dziw­nie. W przy­padku wci­śnię­cia kla­wi­sza jest wywo­ły­wana akcja key­down, key­press oraz keyup. Następ­nie wywo­ły­wane jest ponow­nie zda­rze­nie key­down, po nim key­press oraz znów keyup. I tak, aż do momentu pusz­cze­nia kla­wi­sza. W naszym przy­padku zda­rze­nie key­press pominiemy(tym bar­dziej, że w przy­padku strza­łek nie zawsze jest ono wywo­ły­wane) więc zaob­ser­wu­jemy:

key­down
keyup
key­down
keyup
(…)
key­down
keyup

Oczy­wi­ście w przy­padku naszej gry akcja utrzy­my­wa­nia kla­wi­sza musi być cią­gła dla­tego musimy opóź­nić zwol­nie­nie kla­wi­sza. Napiszmy pro­stą 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ę kla­wi­szy. W przy­padku wci­śnię­cia np. strzałki w lewo(keyCode = 37) nastę­puje zwięk­sze­nie o jeden war­to­ści keyStatus[37]. Jeżeli ta war­tość jest więk­sza od zera metoda isHol­ded() zwraca true co infor­muje nas, że kla­wisz jest przy­trzy­my­wany. W przy­padku sys­te­mów z rodziny Win­dows zda­rze­nie keyUp nie jest powta­rzane. Zatem zwięk­sze­nie war­to­ści naszego licz­nika powinno nastą­pić tylko raz w przy­padku pierw­szego poja­wie­nia się zda­rze­nia key­Down. Aby wie­dzieć jakiego rodzaju zda­rze­nie zostało ostat­nio wywołane(up czy down) stwo­rzy­łem zmienną key­La­stE­vent i mody­fi­kuję ją wg. potrzeb. Dzięki takiej budo­wie naszej klasy(tj. bez warun­ko­wych lini kodu zależ­nych od sys­temu ope­ra­cyj­nego lub prze­glą­darki) akcja przy­trzy­ma­nia kla­wi­sza zosta­nie popraw­nie roz­po­znana nawet w przy­padku popra­wie­nia błę­dów w jed­nej z przeglądarek.

Aby kod ten zadzia­łał potrze­bu­jemy dodat­kowo okre­ślić, które z naszych funk­cji nasłu­chują. Zro­bimy to w meto­dzie init() obiektu Game­Bo­ard tuż pod kodem ładu­ją­cym tekstury.

        this.preloadTextures(1);
 
        document.addEventListener('keydown', keyHandler.keyDown, false);
        document.addEventListener('keyup', keyHandler.keyUp, false);

Pętla główna

W przy­padku gier ważne jest utwo­rze­nie pętli głów­nej pro­gramu. Będzie ona za każ­dym razem spraw­dzała stan zda­rzeń pocho­dzą­cych z kla­wia­tury, sieci oraz sztucz­nej inte­li­gen­cji. W przy­padku małych gier(np. kółko i krzy­żyk, sudoku, saper) jest to nie­istotne, u nas — w grze peł­nej ani­ma­cji — po pro­stu konieczne.

Stwórzmy dodat­kowy obiekt będący cen­tralną ste­row­nią 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 main­Loop() będzie wywo­ły­wana co okre­ślony, bar­dzo krótki czas. Dzięki temu będziemy w sta­nie na bie­rząco wychwy­ty­wać nad­cho­dzące zdarzenia.

Podzie­lona jest ona na kilka akcji. Pierw­szą z nich jest ryso­wa­nie mapy. Następ­nie spraw­dzamy poszcze­gólne kla­wi­sze strza­łek. W przy­padku przy­trzy­ma­nia jedngo z nich, zmie­niamy współ­rzędne naszego czołgu — pola x oraz y klasy Game­Main. Kolej­nym kro­kiem jest zapi­sa­nie stanu obiektu canvas. Trans­la­cja o wek­tor, obrót w odpo­wied­nim kie­runku oraz ryso­wa­nie czołgu. Następ­nie przy­wra­camy stan obszaru canvas i wywo­łu­jemy licz­nik FPS

Odczyt gra­fiki czołgu umie­ści­łem w meto­dzie init(). Jest to nasza nowa metoda ini­cja­li­zu­jąca, którą musimy wywo­łać aby uru­cho­mić grę.

Licz­nik FPS

Waż­nym ele­men­tem pod­czas testo­wa­nia gry jest licz­nik FPS. Dzięki niemu będziemy mogli okre­ślić, które ele­menty gry są napi­sane nie­opty­mal­nie oraz, który kod należy wymie­nić. Oko ludz­kie jest w sta­nie zaob­ser­wo­wać jedy­nie 25 kla­tek filmu w każ­dej sekun­dzie. Dla mnie jest to jed­nak mało. Spró­bujmy spraw­dzić aby gra posia­dała przy­naj­mniej 40fps. Moja klasa licząca ilość kla­tek 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 obli­czana jest róż­nica pomię­dzy aktu­al­nym cza­sem oraz cza­sem wyświe­tla­nia poprzed­niej klatki. W ten spo­sób możemy obli­czyć czas(w mili­se­kun­dach) wyko­ny­wa­nia się jed­nej klatki. Dzie­ląc przez tą war­tość 1000 otrzy­mu­jemy ilość kla­tek na sekundę.

Uru­cho­mie­nie

Zmie­niona została klasa ini­cju­jąca naszą grę więc w meto­dzie 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 dzi­siej­szego arty­kułu jest dostępny rów­nież online, jako dzia­ła­jąca całość. Zapra­szam do testów.

Opty­ma­li­za­cja

Po uru­cho­mie­niu gry zapewne zauwa­ży­łeś, że działa ona bar­dzo wolno. Jedy­nie prze­glą­darki dzia­ła­jące w opar­ciu o WebKit potra­fią osią­gnąć ocze­ki­wane 40 kla­tek. Poni­żej dołą­czam małe zesta­wie­nie przeglądarek(aktualne usta­wie­nia — odświe­ża­nie klatki co 10ms — powo­duje, że mak­sy­malna ilość kla­tek na sekundę to 100).

Prze­glą­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

Oczy­wi­ście jest to zbyt mało aby gra była w pełni gry­walna, ponie­waż na mapie poru­szać będzie się kilka czoł­gów oto­czo­nych sze­re­giem ani­ma­cji. Pamię­tajmy jed­nak, że kod, który umie­ści­łem na ser­we­rze jest nie­zop­ty­ma­li­zo­wany. W kolej­nych arty­ku­łach przed­sta­wię sztuczki jak przy­spie­szyć ryso­wa­nie ele­men­tów w obiek­cie canvas oraz jak skró­cić czas wyko­ny­wa­nia się zwy­kłego kodu JavaScript.

Dodaj arty­kuł do:

  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Gwar
  • Reddit
  • Technorati
  • Twitter
  • Wykop

Komentarzy: 10 »

  1. Może dodał­byś jakieś demka do arty­ku­łów, można by od razu prze­te­sto­wać czy działa. ;]

    Komentarz by Tomasz Kowalczyk — 23 maja 2010, 0:28

  2. Prze­cież w aka­pi­cie „Uru­cho­mi­me­nie“ jest link do dzia­ła­ją­cej gry — http://srodek.info/examples/game-0.0.3/index.html

    Komentarz by Michał Środek — 23 maja 2010, 11:15

  3. Super arty­kuł myślę ze bra­kuje arty­ku­łów o Canvas w pol­skiej sieci, apropo zesta­wie­nia to jeżeli cho­dzi o Opere 10.5X to fps jest ok.68–75 wiec nawet wyż­sze niż w Chrome. Przy­dałby się jesz­cze arty­kuł jak można by zro­bić śledze­nie obiektu jak opu­ści mapę.

    Pozdra­wiam

    Komentarz by maxx — 23 maja 2010, 11:38

  4. Dzięki. Czy się mylę, czy piszesz o Ope­rze w sys­te­mie Windows?

    O ryso­wa­niu można poczy­tać sporo pod tym adre­sem. Nie ma sensu abym to dupli­ko­wał. Mogę napi­sać co nieco na temat ryso­wa­nia obraz­ków, ście­żek i bar­dziej zaawan­so­wa­nych rzeczy.

    Co do opusz­cza­nia mapy. Co masz na myśli? Czołg powi­nien prze­no­sić się tj. gdy znika z pra­wej strony, powi­nien poja­wiać się w lewej? Ogól­nie obser­wo­wać wystar­czy zmienne x oraz y z obiektu GameMain.

    Tak swoją drogą. Napisz­cie na jaki temat chcie­li­by­ście poczy­tać. Ja chęt­nie prze­ana­li­zuje wasze pro­po­zy­cje :D .

    Komentarz by Michał Środek — 23 maja 2010, 11:51

  5. Cho­dzi mi o opere w sys­te­mie Win­dows, nie dokonca cho­dziło mi bar­dziej o śledze­niu obiektu za pomocą tak jakby kamera, mapa ma np 1000 x 1000 wyświe­tla nam się 200 x 200 i ze śledzimy obiekt jak opu­ści ten obszar początkowy.

    Apropo arty­kuł to z chę­cią bym wła­snie prze­czy­tał na ten temat który przed chwila podałem.

    Komentarz by maxx — 23 maja 2010, 12:35

  6. Ok, i tak mia­łem pisać na ten temat ;) .

    Komentarz by Michał Środek — 23 maja 2010, 12:55

  7. U mnie w naj­now­szym FF(windows), FPS to 66–71 :)

    Komentarz by Oskar Wójcicki — 27 maja 2010, 22:23

  8. Safari (mac) 111 FPS
    Chrome (Mac) 90 — 100 FPS
    Mozilla Fire­fox (Mac) — 52 FPS (dużo zakła­dek mia­łem)
    Opera (Mac) 60 FPS (dużo zakła­dek miałem)

    Zauwa­ży­łem jesz­cze jedną rzecz do któ­rej się przy­cze­pię, mia­no­wi­cie jak trzy­mam strzałkę do dołu to mogę jechać w lewo, prawo oraz do góry nie pusz­cza­jąc strzałki w dół. Trzy­ma­jąc strzałkę do góry mogę jechać tylko w lewo. Trzy­ma­jąc w prawo mogę jechać w lewo i do góry. Trzy­ma­jąc w lewo w żadną stronę nie mogę poza lewo.

    Gdy­by­śmy jechali w lewo — wia­domo nie potrzeba nam w prawo, ale przy­da­łoby się nagle zmie­nić kie­ru­nek do dołu lub do góry nie pusz­cza­jąc kla­wi­sza do jazdy w lewo.

    Pozdra­wiam
    PS. Pro­szę o ska­so­wa­nie postu poprzed­niego postu!

    Komentarz by geshu — 8 czerwca 2010, 13:28

  9. To poru­sza­nie się wynika z pry­mi­tyw­nej obsługi trzy­ma­nych kla­wi­szy. Kod wymie­nię wkrótce. Teraz po pro­stu mam urwa­nie głowy w związku z innym projektem.

    Pozdra­wiam

    PS: rozu­miem, że cho­dziło o usu­nię­cie poprzed­niego komentarza?

    Komentarz by Michał Środek — 8 czerwca 2010, 19:16

  10. Dokład­nie tak — komentarza

    Komentarz by geshu — 9 czerwca 2010, 7:10

Kanał RSS z komentarzami do tego wpisu. TrackBack URL

Dodaj komentarz