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

06 Lipiec 2010

Optymalizacja obiektu canvas

Filed under: Inne,JavaScript,Moje projekty — Michał Środek @ 20:32

Jak zapewne zauwa­ży­łeś pod­czas czy­ta­nia mojego poprzed­niego arty­kułu gra, którą two­rzę działa troszkę wolno. Dzieje się tak dla­tego, że mapa jest gene­ro­wana od nowa w każ­dej klatce dzia­ła­nia pro­gramu. Spró­bujmy to zoptymalizować.

Zasada jest bar­dzo pro­sta. To co możemy gene­ro­wać raz, gene­ru­jemy tylko raz. Następ­nie zapi­su­jemy wynik do oddziel­nego obiektu <canvas> i w razie potrzeby kopiu­jemy do głów­nego obiektu naszej gry.

Na potrzeby mojego przy­kładu stwo­rzy­łem dyna­micz­nie dwa nowe obiekty canvas, jeden prze­cho­wu­jący wyge­ne­ro­waną mapę, drugi czołg(obracanie obraz­ków jest bar­dzo czasochłonne).

Upo­rząd­ko­wa­łem troszkę kod umiesz­cza­jąc wszel­kie 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 ana­lo­giczny spo­sób two­rzony 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 jedy­nie składa obraz ze wszyst­kich 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 gene­ro­wa­nia gra­fiki, cza­so­chłonną ope­ra­cją jest licze­nie czasu. Spró­bujmy ogra­ni­czyć wywo­ły­wa­nie pew­nych funk­cji w licz­niku 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 przy­spie­sze­niem gry zauwa­żymy rów­nież zmiany w dzia­ła­niu samego licz­nika. Jego odświe­ża­nie będzie rzad­sze i jed­no­cze­śnie przy­jem­niej­sze dla ludz­kiego oka.

Tech­nika, którą tutaj poka­za­łem bar­dzo przy­po­mina tą ze zwy­kłych gier DHTML budo­wa­nych w opar­ciu o odpo­wied­nio pozy­cjo­no­wane znacz­niki <div>. Dla­czego nie robię tak samo w przy­padku znacz­ni­ków <canvas>? Po pro­stu sądzę, że takie roz­wią­za­nie jest mniej ele­ganc­kie. Two­rze­nie jed­nego obrazu lepiej przy­po­mina kla­syczne gry kom­pu­te­rowe. Czas( oraz licz­nik FPS) pokaże, czy nie zmie­nię zdania…

Dzia­ła­jące demo dostępne jest TUTAJ.

Dla­czego tak długo nie pisa­łem? No cóż mia­łem sporo pracy, a czer­wiec to nie naj­lep­szy czas aby odna­leźć dodat­ko­wych kilka godzin. Jutro posta­ram się opi­sać coś bar­dziej skom­pli­ko­wa­nego ;) .

Dodaj arty­kuł do:

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

