一個(gè)HTML 5 躲避游戲的實(shí)現(xiàn)
前段時(shí)間BrowserQuest激起了我對(duì)html5的樂趣,接下來(lái)記下一個(gè)小型html5躲避游戲的實(shí)現(xiàn)。
先上圖
游戲很簡(jiǎn)單,鍵盤控制人物上下左右移動(dòng),躲開怪物,時(shí)間越長(zhǎng)越牛x。
主要是兩部分組成:一部分就是人物、地圖的結(jié)構(gòu)搭建,另一部分就是讓英雄、怪物相應(yīng)地動(dòng)起來(lái)。
HTML5寫游戲和傳統(tǒng)的游戲思路完全一樣,同樣也是不停刷新屏幕,游戲?qū)嶋H上也就是圖片的適時(shí)擺放問題,HTML5無(wú)非就只用到了一個(gè)canvas(畫布)的性質(zhì)用來(lái)擺放圖片。
Step 1 做好準(zhǔn)備
新建一個(gè)html文件,命名為index.html,用作游戲的容器。代碼如下:
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <title>html5 game</title>
- </head>
- <body>
- <h1>html5 game</h1>
- <script type="text/javascript" src="move.js"></script>
- </body>
- </html>
ps:script的引用最好放在body里放在body會(huì)有問題。
再新建個(gè)文件,move.js
- var canvas = document.createElement("canvas"); //創(chuàng)建元素canvas,即我們要用的畫布
- var ctx = canvas.getContext("2d");//說明我們要用的畫布是2d,因?yàn)閏anvas也有WebGL支持3d
- canvas.width = 512;//設(shè)置畫布的長(zhǎng)寬
- canvas.height = 480;
- document.body.appendChild(canvas);//前面基本信息都設(shè)置好了之后,將這個(gè)元素添加到body標(biāo)簽下。
這樣畫布就算是搭建好了。
順帶在下面加幾個(gè)和圖片有關(guān)的函數(shù)。
- var bgReady = false;
- var bgImage = new Image();
- bgImage.src = "move/background.png";
- bgImage.onload = function(){
- bgReady = true;
- }
- var heroReady = false;
- var heroImage = new Image();
- heroImage.src = "move/hero.png";
- heroImage.onload = function(){
- heroReady = true;
- }
- var monsterReady = false;
- var monsterImage = new Image();
- monsterImage.src = "move/monster.png";
- monsterImage.onload = function(){
- monsterReady = true;
- }
這個(gè)游戲用了三張圖片,依次為背景、英雄、怪物。這段代碼很容易理解,為了不在圖片沒有加載完成的時(shí)候就draw圖片。
#p#
Step 2 定義原型
接下來(lái)定義一下英雄的原型。
- var hero = {
- speed: 256,
- x: canvas.width/2,
- y: canvas.height/2
- }
這個(gè)原型也很好理解,每秒鐘英雄可以移動(dòng)256個(gè)像素,英雄初始的位置為畫布中央(x,y分別為坐標(biāo))。
接下來(lái)輪到怪物了。: )
- function monster() {
- this.x = Math.random() * canvas.width;//初始為止隨機(jī)
- this.y = Math.random() * canvas.height;
- this.speed = 100;
- this.xDirection = 1;//默認(rèn)移動(dòng)方向?yàn)閤軸正方向(以左上角為零點(diǎn),下方和右方為正)
- this.yDirection = 1;//同樣也為y軸正方向
- this.move = function (modifier) {//移動(dòng)函數(shù)
- this.x += this.xDirection * this.speed * modifier;
- this.y += this.yDirection * this.speed * modifier;
- if (this.x >= canvas.width - 32)//碰撞返回部分
- {
- this.x = canvas.width - 32;
- this.xDirection = -1;
- }else if (this.x <= 0)
- {
- this.x = 0;
- this.xDirection = 1;
- }else if (this.y >= canvas.height - 32)
- {
- this.y = canvas.height - 32;
- this.yDirection = -1;
- }else if (this.y <= 0)
- {
- this.y = 0;
- this.yDirection = 1;
- }
- };
- }
怪物比英雄的定義要復(fù)雜得多。首先,怪物每隔五秒會(huì)增加一個(gè)(為增加難度),故不能單純創(chuàng)建一個(gè)數(shù)組,而是需要一個(gè)類,再用類創(chuàng)建怪物對(duì)象。js當(dāng)中只有類的半實(shí)現(xiàn),具體使用function來(lái)創(chuàng)建。然后,怪物需要有撞墻返回的性質(zhì)。
怪物的速度比英雄略慢,為100像素/秒。默認(rèn)坐標(biāo)在畫布當(dāng)中隨機(jī)。怪物以45度移動(dòng)。xDirection,yDirection合起來(lái)表示左上、左下、右上、右下4個(gè)方向。然后monster這個(gè)類有個(gè)move的動(dòng)作,modifier表示兩次刷新的時(shí)間間隔,可以計(jì)算出經(jīng)過時(shí)間間隔后怪物的坐標(biāo)。下面4個(gè)if函數(shù)用來(lái)判斷,是否超越邊界,超越則馬上轉(zhuǎn)向,以實(shí)現(xiàn)碰撞的效果。
- var monsterSum = 0;
- var monsterList = new Array();
- monsterList[monsterSum] = new monster();
前面用var已經(jīng)創(chuàng)建了英雄的實(shí)例,但是monster我們只建立了類而已,接下來(lái)要實(shí)例化。monsterSum表示怪物的數(shù)量,為方便,按照c的習(xí)慣,從0開始技術(shù),即0表示有一個(gè)怪物。monsterList用來(lái)表示一個(gè)存怪物對(duì)象的數(shù)組,然后順帶新建一個(gè)怪物。
#p#
Step 3 游戲動(dòng)起來(lái)!
先添加一個(gè)事件來(lái)接收鍵盤的動(dòng)作,上下左右用對(duì)應(yīng)的ascii碼。因?yàn)橛螒虿⒉皇寝粢幌路较蜴I,就移動(dòng)一段距離。而是,判斷一個(gè)時(shí)間間隔內(nèi)的動(dòng)作(最后的動(dòng)作,中間有可能會(huì)變化,故用數(shù)組保存最后結(jié)果)。
- var keysDown = {};
- addEventListener("keydown", function (e) {
- keysDown[e.keyCode] = true;//如果有"keydown"這個(gè)動(dòng)作,即摁下某鍵,就會(huì)存進(jìn)keysDown數(shù)組
- }, false);
- addEventListener("keyup", function (e) {
- delete keysDown[e.keyCode];
- });
下面上主函數(shù)
- var main = function () {
- var now = Date.now();
- var delta = now - then;
- Move(delta / 1000);//每次間隔時(shí)間根本不是1ms,比1ms要大得多
- Draw();
- Check();
- then = now;
- }
main函數(shù)就是主函數(shù),就是一個(gè)刷新所執(zhí)行的函數(shù)。now、then兩個(gè)變量記錄兩次刷新的時(shí)間間隔,這個(gè)時(shí)間間隔并不是固定的,一般為幾百毫秒。delta 是兩者之差,單位為毫秒。下面依次解釋各個(gè)函數(shù):Move()用來(lái)計(jì)算英雄和怪物的新位置。Draw()用來(lái)畫背景、人物、文字。Check()用來(lái)檢查,怪物和英雄是否相撞。
Move():
- var Move = function (modifier) {
- if (38 in keysDown) {
- hero.y -= hero.speed * modifier;
- }
- if (40 in keysDown) {
- hero.y += hero.speed * modifier;
- }
- if (37 in keysDown) {
- hero.x -= hero.speed * modifier;
- }
- if (39 in keysDown) {
- hero.x += hero.speed * modifier;
- }
- if (hero.x >= canvas.width - 32) {
- hero.x = 0;
- }else if (hero.x <= 0) {
- hero.x = canvas.width - 32;
- }
- if (hero.y >= canvas.height - 32) {
- hero.y = 0;
- }else if (hero.y <= 0) {
- hero.y = canvas.height - 32;
- }
- for (var i = 0; i <= monsterSum; i++) {
- monsterList[i].move(modifier);
- }
- }
這里很好理解,判斷這段時(shí)間間隔英雄的動(dòng)作。算出新位置后,判斷英雄是否跑出了畫布,跑出了就從另一頭出來(lái)(感謝 @昭曈 的創(chuàng)意)。接下來(lái)依次調(diào)用各個(gè)怪物的move函數(shù),計(jì)算他們的新位置。
Draw():
- var Draw = function () {
- if (bgReady) {
- ctx.drawImage(bgImage, 0 ,0);
- }
- if (heroReady) {
- ctx.drawImage(heroImage, hero.x, hero.y);
- }
- if (monsterReady) {
- for (var i = 0; i <= monsterSum; i++)
- ctx.drawImage(monsterImage, monsterList[i].x, monsterList[i].y);
- }
- ctx.fillStyle = "rgb(250, 250, 250)";
- ctx.font = "24px Helvetica";
- ctx.textAlign = "left";
- ctx.textBaseline = "top";
- last = Date.now() - start;
- ctx.fillText(last/1000, 32, canvas.height - 64);
- }
前三個(gè)函數(shù)類似,就是如果準(zhǔn)備好了圖片就畫東西上去(前面的三個(gè)ready函數(shù)派上了用場(chǎng))。下面先定義了文字的style,然后計(jì)算出時(shí)間間隔last,然后畫上去。
Check():
- var Check = function () {
- if (monsterSum != Math.floor(last / 5000)){//如果時(shí)間經(jīng)過5秒就增加一個(gè)怪獸實(shí)例
- monsterSum ++;
- monsterList[monsterSum] = new monster();
- }
- for (var i = 0; i <= monsterSum; i++) {
- if (
- (monsterList[i].x - 32) <= hero.x
- && hero.x <= (monsterList[i].x + 32)
- && (monsterList[i].y - 32) <= hero.y
- && hero.y <= (monsterList[i].y + 32)
- ) {
- end = Date.now();
- alert("你堅(jiān)持了" + (end - start)/1000 + "秒");
- End();
- }
- }
- }
第一步是 如果經(jīng)過5秒就增加一個(gè)怪獸,然后下面一個(gè)個(gè)判斷怪獸與英雄是否接觸。(這里用的是矩形,@小樟 說可以用圓心,感興趣的可以試試: ) )
大家注意到了下面用到了一個(gè)end函數(shù)。下面補(bǔ)充,如果不停止一直刷新就瀏覽器就一直動(dòng)了,故要有個(gè)結(jié)束函數(shù)。
End():
- var End = function () {
- if (bgReady) {
- ctx.drawImage(bgImage, 0 ,0); //留住背景
- }
- window.clearInterval(timer);
- return;
- }
用到的clearInterval()稍后會(huì)說到。
#p#
Step 4 程序入口
- var then = Date.now();
- var start = then;
- timer = setInterval(main, 1);
定義了初始的then和start,接下里用了setInterval(),意為每隔1ms就執(zhí)行一次main函數(shù)。1ms是虛指,就是立即執(zhí)行的意思。要想停止就用前面的clearInterval()函數(shù)。
到這里為止一個(gè)完整的游戲就寫好了~ : )
后來(lái) @志謙 實(shí)踐出了一個(gè)bug,就是窗口如果最小化后,會(huì)一直計(jì)時(shí),但不會(huì)被撞。問題在于高級(jí)的瀏覽器(當(dāng)然沒在說IE : ) ),默認(rèn)如果最小化后,會(huì)終止interval、timeout這類函數(shù)的執(zhí)行,以節(jié)約資源。但我們計(jì)時(shí)的方法是結(jié)束時(shí)間減去開始時(shí)間,所以就造成這個(gè)刷分的bug。感謝 @小樟 提供了main函數(shù)遞歸,全局變量表示是否停止的辦法。
哦,第一次就那么沒了~
源碼下載地址:html5game