網頁HTML5--飛機大戰小遊戲開發--canvas的應用
一,概述
此小專案,是用來練習HTML5的canvas的程式設計運用。在這個專案中,我們需要建立一個可執行的網頁小遊戲,開發此小遊戲並不難,大概如下圖所示:
在整個遊戲的執行中,總共要分為5個狀態(state)去實現,分別是首頁(START),載入中(STARTING),遊戲中(RUNNING),暫停(PAUSE)和遊戲結束(GAME_OVER),運用一個計時器在網頁的canvas畫布上畫出對應的圖片,再通過滑鼠的事件來觸發並轉換狀態,就可以實現遊戲的運行了。大體程式碼框架:
<div id="stage" style="width:480px;height:650px;margin:0px auto;"> <canvas id="canvas" width="480" height="650"></canvas> </div> <script> //定義寬和高 var WIDTH=480,HEIGHT=650; //定義狀態 var START = 0; var STARTING = 1; var RUNNING = 2; var PAUSE = 3; var GAME_OVER = 4; /** * state表示遊戲的狀態 * 取值必須為以上五種之一 */ var state = START; //獲取資料 var canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); //建立影象物件用來表示天空、英雄、敵人、版權 //為canvas新增事件(onclick,onmousemove,onmoseout) //資料物件 //業務物件 //建立業務物件 //定義計時器,固定重新整理頻率為 1000 / 100 setInterval(function(){ switch(state){ case START: //首頁 break; case STARTING: //載入中 break; case RUNNING: //遊戲中 break; case PAUSE: //暫停 break; case GAME_OVER: //遊戲結束 break; } },1000/100); </script>
二,首頁
編寫好大體框架後,接下來,我們要逐步逐步完成各個狀態的程式碼。首頁很簡單的,包括兩元素,一個是背景天空的圖片(持續由上往下迴圈滾動),一個是飛機大戰的圖示(固定在網頁正中間)。如下圖所示:
對應程式碼如下,將它補充到上述框架中:
<script> //建立影象物件用來表示天空 var bg = new Image(); bg.src="img/background.png"; //建立影象物件用來表示圖示 var copyright = new Image(); copyright.src="img/shoot_copyright.png"; //資料物件 var SKY = {image:bg,width:480,height:650,speed:20}; var LOADING = {frames:l,width:186,height:38,x:0,y:HEIGHT-38,speed:5}; //業務物件 /** * 天空的業務物件 * config:表示天空的資料物件 */ var Sky = function(config){ this.bg = config.image; //設定背景影象 this.width=config.width; this.height = config.height; this.speed = 1000 / config.speed; this.x1 = 0; this.y1 = 0; this.x2 = 0; this.y2 = -this.height; this.lastTime = 0; //上一次執行動作時間的毫秒數 /** * 移動背景縱座標 */ this.step = function(){ //判斷是否到達天空移動的時間 //獲取當前時間的毫秒數 var currentTime = new Date().getTime(); if(currentTime - this.lastTime >= this.speed){ // y1++ , y2++ this.y1++; this.y2++; this.lastTime = new Date().getTime(); } //判斷y1、y2 是否超出範圍 if(this.y1 >= this.height){ this.y1 = -this.height; } if(this.y2 >= this.height){ this.y2 = -this.height; } } /** * 繪製天空影象 *ctx : canvas 繪圖上下文 */ this.paint = function(ctx){ ctx.drawImage(this.bg,this.x1,this.y1); ctx.drawImage(this.bg,this.x2,this.y2); } } //建立業務物件 var sky = new Sky(SKY); //以下程式碼放入計時器中 case START: //空在移動 sky.step(); sky.paint(ctx); //繪製copyright var x = (WIDTH - copyright.naturalWidth) / 2; var y = (HEIGHT - copyright.naturalHeight) / 2; ctx.drawImage(copyright,x,y); break; <script>
三,載入中
載入中的狀態相對於首頁就複雜一點了,載入狀態是由首頁進行滑鼠單擊切換而來,載入完畢即進入遊戲中的狀態,同樣是天空的背景滾動(天空全程都在滾動,即使暫停或者遊戲結束頁在滾動),但是,下面需要有一架飛機飛過表示載入進度,這由4幅圖切換而成。如下圖所示:
對應程式碼如下所示:
<script> //建立陣列儲存載入的四個圖片物件 var l = []; l[0] = new Image(); l[0].src="img/game_loading1.png"; l[1] = new Image(); l[1].src="img/game_loading2.png"; l[2] = new Image(); l[2].src="img/game_loading3.png"; l[3] = new Image(); l[3].src="img/game_loading4.png"; //為canvas新增事件(onclick,onmousemove,onmoseout) canvas.onclick=function(){ if(state == START){ state = STARTING; } } //資料物件 var LOADING = {frames:l,width:186,height:38,x:0,y:HEIGHT-38,speed:5}; //業務物件 /** * 載入的業務物件 * config:表示載入的資料物件 */ var Loading = function(config){ this.speed = 1000 / config.speed; this.lastTime = 0; this.frame=null; this.frameIndex = 0; //更換loading影象 this.step = function(){ var currentTime = new Date().getTime(); if(currentTime - this.lastTime >= this.speed){ //獲取不同的影象config.frames中的元素給frame this.frame = config.frames[this.frameIndex]; this.frameIndex ++; if(this.frameIndex >= 4){ //更新狀態 state = RUNNING; } this.lastTime = new Date().getTime(); } } /** * 繪製不同的影象到canvas上 */ this.paint = function(ctx){ ctx.drawImage(this.frame,config.x,config.y); } } //建立業務物件 var loading = new Loading(LOADING); //以下程式碼放入計時器中 case STARTING: //準備開始 sky.step(); sky.paint(ctx); loading.step(); loading.paint(ctx); break; </script>
三,遊戲中
遊戲中的狀態是最複雜的,裡面需要有滾動的天空,我們自己控制的飛機(跟隨滑鼠位置移動),子彈(自動從飛機頭髮射),敵方飛機(隨機從上方生成,不會攻擊,有三種類型,按大小分為大中小),分數(score)以及生命值(life)。大致如下圖所示:
對應程式碼如下所示:
<script>
//建立英雄影象陣列
var h = [];
h[0] = new Image();
h[0].src="img/hero1.png";
h[1] = new Image();
h[1].src="img/hero2.png";
h[2] = new Image();
h[2].src="img/hero_blowup_n1.png";
h[3] = new Image();
h[3].src="img/hero_blowup_n2.png";
h[4] = new Image();
h[4].src="img/hero_blowup_n3.png";
h[5] = new Image();
h[5].src="img/hero_blowup_n4.png";
//建立子彈影象
var b = new Image();
b.src="img/bullet1.png";
//建立小飛機影象陣列
var e1 = [];
e1[0] = new Image();
e1[0].src="img/enemy1.png";
e1[1] = new Image();
e1[1].src="img/enemy1_down1.png";
e1[2] = new Image();
e1[2].src="img/enemy1_down2.png";
e1[3] = new Image();
e1[3].src="img/enemy1_down3.png";
e1[4] = new Image();
e1[4].src="img/enemy1_down4.png";
//建立中型飛機影象陣列
var e2 = [];
e2[0] = new Image();
e2[0].src="img/enemy2.png";
e2[1] = new Image();
e2[1].src="img/enemy2_down1.png";
e2[2] = new Image();
e2[2].src="img/enemy2_down2.png";
e2[3] = new Image();
e2[3].src="img/enemy2_down3.png";
e2[4] = new Image();
e2[4].src="img/enemy2_down4.png";
//建立大型飛機的影象陣列
var e3 = [];
e3[0] = new Image();
e3[0].src="img/enemy3_n1.png";
e3[1] = new Image();
e3[1].src="img/enemy3_n2.png";
e3[2] = new Image();
e3[2].src="img/enemy3_down1.png";
e3[3] = new Image();
e3[3].src="img/enemy3_down2.png";
e3[4] = new Image();
e3[4].src="img/enemy3_down3.png";
e3[5] = new Image();
e3[5].src="img/enemy3_down4.png";
e3[6] = new Image();
e3[6].src="img/enemy3_down5.png";
e3[7] = new Image();
e3[7].src="img/enemy3_down6.png";
/**
*滑鼠移動事件
*處理 hero與滑鼠的位置
*/
canvas.onmousemove = function(e){
var x = e.offsetX;
var y = e.offsetY;
hero.x = x - HERO.width / 2;
hero.y = y - HERO.height / 2;
}
//資料物件
var HERO = {frames:h,baseFrameCount:2,width:99,height:124,speed:20};
var BULLET = {image:b,width:9,height:21};
var E1 = {type:1,score:1,frames:e1,baseFrameCount:1,life:1,minSpeed:160,maxSpeed:180,width:57,height:51};
var E2 = {type:2,score:5,frames:e2,baseFrameCount:1,life:5,minSpeed:120,maxSpeed:150,width:69,height:95};
var E3 = {type:3,score:20,frames:e3,baseFrameCount:2,life:20,speed:80,width:169,height:258};
//儲存由hero發射的所有子彈
var bullets = [];
//儲存生成的敵機
var enemies = [];
//業務物件
var Enemy = function(config){
this.down = false;//是否播放爆破狀態,預設為否
this.canDelete = false;//是否刪除當前飛機,預設為否
this.life = config.life;//敵人的生命力
this.score = config.score;//分數
this.frames = config.frames;//影象列表
this.frame=null;//當前顯示的影象
this.frameIndex = 0;//當前顯示的影象索引累加值
this.baseFrameCount=config.baseFrameCount;
this.width = config.width;
this.height = config.height;
//橫縱座標
this.x = Math.ceil(Math.random()*(WIDTH - config.width));
this.y = -config.height;
this.type = config.type;
this.speed = 0;
if(config.minSpeed && config.maxSpeed){
this.speed = 1000 / (Math.random() * (config.maxSpeed - config.minSpeed) + config.minSpeed);
}else {
this.speed = 1000 / config.speed;
}
this.lastTime = 0;
/**
* 檢查時間是否到期
*/
this.timeInterval = function(){
var currentTime = new Date().getTime();
if(currentTime - this.lastTime >= this.speed){
this.lastTime = new Date().getTime();
return true;
}
return false;
}
this.step=function(){
if(this.timeInterval()){
if(this.down){
//播放爆破影象
//已經確定this.frameIndex = this.baseFrameCount;
if(this.frameIndex ==this.frames.length){
this.canDelete = true;
} else {
this.frame = this.frames[this.frameIndex];
this.frameIndex ++;
}
}else{
//播放基本影象
this.frame = this.frames[this.frameIndex % this.baseFrameCount];
this.frameIndex ++;
//飛機移動
this.move();
}
}
}
this.move = function(){
//飛機移動
this.y ++;
}
this.paint = function(ctx){
//繪製飛機影象
ctx.drawImage(this.frame,this.x,this.y);
}
/**
* 判斷當前飛機物件是否超出canvas邊界
*/
this.outOfBounds = function(){
if(this.y > HEIGHT){
return true;
}
return false;
}
/**
* 判斷敵人是否與其他物體碰撞
* c:c可以是英雄,可以是子彈
*/
this.hit = function(c){
//c的中心點座標
var cX = c.x + c.width / 2;
var cY = c.y + c.height / 2;
var leftStart = this.x - c.width / 2;
var leftEnd = this.x + this.width + c.width / 2;
var topStart = this.y - c.height / 2;
var topEnd = this.y + this.height + c.height / 2;
var result = leftStart < cX && cX < leftEnd && topStart < cY && cY < topEnd;
return result;
}
/**
* 當敵人飛機與其他元素碰撞時的操作方法
*/
this.duang = function(){
//生命的減少
this.life --;
if(this.life == 0){
//切換到爆破狀態
this.down = true;
score += this.score;
this.frameIndex = this.baseFrameCount;
}
}
}
/**
* 子彈物件
*/
var Bullet = function(config,x,y){
this.width = config.width;
this.height = config.height;
this.frame = config.image;
this.x = x;
this.y = y;
this.canDelete = false;//是否刪除子彈,預設為否
this.move = function(){
this.y -= 2;
}
this.paint = function(ctx){
ctx.drawImage(this.frame,this.x,this.y);
}
this.outOfBounds = function(){
return this.y < 0-this.height;
}
/**
* 子彈與敵人飛機碰撞時所做的操作
*/
this.duang = function(){
this.canDelete = true;
}
}
/**
* 建立飛機業務物件
*/
var Hero = function(config){
this.frames = config.frames;
this.frameIndex = 0;
this.baseFrameCount=config.baseFrameCount;
this.width=config.width;
this.height=config.height;
this.speed=1000/config.speed;
this.lastTime = 0;
this.x = (WIDTH - this.width) / 2;
this.y = HEIGHT - this.height - 30;
this.down=false;
this.canDelete = false;
this.step = function(){
var currentTime = new Date().getTime();
if(currentTime - this.lastTime >= this.speed){
if(this.down){
//爆破狀態
if(this.frameIndex == this.frames.length){
this.canDelete = true;
}else {
this.frame = this.frames[this.frameIndex];
this.frameIndex ++;
}
}else{
//正常狀態
this.frame = this.frames[this.frameIndex % this.baseFrameCount];
this.frameIndex ++;
this.lastTime = new Date().getTime();
}
}
}
this.paint = function(ctx){
ctx.drawImage(this.frame,this.x,this.y);
}
this.shootLastTime=0;
//發射子彈的間隔
this.shootInterval = 200;
//處理子彈的發射
this.shoot = function(){
var currentTime = new Date().getTime();
if(currentTime - this.shootLastTime >= this.shootInterval){
//到達時間間隔,可以發射子彈
var bullet = new Bullet(BULLET,this.x+45,this.y);
bullets[bullets.length] = bullet;
//console.log("子彈數量:"+bullets.length);
this.shootLastTime = new Date().getTime();
}
}
/**
* 英雄與敵人碰撞後的操作
*/
this.duang = function(){
this.down = true;
this.frameIndex = this.baseFrameCount;
}
}
//建立業務物件
var hero = new Hero(HERO);
/**
* 檢查 敵人是否與子彈、英雄碰撞
*/
function checkHit(){
for(var i=0;i<enemies.length;i++){
var enemy = enemies[i];
if(enemy.down || enemy.canDelete){
continue;
}
//與子彈相比較
for(var j=0;j<bullets.length;j++){
var bullet = bullets[j];
//進行比較
if(enemy.hit(bullet)){
enemy.duang();
bullet.duang();
}
}
//判斷與英雄比較
if(enemy.down || enemy.canDelete){
continue;
}
if(enemy.hit(hero)){
enemy.duang();
hero.duang();
}
}
}
//刪除多餘元件
function deleteComponent(){
//刪除超出下邊界的小飛機
for(var i=0;i<enemies.length;i++){
if(enemies[i].outOfBounds() || enemies[i].canDelete){
enemies.splice(i,1);
}
}
//刪除超出上邊界的子彈
for(var i=0;i<bullets.length;i++){
if(bullets[i].outOfBounds() || bullets[i].canDelete){
bullets.splice(i,1);
}
}
//判斷英雄是否需要被刪除
if(hero.canDelete){
life --;//減少生命
if(life == 0){
//GAME_OVER
state = GAME_OVER;
}else{
hero = new Hero(HERO);
}
}
}
//建立敵人飛機的資料
var lastTime = new Date().getTime();
var interval = 800;
/**
*根據指定時間差建立不同型別的敵人飛機
*將建立好的飛機儲存進 enemies 陣列中
*/
function componentEnter(){
var currentTime = new Date().getTime();
if(currentTime - lastTime >= interval){
var n = Math.floor(Math.random()*10);
if(n >= 0 && n <= 7){
//建立小型飛機
enemies[enemies.length]=new Enemy(E1);
}else if(n == 8){
//建立中型飛機
enemies[enemies.length]=new Enemy(E2);
} else {
//建立大型飛機
//如果陣列中第一個元素不是大型飛機,則建立一個,並且放在第一個位置處,其他的飛機位置後移
if(enemies[0].type != 3){
enemies.splice(0,0,new Enemy(E3));
}
}
lastTime = new Date().getTime();
}
}
/**
* 繪製各個元件
*/
function paintComponent(){
//繪製子彈
for(var i=0;i<bullets.length ; i ++){
var bullet = bullets[i];
bullet.paint(ctx);
}
//繪製所有敵人小飛機
for(var i=0;i<enemies.length ; i ++){
enemies[i].paint(ctx);
}
//將繪製hero的方法移動至此
hero.paint(ctx);
ctx.font = "20px 微軟雅黑";
ctx.fillText("SCORE:"+score,10,20);
ctx.fillText("LIFE:"+life,400,20)
}
/**
* 讓所有的元件動起來(更新y座標)
*/
function stepComponent(){
for(var i=0;i<bullets.length ; i ++){
bullets[i].move();
}
//移動所有敵人小飛機
for(var i=0;i<enemies.length ; i ++){
enemies[i].step();
}
}
//以下程式碼放入計時器中
case RUNNING:
//遊戲進行
sky.step();
sky.paint(ctx);
hero.step();
hero.paint(ctx);
hero.shoot();
checkHit();
//新增新元件(敵人小飛機)
componentEnter();
stepComponent();
deleteComponent();
//繪製所有的元件
paintComponent();
break;
</script>
我方飛機(hero)由2個正常狀態與4個爆炸狀態的圖片組成,子彈(b)由單獨一張圖片表示,小飛機(e1)由一張正常狀態和4張爆炸狀態組成,中型飛機(e2)由一張正常狀態和4張爆炸狀態圖片組成,大型飛機(e3)由兩張正常狀態與6張爆炸狀態圖片組成。
***************************hero的位置問題***************************
在獲取滑鼠位置時,如果直接將滑鼠位置新增給圖片,會發現圖片在滑鼠的右下方,並不是圖片的正中間是滑鼠,所有,我們還有在獲取到滑鼠位置後,在橫縱軸的方向上各自減去hero的圖片大小的一半,將圖片向左上移動。
******************敵機與子彈和英雄的碰撞問題**********************
我們可以將碰撞問題簡化,簡化為兩個矩形,如果對方的頂點進入了對方矩形範圍的區域,則為發生了碰撞。這時,我們可以以敵機(e)為參考物件,計算出敵機與對方的碰撞範圍(橫縱座標的範圍值)與對方的中心座標進行比較,如果我方飛機或者子彈的中心座標進入了敵機的碰撞範圍,則觸發爆炸效果。大致如下圖所示:
四,暫停
遊戲中的狀態做完,接下來就簡單多了。暫停狀態,只要滑鼠離開了canvas的範圍,就遊戲暫停,敵機,子彈,hero都停止,天空繼續動,並且在中間位置出現暫停的圖示,滑鼠移回canvas範圍,則又繼續遊戲。效果如下所示:
對應程式碼如下:
<script>
//建立影象物件用來表示暫停
var pause = new Image();
pause.src="img/game_pause_nor.png";
/**
*滑鼠移動事件
*處理 hero與滑鼠的位置
*/
canvas.onmouseout = function(e){
if(state == RUNNING){
state = PAUSE;
}
}
canvas.onmouseover = function(e){
if(state == PAUSE){
state = RUNNING;
}
}
//以下程式碼放入計時器中
case PAUSE:
//暫停
sky.step();
sky.paint(ctx);
paintComponent();
ctx.drawImage(pause,(WIDTH-pause.width)/2,(HEIGHT-pause.height)/2);
break;
</script>
五,遊戲結束
遊戲結束只需要所有元素都停止,並顯示GAME_OVER即可,所以直接改計時器的內容。如下所示:
<script>
case GAME_OVER:
//遊戲結束
ctx.font = "bold 24px 微軟雅黑";
var width =ctx.measureText("GAME_OVER").width;
ctx.fillText("GAME_OVER",(WIDTH-width)/2,300);
break;
</script>
只需要將各個狀態的程式碼加入對應的位置,即可實現執行。各個階段的程式碼也可單獨執行,所有的程式碼資源和圖片資源已打包上傳在該賬號的資源上了。
注:本文的程式碼與圖片皆為轉發自已有的專案內容,本文內容我個人為專案的經驗歸納總結。