Komentarzy: 7 »

  1. Lepiej niż ostat­nio. Wcze­śniej na naj­now­szym 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

  2. Dziwne, u mnie wie­cej niż 50 — 55 nie może uro­dzić na dewe­lo­per­skim chromie.

    Komentarz by cypherq — 7 lipca 2010, 19:23

  3. Wszystko faj­nie tylko mnie kolą w oczy 2 sprawy.

    Po pierw­sze raz uży­wasz this a raz nazwy zmien­nej obiektu, utrud­nia to tro­chę ana­lizę kodu. Ofc. w nie­któ­rych przy­pad­kach sama zamiana nazwy zmien­nej na „this“ nie pomoże, np. metoda pre­lo­ad­Te­xtu­res z Game­Bo­ard i funk­cja zda­rze­nie load obrazka. Nale­ża­łoby zapi­sać kon­tekst this przed funk­cją zda­rze­nia i z tego w jej wnę­trzu korzy­stać (btw. tro­chę to męczące i szcze­rze nie podoba mi się takie roz­wią­za­nie ale innego/lepszego nie znam). Ot taka pier­doła, ale myślę, że warto to poprawić.

    Po dru­gie, już bar­dziej zna­czące, dla­czego nie defi­niu­jesz klas tylko gołe obiekty, które są sin­gle­to­nami? Ogra­ni­czasz w ten spo­sób funk­cjo­nal­ność, bo nie będzie można stwo­rzyć dwóch gier na jed­nej stro­nie. Dość absur­dalny przy­kład ale jed­nak taka moż­li­wość imho powinna być. A czołgi ste­ro­wane przez kom­pu­ter? Powinna być klasa Tank, a Game­Tank powinna być obiek­tem tejże klasy czy tam nawet Tank powi­nien być klasą abs­trak­cyjną i po niej dzie­dzi­czą Com­pu­ter­Tank oraz Human­Tank… Tak tro­chę nie z duchem OOP.

    Nato­miast patrząc od end-usera efekt jest bar­dzo fajny ;)

    Komentarz by luq — 10 lipca 2010, 19:15

  4. Dzię­kuje luq za kon­kretny komen­tarz :)

    1. Nie zawsze można użyć this. Wtedy po pro­stu sto­suję nazwę obiektu

    2. Po pro­stu nie prze­wi­dzia­łem, że ktoś będzie chciał uru­cho­mić 2 gry na jed­nej stro­nie. Ope­ra­cje zwią­zane z canva­sem i tak zże­rają ogromną ilość zaso­bów. Kilka gier obok sie­bie po pro­stu by wolno dzia­łało. Co do ogól­nej budowy klas obiek­tów, wzor­ców etc. — wszystko będzie powoli wymieniane/poprawiane. Ja nie mam napi­sa­nej całej gry. Piszę ją na bie­żąco two­rząc kolejne arty­kuły. W każ­dym razie dzię­kuję za uwagi. Na pewno się dosto­suję ;) .

    PS: zawszę mogę zro­bić jeden wielki pojem­nik — klasę Game i w ten spo­sób two­rzyć kolejne instan­cje gry ;) . Pro­ble­mem jed­nak będzie zde­cy­do­wa­nie obsługa zda­rzeń itp. Imho strasz­nie dużo zachodu.

    Komentarz by Michał Środek — 10 lipca 2010, 19:24

  5. 1. No wła­śnie można :) Albo this albo uży­wasz zmien­nej, która jest wskaź­ni­kiem na to co poka­zy­wał this wcześniej:

    var Game­Bo­ard = {
    pre­lo­ad­Te­xtu­res: function(i){
    (…)
    var thi­sIn­stance = this;

    img.onload = func­tion() {
    thisInstance.textures[i] = GameBoard.ctx.createPattern(img, ‚repeat’);
    (…)
    }
    },
    (…)
    }

    2. Tak, tak, rozu­miem. Wiem jak to jest bo sam teraz na swoim blogu pisze, w sumie coś podob­nego, i mój kod też ewo­lu­uje z każ­dym wpisem :]

    Owoc­nego kodze­nia! ;)

    Komentarz by luq — 11 lipca 2010, 10:40

  6. 1. Rozu­miem o co cho­dzi. Uży­wam bar­dzo czę­sto iden­tycz­nej kon­struk­cji gdy piszę pod jQu­ery tj.
    var $$ = $(this);

    PS: zapo­mnia­łeś zamie­nić jedno Game­Bo­ard ;)
    img.onload = func­tion() {
    thisInstance.textures[i] = thisInstance.ctx.createPattern(img, ‚repeat’);
    (…)
    }

    2. Wiem widzia­łem. Cie­kawe arty­kuły. Dorzu­ci­łem Cię nawet do blo­grolla ;) .

    Komentarz by Michał Środek — 11 lipca 2010, 12:51

  7. No dokład­nie, pod jQu­ery też czę­sto takie „cuda“ trzeba robić :)

    Ups… Widocz­nie cho­chliki pozmie­niały treść pakie­tów idą­cych tuż po klik­nię­ciu „Wyślij komen­tarz“ :D

    Dzięki za blo­grollowanie mojego bloga :) U sie­bie zro­bi­łem to samo ;]

    Komentarz by luq — 11 lipca 2010, 17:48

Kanał RSS z komentarzami do tego wpisu. TrackBack URL

Dodaj komentarz