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
Jak zapewne zauważyłeś podczas czytania mojego poprzedniego artykułu gra, którą tworzę działa troszkę wolno. Dzieje się tak dlatego, że mapa jest generowana od nowa w każdej klatce działania programu. Spróbujmy to zoptymalizować.
Zasada jest bardzo prosta. To co możemy generować raz, generujemy tylko raz. Następnie zapisujemy wynik do oddzielnego obiektu <canvas> i w razie potrzeby kopiujemy do głównego obiektu naszej gry.
Na potrzeby mojego przykładu stworzyłem dynamicznie dwa nowe obiekty canvas, jeden przechowujący wygenerowaną mapę, drugi czołg(obracanie obrazków jest bardzo czasochłonne).
Uporządkowałem troszkę kod umieszczając wszelkie dane czołgu do klasy GameTank
var GameTank = { x:0, y:480, canvas: null, ctx: null, direction: 1, // 0= LEFT 1 =UP, 2 = RIGHT, 3=DOWN changeDirection: function(newDirection) { if(newDirection!=GameTank.direction) { GameTank.direction = newDirection; GameTank.ctx.clearRect(0,0,32,32); GameTank.ctx.save(); switch(GameTank.direction) { case 2: GameTank.ctx.rotate(90 * Math.PI / 180); GameTank.ctx.drawImage(GameMain.img.tank,0,-32); break; case 3: GameTank.ctx.rotate(180 * Math.PI / 180); GameTank.ctx.drawImage(GameMain.img.tank,-32,-32); break; case 0: GameTank.ctx.rotate(270 * Math.PI / 180); GameTank.ctx.drawImage(GameMain.img.tank,-32,0); break; default: GameTank.ctx.drawImage(GameMain.img.tank,0,0); } GameTank.ctx.restore(); } }, init: function() { this.canvas = document.createElement('canvas'); this.canvas.width = 32; this.canvas.height = 32; this.ctx = this.canvas.getContext('2d'); this.ctx.drawImage(GameMain.img.tank,0,0); } };
W analogiczny sposób tworzony jest obiekt <cavas> dla mapy
var GameBoard = { canvas: null, ctx: null, TEXTURE_ID: {ICE: 1, BRICK: 2, WATER: 3}, textures: [null, 'images/ice.png', 'images/brick.png', 'images/water.png'], board:[ [1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1], [1,1,1,2,2,2,2,2,2,2,2,2,2,1,1,1], [1,1,2,2,2,2,2,2,2,2,2,2,2,2,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,2,1,1,2,2,2,2,2,2,1,1,2,2,1], [2,2,2,1,3,2,2,2,2,2,2,1,3,2,2,2], [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2], [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2], [2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2], [0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0], [0,2,2,2,1,2,2,2,2,2,2,1,2,2,2,0], [0,2,2,2,2,1,1,1,1,1,1,2,2,2,2,0], [0,0,2,2,2,2,2,2,2,2,2,2,2,2,0,0], [0,0,0,2,2,2,2,2,2,2,2,2,2,0,0,0], [0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ], drawBoard: function() { this.ctx.clearRect(0,0,512,512); for(var y=0; y<16; y++) { for(var x=0; x<16; x++) { if(this.board[y][x]>0) { this.ctx.save(); this.ctx.fillStyle = this.textures[this.board[y][x]]; this.ctx.fillRect(x*32,y*32,32,32); this.ctx.restore(); } } } }, 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{ GameBoard.drawBoard(); GameTank.init(); window.setInterval(GameMain.mainLoop,10); } } }, init: function (){ this.canvas = document.createElement('canvas'); this.canvas.width = 512; this.canvas.height = 512; this.ctx = this.canvas.getContext('2d'); this.preloadTextures(1); } };
Główna klasa gry jedynie składa obraz ze wszystkich elementów
var GameMain = { canvas: null, ctx: null, img: {}, mainLoop: function(){ GameMain.ctx.clearRect(0,0,512,512); GameMain.ctx.drawImage(GameBoard.canvas, 0, 0,512,512,0,0,512,512); if(keyHandler.isHolded(37)) // LEFT { GameTank.changeDirection(0); if(GameTank.x>0) GameTank.x-=1; } else if(keyHandler.isHolded(38)) // UP { GameTank.changeDirection(1); if(GameTank.y>0) GameTank.y-=1; } else if(keyHandler.isHolded(39)) // RIGHT { GameTank.changeDirection(2); if(GameTank.x<480) GameTank.x+=1; } else if(keyHandler.isHolded(40)) // DOWN { GameTank.changeDirection(3); if(GameTank.y<480) GameTank.y+=1; } GameMain.ctx.drawImage(GameTank.canvas, 0, 0,32,32,GameTank.x,GameTank.y,32,32); FrameCounter.updateFPS(); }, init: function(){ this.canvas = document.getElementById('game'); if (this.canvas.getContext) this.ctx = this.canvas.getContext("2d"); this.img.tank = new Image(); this.img.tank.src = 'images/btank.png'; this.img.tank.onload = function(){ GameBoard.init(); } document.addEventListener('keydown', keyHandler.keyDown, false); document.addEventListener('keyup', keyHandler.keyUp, false); } }
Kolejną, obok generowania grafiki, czasochłonną operacją jest liczenie czasu. Spróbujmy ograniczyć wywoływanie pewnych funkcji w liczniku FPS. Czas liczył będę co 10 klatek.
var FrameCounter = { x: 0, lastFrameTime: new Date().getTime(), updateFPS: function(){ if(this.x==10) { var currentTime = new Date().getTime(); document.getElementById('fps').innerHTML = Math.floor(10000/(currentTime-this.lastFrameTime),2)+'fps'; this.lastFrameTime = currentTime; this.x=0; } else this.x++ } };
Poza przyspieszeniem gry zauważymy również zmiany w działaniu samego licznika. Jego odświeżanie będzie rzadsze i jednocześnie przyjemniejsze dla ludzkiego oka.
Technika, którą tutaj pokazałem bardzo przypomina tą ze zwykłych gier DHTML budowanych w oparciu o odpowiednio pozycjonowane znaczniki <div>. Dlaczego nie robię tak samo w przypadku znaczników <canvas>? Po prostu sądzę, że takie rozwiązanie jest mniej eleganckie. Tworzenie jednego obrazu lepiej przypomina klasyczne gry komputerowe. Czas( oraz licznik FPS) pokaże, czy nie zmienię zdania…
Działające demo dostępne jest TUTAJ.
Dlaczego tak długo nie pisałem? No cóż miałem sporo pracy, a czerwiec to nie najlepszy czas aby odnaleźć dodatkowych kilka godzin. Jutro postaram się opisać coś bardziej skomplikowanego
.
Lepiej niż ostatnio. Wcześniej na najnowszym FF miałem ok. 66–72 FPS, teraz jest to 80–90. Na Chrome podobnie.
Komentarz by Oskar Wójcicki — 7 lipca 2010, 10:17
Dziwne, u mnie wiecej niż 50 — 55 nie może urodzić na deweloperskim chromie.
Komentarz by cypherq — 7 lipca 2010, 19:23
Wszystko fajnie tylko mnie kolą w oczy 2 sprawy.
Po pierwsze raz używasz this a raz nazwy zmiennej obiektu, utrudnia to trochę analizę kodu. Ofc. w niektórych przypadkach sama zamiana nazwy zmiennej na „this“ nie pomoże, np. metoda preloadTextures z GameBoard i funkcja zdarzenie load obrazka. Należałoby zapisać kontekst this przed funkcją zdarzenia i z tego w jej wnętrzu korzystać (btw. trochę to męczące i szczerze nie podoba mi się takie rozwiązanie ale innego/lepszego nie znam). Ot taka pierdoła, ale myślę, że warto to poprawić.
Po drugie, już bardziej znaczące, dlaczego nie definiujesz klas tylko gołe obiekty, które są singletonami? Ograniczasz w ten sposób funkcjonalność, bo nie będzie można stworzyć dwóch gier na jednej stronie. Dość absurdalny przykład ale jednak taka możliwość imho powinna być. A czołgi sterowane przez komputer? Powinna być klasa Tank, a GameTank powinna być obiektem tejże klasy czy tam nawet Tank powinien być klasą abstrakcyjną i po niej dziedziczą ComputerTank oraz HumanTank… Tak trochę nie z duchem OOP.
Natomiast patrząc od end-usera efekt jest bardzo fajny
Komentarz by luq — 10 lipca 2010, 19:15
Dziękuje luq za konkretny komentarz
1. Nie zawsze można użyć this. Wtedy po prostu stosuję nazwę obiektu
2. Po prostu nie przewidziałem, że ktoś będzie chciał uruchomić 2 gry na jednej stronie. Operacje związane z canvasem i tak zżerają ogromną ilość zasobów. Kilka gier obok siebie po prostu by wolno działało. Co do ogólnej budowy klas obiektów, wzorców etc. — wszystko będzie powoli wymieniane/poprawiane. Ja nie mam napisanej całej gry. Piszę ją na bieżąco tworząc kolejne artykuły. W każdym razie dziękuję za uwagi. Na pewno się dostosuję
.
PS: zawszę mogę zrobić jeden wielki pojemnik — klasę Game i w ten sposób tworzyć kolejne instancje gry
. Problemem jednak będzie zdecydowanie obsługa zdarzeń itp. Imho strasznie dużo zachodu.
Komentarz by Michał Środek — 10 lipca 2010, 19:24
1. No właśnie można
Albo this albo używasz zmiennej, która jest wskaźnikiem na to co pokazywał this wcześniej:
var GameBoard = {
preloadTextures: function(i){
(…)
var thisInstance = this;
img.onload = function() {
thisInstance.textures[i] = GameBoard.ctx.createPattern(img, ‚repeat’);
(…)
}
},
(…)
}
2. Tak, tak, rozumiem. Wiem jak to jest bo sam teraz na swoim blogu pisze, w sumie coś podobnego, i mój kod też ewoluuje z każdym wpisem :]
Owocnego kodzenia!
Komentarz by luq — 11 lipca 2010, 10:40
1. Rozumiem o co chodzi. Używam bardzo często identycznej konstrukcji gdy piszę pod jQuery tj.
var $$ = $(this);
PS: zapomniałeś zamienić jedno GameBoard
img.onload = function() {
thisInstance.textures[i] = thisInstance.ctx.createPattern(img, ‚repeat’);
(…)
}
2. Wiem widziałem. Ciekawe artykuły. Dorzuciłem Cię nawet do blogrolla
.
Komentarz by Michał Środek — 11 lipca 2010, 12:51
No dokładnie, pod jQuery też często takie „cuda“ trzeba robić
Ups… Widocznie chochliki pozmieniały treść pakietów idących tuż po kliknięciu „Wyślij komentarz“
Dzięki za blogrollowanie mojego bloga
U siebie zrobiłem to samo ;]
Komentarz by luq — 11 lipca 2010, 17:48