一、Canvas基本使用
Canvas是HTML5的畫布,Canvas算是“不務正業”的物件導向大總結,將物件導向玩極致。
演算法為王!就是說canvas你不會,但是演算法好,不怕寫業務,不怕程式碼量,只要稍微學學API就能出活。
Canvas這裡是HTML5新標籤,直接要了flash的命。
1.1 Canvas簡介
MDN的Canvas線上手冊:
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
瞭解:
<canvas>是一個可以使用指令碼(通常為JavaScript)來繪製圖形的 HTML 元素.它可以用於繪製圖表、製作圖片構圖或者製作簡單的(以及不那麼簡單的)動畫. 右邊的圖片展示了一些 <canvas> 的實現示例。
歷史:
<canvas> 最早由Apple引入WebKit,用於Mac OS X 的 Dashboard,隨後被各個瀏覽器實現。如今,所有主流的瀏覽器都支援它。
Mozilla 程式從 Gecko 1.8 (Firefox 1.5) 開始支援 <canvas>。它首先是由 Apple 引入的,用於 OS X Dashboard 和 Safari。Internet Explorer 從IE9開始支援<canvas> ,更舊版本的IE可以引入 Google 的 Explorer Canvas 專案中的指令碼來獲得<canvas>支援。Chrome和Opera 9+ 也支援 <canvas>。
Canvas相容到IE9。
1.2 Canvas入門
canvas是HTML5中比較特殊的雙標籤,可以在body中放:
<html> <head> <meta charset="UTF-8" /> <title>Document</title> <style type="text/css"> canvas{border:1px solid #000;} </style> </head> <body> <canvas width="600" height="400"></canvas> </body> </html>
不能將width、height在CSS中設定,否則畫布的畫素的會被縮放,畫面質量粗糙了。
<canvas>元素可以像任何一個普通的影象一樣(有margin,border,background等等屬性)被設計。然而,這些樣式不會影響在canvas中的實際影象。
畫布沒什麼用,所有操作都要在“上下文”中進行,這裡的上下文是環境的意思,不是物件導向中的this。
<script type="text/javascript"> //得到畫布標籤 var canvas = document.querySelector("canvas"); //使用上下文,得到一個2D的畫布 var ctx = canvas.getContext("2d"); //畫畫 ctx.fillRect(100, 100, 300, 100); </script>
Canvas的本質就是用js來畫畫,所有的繪畫函式,都是ctx的方法。
canvas馬上開始面對一堆API:
<script type="text/javascript"> //得到畫布標籤 var canvas = document.querySelector("canvas"); //使用上下文,得到一個2D的畫布 var ctx = canvas.getContext("2d"); //繪製矩形 ctx.fillStyle = "orange"; //先提供一個顏色的筆 ctx.fillRect(100, 100, 300, 100); //在根據以上顏色填充 ctx.fillStyle = "green"; //先提供一個顏色的筆 ctx.fillRect(100, 200, 300, 200); //在根據以上顏色填充 </script>
Canvas的座標系和絕對定位的座標系是一樣的。
二、Canvas繪製形狀
2.1繪製形狀路徑
在Canvas中有兩種東西:
l stroke路徑【筆觸】,也叫描邊,就是形狀的輪廓
l fill填充,就是裡面的顏色
//得到畫布標籤 var canvas = document.querySelector('canvas'); //使用上下文,得到一個2D畫布 var ctx = canvas.getContext("2d"); //畫畫 ctx.beginPath(); //宣告要開始繪製路徑 ctx.moveTo(100,100); //移動到繪製點,將“畫筆”移動到100,100的位置 ctx.lineTo(250,250); //劃線 ctx.lineTo(500,250); //劃線 ctx.lineWidth = 10; //線的粗細 ctx.strokeStyle = "red"; //線的顏色 ctx.fillStyle = "blue"; //準備填充的顏色 ctx.closePath(); //閉合路徑(自動補全) ctx.stroke(); //顯示線(繪製線),可以繪製的路徑顯示出來 ctx.fill(); //填充顏色
只有矩形有快捷方法,比如想繪製多邊形,都要用以上這些組合。
2.2繪製矩形
ctx.fillRect(x,y,w,h); //繪製填充矩形 ctx.strokeRect(x,y,w,h); //繪製路徑矩形
繪製調色盤:
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); for (var i = 0;i < 6;i++){ for (var j = 0;j < 6;j++){ ctx.fillStyle = 'rgba('+ Math.floor(255-42.5 * i) +','+ Math.floor(255-42.5 * j) +', 200)'; ctx.fillRect(i * 25, j * 25, 25, 25); } }
記住一句話:Canvas是不可逆,繪製的元素一旦上了螢幕,是無法針對它再次操作。
2.3繪製弧度
ctx.arc(圓心x, 圓心y, 半徑, 開始的弧度, 結束的弧度, 是否逆時針); ctx.beginPath(); //開始繪製路徑 // ctx.arc(100, 100, 60, 0, 6.28, false); ctx.arc(100, 100, 60, 0, Math.PI * 2, false); ctx.stroke(); //顯示路徑線
切一個圓,讓切下來的弧邊長等於圓的半徑,此時弧對應的角度是57.3度左右,此時角度是固定的。
正方向是正右方
在canvas中所有涉及角度的座標系有兩點注意的:
l 0弧度的方向是正右方向。
弧度的順時針和逆時針:
ctx.arc(100,100,60, 0, 3, false); //繪製圓弧
ctx.arc(100,100,60, 0, 1, true); //繪製圓弧
繪製圓形:
ctx.arc(100,100,60, 0, Math.PI * 2, false); ctx.arc(100,100,60, 0, 6.28, false); ctx.arc(100, 100, 60, 0, -6.28, true);
注意:x和y座標是到圓心的位置,而且圓的大小是半徑,後面繪製的形狀會覆蓋前面的形狀。
繪製笑臉
<script type="text/javascript"> var canvas = document.querySelector("canvas"); var ctx = canvas.getContext("2d"); //繪製大臉 ctx.beginPath(); //開始繪製路徑 ctx.arc(300,200, 160, 0, 6.28, false); //繪製圓弧 ctx.stroke(); //顯示路徑線 ctx.fillStyle = "brown"; ctx.fill(); //繪製左眼睛 ctx.beginPath(); ctx.arc(230,150, 30, 0, 6.28, false); ctx.stroke(); ctx.fillStyle = "orange"; ctx.fill(); //繪製右眼睛 ctx.beginPath(); ctx.arc(370,150, 30, 0, 6.28, false); ctx.stroke(); ctx.fillStyle = "blue"; ctx.fill(); //繪製嘴巴 ctx.beginPath(); ctx.arc(300,160, 120, 0.5, 2.6, false); ctx.lineWidth = 10; ctx.stroke(); </script>
三、使用圖片
3.1圖片基本使用
canvas中不可能所有形狀都自己畫,一定是設計師給我們素材,然後使用。
canvas中使用圖片的方法:注意,必須等img完全載入後才能呈遞圖片。
ctx.drawImage(); var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); //建立一個img標籤 var image = new Image() //設定圖片的路徑 image.src = "images/baby1.jpg"; //當圖片成功載入,就畫圖(上螢幕) image.onload = function(){ //顯示圖片的API ctx.drawImage(image, 100, 100); //表示x和y座標 }
3.2使用切片
如果2個數字引數,此時表示左上角位置的x和y座標:
ctx.drawImage(img,100,100);
ctx.drawImage(img圖片物件,畫布X,畫布Y);
如果4個數字引數,此時表示x、y、w、h:
ctx.drawImage(img圖片物件, 畫布X,畫布Y,圖片W,圖片H);
如果8個數字引數,此時表示:
ctx.drawImage(img,切片X,切片Y,切片W,切片H,畫布X,畫布Y,圖片W,圖片H);
//建立一個img標籤 var image = new Image() //設定圖片的路徑 image.src = "images/baby1.jpg"; //當圖片成功載入,就畫圖(上螢幕) image.onload = function(){ //顯示圖片的API // ctx.drawImage(image, 100, 100); //表示x和y座標 // ctx.drawImage(image, 100, 100, 150, 150); //表示x和y座標 // ctx.drawImage(img,切片X,切片Y,切片W,切片H,畫布X,畫布Y,圖片W,圖片H); ctx.drawImage(image, 108, 200, 145, 120, 100, 100, 145, 120); }
圖片API:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images
3.3簡易的圖片載入器
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); var R = { "0":"images/d1.jpg", "1":"images/d2.jpg", "2":"images/d3.jpg" } var arr = []; for(var k in R){ arr[k] = new Image(); //建立img物件 arr[k].src = R[k]; //設定圖片地址 // 當圖片成功載入,就畫圖(上螢幕) arr[k].onload = function(){ ctx.drawImage(arr[k], 50, 50) } }
3.4顯示GIF動態圖
HTML程式碼:
<img id="testImg" src="xxx.gif" width="224" height="126"> <p><input type="button" id="testBtn" value="停止"></p>
if ('getContext' in document.createElement('canvas')) { HTMLImageElement.prototype.play = function() { if (this.storeCanvas) { // 移除儲存的canvas this.storeCanvas.parentElement.removeChild(this.storeCanvas); this.storeCanvas = null; // 透明度還原 image.style.opacity = ''; } if (this.storeUrl) { this.src = this.storeUrl; } }; HTMLImageElement.prototype.stop = function() { var canvas = document.createElement('canvas'); // 尺寸 var width = this.width, height = this.height; if (width && height) { // 儲存之前的地址 if (!this.storeUrl) { this.storeUrl = this.src; } // canvas大小 canvas.width = width; canvas.height = height; // 繪製圖片幀(第一幀) canvas.getContext('2d').drawImage(this, 0, 0, width, height); // 重置當前圖片 try { this.src = canvas.toDataURL("image/gif"); } catch(e) { // 跨域 this.removeAttribute('src'); // 載入canvas元素 canvas.style.position = 'absolute'; // 前面插入圖片 this.parentElement.insertBefore(canvas, this); // 隱藏原圖 this.style.opacity = '0'; // 儲存canvas this.storeCanvas = canvas; } } }; } var image = document.getElementById("testImg"), button = document.getElementById("testBtn"); if (image && button) { button.onclick = function() { if (this.value == '停止') { image.stop(); this.value = '播放'; } else { image.play(); this.value = '停止'; } }; }
3.5遊戲圖片資源載入器
//得到畫布 var canvas = document.querySelector("canvas"); // 使用上下文,得到一個2D的畫布 var ctx = canvas.getContext("2d"); //資原始檔 var R = { "d1" : "images/d1.jpg", "d2" : "images/d2.jpg", "d3" : "images/d3.jpg" } //遍歷這個物件,將他們的地址變為真實圖片地址 var count = 0; //已成功載入的圖片個數 var length = Object.keys(R).length; //所有圖片的總數 for(var k in R){ //建立image物件 var image = new Image(); //設定src圖片路徑 image.src = R[k]; //將R裡面的資原始檔,變為真正的圖片物件 R[k] = image; //當image載入成功後,顯示圖片在畫布上 image.onload = function(){ count++; //當某張圖片載入成功,給計數器+1 ctx.clearRect(0,0,600,600) //繪製文字,提升使用者體驗,提示載入的進度 //填充文字API ctx.textAlign = "center"; ctx.font = "30px 微軟雅黑"; ctx.fillText("正在載入圖片:" + count + "/" + length, canvas.width / 2,50) //當載入完畢,開始遊戲 if(count == length){ //開始遊戲的回撥函式 ctx.clearRect(0,0,600,600) start(); } } } // 開始遊戲的函式 function start(){ ctx.drawImage(R["d1"],100,100); ctx.drawImage(R["d2"],0,100); ctx.drawImage(R["d3"],300,200); }
四、畫布的變形
4.1 translate移動變形
translate()移動畫布,rotate()旋轉畫布。
canvas中不能只移動某一個物件,移動的都是整個畫布。
canvas中不能只旋轉某一個物件,旋轉的都是整個畫布。
但是可以用save()、restore()來巧妙設定,實現讓某一個元素進行移動和旋轉。
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); ctx.translate(100, 100); //將畫布移動,座標系就發生變化了 ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫
移動變形、移動的是整個畫布、而不是某個元素,在ctx.translate()之後繪製的語句都將被影響。
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); ctx.translate(100, 100); //將畫布移動,座標系就發生變化了 ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫 ctx.beginPath(); ctx.arc(100,100, 100, 0, 6.28, false); ctx.fillStyle = 'skyblue'; ctx.fill();
4.2 save()儲存和restore()恢復
ctx.save()表示儲存上下文的物理性質,ctx.restore()表示恢復最近一次的儲存。
save表示儲存sava函式之前的狀態,restore表示獲取save儲存的狀態。
移動了的元素,會影響不需要移動圓點座標的元素,所以可以使用以上兩個方法儲存起來,可以解決讓某一個元素移動變形不受影響。
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext("2d"); ctx.save(); ctx.translate(100, 100); //將畫布移動,座標系就發生變化了 ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫 ctx.restore(); ctx.beginPath(); ctx.arc(100,100, 100, 0, 6.28, false); ctx.fillStyle = 'skyblue'; ctx.fill();
4.3 rotate()旋轉變形
旋轉的是整個座標系,座標系以0,0點為中心點進行旋轉。
rotate(1)的引數,是弧度,旋轉的也不是矩形,而是畫布。
var canvas = document.querySelector("canvas"); var ctx = canvas.getContext("2d"); ctx.rotate(1); //1表示57.3度(1弧度) ctx.fillRect(100, 100, 100, 100); //相對於旋轉後的座標系開始畫畫
如果想旋轉某一個元素,必須將座標軸原點,放到要旋轉的元素身上,然後再旋轉。
ctx.save(); ctx.translate(150,150) ctx.rotate(1); //1表示57.3度(1弧度) ctx.fillRect(-50, -50, 100, 100); //相對於旋轉後的座標系開始畫畫 ctx.restore();
座標系移動到物體的中心點,物體以負半寬、半高、為x,y繪製。
function Box(){ this.x = 150; this.y = 150; this.w = 100; this.h = 100; this.deg = 0; } Box.prototype.render = function(){ ctx.save() ctx.translate(this.x,this.y) ctx.rotate(this.deg); ctx.fillRect(-this.w / 2,-this.h / 2,this.w,this.h); ctx.restore() } Box.prototype.update = function(){ this.deg += 0.2; } var b = new Box(); b.render(); setInterval(function(){ ctx.clearRect(0,0,600,400) b.update(); b.render(); },20);
globalCompositeOperation
用來設定新影象和老圖形如何“融合”、“裁剪”。
值有以下這些:
新圖形是:source,老圖形是destination
ctx.globalCompositeOperation="destination-over";
五、FlappyBird遊戲
5.1遊戲結構
遊戲採用中介者模式開發,Game類統領全域性,負責讀取資源、設定定時器、維護各種演員的例項,也就是說所有的演員都是Game類new出來,當做一個子屬性。
也就是,遊戲專案外部就一條語句:
var game = new Game();
其他的所有語句都寫在Game類裡面。
需要的類:
Game類: 中介者,讀取資源、設定定時器、維護各種演員的例項
Bird類: 小鳥類,這個類是單例的,例項化一次
Pipe類: 管子類
Land類: 大地類
background類: 背景類
<body> <canvas width="414" height="650"></canvas> </body> <script type="text/javascript" src="js/lib/underscore-min.js"></script> <script type="text/javascript" src="js/Game.js"></script> <script type="text/javascript" src="js/Bird.js"></script> <script type="text/javascript" src="js/Land.js"></script> <script type="text/javascript" src="js/Pipe.js"></script> <script type="text/javascript" src="js/Background.js"></script>
5.2建立Game類:開始介面、載入資源
(function(){ window.Game = function() { this.f = 0; //幀編號 this.init();//初始化DOM } Game.prototype.init = function() { this.canvas = document.getElementById("canvas"); this.ctx = this.canvas.getContext("2d"); //R物件表示資原始檔,圖片總數 this.R = { "bg_day": "images/bg_day.png", "land": "images/land.png", "pipe_down": "images/pipe_down.png", "pipe_up": "images/pipe_up.png", "bird0_0": "images/bird0_0.png", "bird0_1": "images/bird0_1.png", "bird0_2": "images/bird0_2.png", } var self = this; //遍歷物件用for in語句 //遍歷這個物件,將它們變為真的圖片地址 var count = 0; //計算載入好的圖片總數(成功載入一張就+1) var length = Object.keys(this.R).length; //得到圖片的總數 for (var k in this.R) { //建立一個img標籤,發出圖片的請求,目前img物件是孤兒節點 var img = new Image(); //將這個R[k]物件賦值給src設定圖片的路徑 img.src = this.R[k]; //將R裡面的資原始檔,改為img真的圖片物件 this.R[k] = img; //當圖片載入完畢,就畫圖上畫布(圖片必須load才能上畫布) img.onload = function () { count++; //當某張圖片載入完畢,給計數器+1 //清屏 self.clear() //繪製文字,提升使用者載入到什麼程度了 //save和restore方法配合使用,防止汙染其他樣式 self.ctx.save(); //儲存狀態 self.ctx.textAlign = "center"; self.ctx.font = "18px 微軟雅黑"; self.ctx.fillStyle = "blue"; //填充文字 self.ctx.fillText(`載入中 ${count} / ${length}`, self.canvas.width / 2, 100); self.ctx.restore(); //恢復儲存的狀態 //當載入完畢的圖片總數==圖片總數時,此時就開始載入圖片並開始遊戲 if (count == length) { self.start(); //開始遊戲的回撥函式 } } } } //清屏 Game.prototype.clear = function() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) } // 遊戲主迴圈 Game.prototype.start = function () { var self = this; this.timer = setInterval(function(){ self.f++; // 清屏 self.clear(); //顯示幀率 self.ctx.font = "16px 微軟雅黑"; self.ctx.fillText(self.f,10,20); },20); } })();
5.3建立background.js背景類
(function () { window.Background = function () { this.image = game.R["bg_day"]; //圖片 this.x = 0; } Background.prototype.render = function () { // 畫一個矩形,補充一下天空的顏色 game.ctx.save() game.ctx.fillStyle = "#4ec0ca"; game.ctx.fillRect(0,0,game.canvas.width,game.canvas.height - 512); //第一步:為了不穿幫繪製背景連續放3張圖片讓背景無縫滾動 game.ctx.drawImage(this.image,this.x,game.canvas.height - 512); game.ctx.drawImage(this.image,this.x + 288 ,game.canvas.height - 512); game.ctx.drawImage(this.image,this.x + 288 * 2 ,game.canvas.height - 512); game.ctx.restore(); } Background.prototype.update = function () { this.x--; if(this.x < -288){ this.x = 0; } } })();
Game.prototype.start = function() { // 遊戲開始主 迴圈 var self = this; this.background = new Background();// new 背景類 this.land = new Land(); //new例項化大地類 this.timer = setInterval(function(){ self.f++; self.clear(); // 渲染 和 更新 背景類 self.background.render(); self.background.update(); // 每隔100幀,例項化一根管子類 self.f % 100 == 0 && new Pipe(); // 渲染 和 更新 大地類 self.land.render(); self.land.update(); // 渲染 和 更新所有管子類 for (var i = 0; i < self.pipeArr.length; i++) { self.pipeArr[i].render() self.pipeArr[i].update() } self.ctx.font = "16px 微軟雅黑"; self.ctx.fillText(self.f,10,20); }, 20) }
5.4建立Land.js大地類
(function () { window.Land = function () { this.image = game.R["land"]; //圖片 this.x = 0; } Land.prototype.render = function () { game.ctx.drawImage(this.image, this.x, game.canvas.height - 112); game.ctx.drawImage(this.image, this.x + 336, game.canvas.height - 112); game.ctx.drawImage(this.image, this.x + 336 * 2, game.canvas.height - 112); } Land.prototype.update = function () { this.x--; if (this.x < -336) { this.x = 0; } } })();
5.5建立Pipe.js管子類:
(function () { window.Pipe = function () { this.pipeDown = game.R["pipe_down"]; //上管子 this.pipeUp = game.R["pipe_up"]; //下管子 this.pipeDownH = _.random(50,300); //隨機一個上面管子的高度(因) this.space = 120; //上下管子之間的空隙(因) //下面管子的高度隨之而定了(果),高度-大地高-上管子高-空隙 this.pipeUpH = game.canvas.height - 112 - this.pipeDownH - this.space; this.x = game.canvas.width; //讓管子在螢幕右側外面就緒 game.pipeArr.push(this); //將自己存進陣列 } Pipe.prototype.render = function () { //兩根管子在畫布的位置(image物件, 切片X, 切片Y, 切片W,切片H,畫布X,畫布Y,圖片W,圖片H) //渲染上面的管子 game.ctx.drawImage(this.pipeDown, 0, 400 - this.pipeDownH, 52, this.pipeDownH, this.x, 0, 52, this.pipeDownH); //下面的管子 game.ctx.drawImage(this.pipeUp, 0, 0, 52, this.pipeUpH, this.x, this.pipeDownH + this.space, 52, this.pipeUpH); } Pipe.prototype.update = function () { this.x -= 2;//更新管子(讓管子移動) if(this.x < -52){ this.goDie(); //超過螢幕左側-52的位置(刪除管子) } } Pipe.prototype.goDie = function () { for(var i = game.pipeArr.length - 1; i >= 0; i--){ if (game.pipeArr[i] == this){ game.pipeArr.splice(i,1); } } } })();
5.6建立Bird.js小鳥類
(function () { window.Bird = function () { this.img = [game.R["bird0_0"], game.R["bird0_1"], game.R["bird0_2"]]; //小鳥 this.x = 100; this.y = 100; //位置,這裡的x,y不是小鳥的左上角位置,而是小鳥的中心點 // this.x = game.width / 2 * 0.618; this.dy = 0.2; //下降的增量,每幀的恆定變 this.deg = 0; //旋轉 this.wing = 0; //拍打翅膀的編號 } //渲染小鳥 Bird.prototype.render = function() { game.ctx.save(); game.ctx.translate(this.x,this.y); game.ctx.rotate(this.deg); //減去24是因為x、y是中心點位置(減去寬度和高度的一半) game.ctx.drawImage(this.img[this.wing],-24,-24); game.ctx.restore() } //更新小鳥 Bird.prototype.update = function () { // 下降的增量0.88,變化的量也在變,這就是自由落體 this.dy += 0.88; //旋轉角度的增量 this.deg += 0.08; this.y += this.dy; //每2幀拍打一次翅膀 game.f % 2 && this.wing++; if(this.wing > 2){ this.wing = 0; } } //小鳥飛 Bird.prototype.fly = function() { //小鳥只要有一個負的dy此時就會向上飛,因為this.y += 一個數 this.dy = -10; this.deg = -1; } })();
5.7碰撞檢測
小鳥的碰撞檢測,使用AABB盒方法,就是把小鳥看作是一個矩形,去判斷有沒有碰撞。
碰撞公式:
鳥的X2 >管子的X1 && (鳥的Y1 < 管子的Y1 || 鳥的Y2 > 管子的Y2) && 鳥的X1 < 管子的X2
AABB盒,軸對齊包圍盒也稱為矩形盒,要自己會除錯,將關鍵的資料fillText()渲染到介面上。
碰撞檢測寫在管子身上,因為管子很多,只需要檢測有沒有碰撞到那唯一的小鳥即可,沒有for迴圈。
如果寫在鳥身上,要用for迴圈遍歷所有管子,一一檢測。
六、場景管理器
遊戲是有各種場景的:
① 歡迎介面
② 教學介面
③ 遊戲介面
④ GameOver介面
l 場景管理器(SceneManager)的好處就是可以管理零碎的東西。
l Game類現在不再直接管理Bird、Background、Pipe、Land了。而是隻管理場景管理器。
l 場景管理器負責管理其他類的例項化、更新渲染。
刪除Game類所有的例項化、更新、渲染,然後建立場景管理器,並例項化場景管理器
繼續引入圖片資源:
this.R = { ... "title" : "images/title.png", "button_play" : "images/button_play.png", "text_ready" : "images/text_ready.png", "tutorial" : "images/tutorial.png", "gameoverbg": "images/gameoverbg.png", "b0" : "images/b0.png", "b1" : "images/b1.png", "b2" : "images/b2.png", "b3" : "images/b3.png", "b4" : "images/b4.png", "b5" : "images/b5.png", "b6" : "images/b6.png", "b7" : "images/b7.png", "b8" : "images/b8.png", "b9" : "images/b9.png", "b10" : "images/b10.png", "b11" : "images/b11.png" }
3號場景:
window.SceneManager = function () { //當前場景的編號 this.smNumber = 1; // 初始化場景編號的方法 this.init(1); this.bindEvent(); } (function () { //場景初始化方法 SceneManager.prototype.init = function(number) { switch(number){ case 1: // 1號場景 break; case 2: // 2號場景 break; case 3: // 3號場景:遊戲的主場景 this.background = new Background(); //例項化背景類 this.land = new Land(); //例項化大地類 this.bird = new Bird(); //例項化小鳥類 break; } } //場景渲染方法 SceneManager.prototype.render = function () { //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了 switch (this.smNumber) { case 1: break; case 2: break; case 3: // 渲染 和 更新背景 this.background.render(); this.background.update(); // 每間隔100幀,例項化一根管子 game.f % 100 == 0 && new Pipe(); //例項化管子類 // 迴圈遍歷管子陣列,更新和渲染管子類 for(var i = 0; i < game.pipeArr.length;i++){ game.pipeArr[i].render(); game.pipeArr[i].update(); } // 渲染 和 更新大地類 this.land.render(); this.land.update(); // 渲染 和 更新小鳥類 this.bird.render(); this.bird.update(); break; } } })();
修改碰撞檢測: if(game.sm.bird.x2 > this.x1 && ( game.sm.bird.y1 < this.y1 || game.sm.bird.y2 > this.y2 ) && game.sm.bird.x1 < this.x2) { }
3號場景事件:
SceneManager.prototype.bindEvent = function(){ var self = this; game.canvas.onmousedown = function(e){ //新增事件監聽,要根據當前場景是幾號,觸發對應的場景事件 switch(self.smNumber){ case 1: break; case 2: break; case 3: self.bird.fly(); break; case 4: break; } } }
以上是全是3號場景業務,都已經完成。
1號場景:
//場景初始化方法 //不管什麼時候來到這個場景,此時都有一個預設就位狀態 //我們動畫是可以重複的,但是這個函式不是每幀執行。 SceneManager.prototype.init = function(number) { //init中只有一個初始化引數,不要涉及到運動 switch(number){ case 1: // 1號場景:遊戲封面和開始按鈕場景的初始化 this.background = new Background(); //例項化背景類 this.land = new Land(); //例項化大地類 this.titleY = -48; //初始化title位置 this.titleYTarget = 120; //title停留的位置 this.buttonY = game.canvas.height; //初始化按鈕的位置 this.buttonYTarget = 360; //按鈕停留的位置 this.birdY = 180; //初始化小鳥的位置 this.birdD = "down"; //小鳥的運動方向 break; case 2: // 2號場景 break; case 3: // 3號場景:遊戲的主場景 this.background = new Background(); //例項化背景類 this.land = new Land(); //例項化大地類 this.bird = new Bird(); //例項化小鳥類 break; } } //場景渲染方法 SceneManager.prototype.render = function () { //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了 switch (this.smNumber) { case 1: // 1號場景:遊戲封面和開始按鈕場景的更新和渲染 // 渲染 和 更新背景 this.background.render(); this.background.update(); // 渲染 和 更新大地類 this.land.render(); this.land.update(); //渲染title圖片 game.ctx.drawImage(game.R["title"],(game.canvas.width-178)/2,this.titleY); game.ctx.drawImage(game.R["button_play"],(game.canvas.width-116)/2,this.buttonY); game.ctx.drawImage(game.R["bird1_2"], (game.canvas.width - 48) / 2, this.birdY); //title下降運動到目標位置 this.titleY += 2; if(this.titleY > this.titleYTarget){ this.titleY = this.titleYTarget; } //按鈕上升運動到目標位置 this.buttonY -= 5; if (this.buttonY < this.buttonYTarget) { this.buttonY = this.buttonYTarget; } //小鳥不停的上下運動 if(this.birdD == "down") { this.birdY += 2; if (this.birdY > 260){ this.birdD = "up"; } } else if (this.birdD == "up"){ this.birdY -= 2; if (this.birdY < 170) { this.birdD = "down"; } } break; case 2: break; case 3: ... break; } } //事件監聽方法 SceneManager.prototype.bindEvent = function () { //根據當前場景觸發事件 var self = this; game.canvas.onmousedown = function(e) { //滑鼠點選的座標位置 var x = e.offsetX; var y = e.offsetY; switch (self.smNumber) { case 1: //1號場景:遊戲封面和開始按鈕場景的初始化 //得到按鈕的上下左右包圍盒 var left = (game.canvas.width - 116) / 2 var right = (game.canvas.width - 116) / 2 + 116; var up = self.buttonYTarget; var down = self.buttonYTarget + 60; if(x >= left && x <= right && y <= down && y >= up){ //點選進去2號場景 self.smNumber = 2; self.init(2); } break; case 2: break; case 3: break; } } }
以上,1號場景完成。
2號場景:
2號場景init初始化:
SceneManganer.prototype.init = function(number) { switch (number) { case 1: ... break; case 2: // 教學 場景 this.background = new Background(); this.land = new Land(); this.readyY = -62; //2號場景的ready圖片 // 修改 tutorial 的透明度 this.tutorial = 1 this.tutorialD = "A" break; case 3: ... break; case 4: ... break; } }; 2號場景render渲染方法: //場景渲染方法 SceneManager.prototype.render = function () { //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了 switch (this.smNumber) { case 1: ... break; case 2: // 2號場景:教學場景 this.background.render(); this.background.update(); // 渲染 和 更新大地類 this.land.render(); this.land.update(); //渲染title圖片 game.ctx.drawImage(game.R["text_ready"],(game.canvas.width-196)/2,this.readyY) game.ctx.drawImage(game.R["bird0_1"],100,180); this.readyY += 2; if (this.readyY > this.readyYTarget) { this.readyY = this.readyYTarget; } game.ctx.save(); //讓一個物體閃爍 if(this.tutorialD == "A"){ this.tutorial -= 0.04; if(this.tutorial < 0.1){ this.tutorialD = "B" } }else if(this.tutorialD == "B"){ this.tutorial += 0.04; if(this.tutorial > 1){ this.tutorialD = "A" } } // ctx.globalAlpha改變透明度的API game.ctx.globalAlpha = this.tutorialOpacity; game.ctx.drawImage(game.R["tutorial"], (game.canvas.width - 114) / 2, 250); game.ctx.restore(); break; case 3: ... break; case 4: ... break; } } 2號場景bindEvent監聽事件方法: //事件監聽方法 SceneManager.prototype.bindEvent = function () { //根據當前場景觸發事件 var self = this; game.canvas.onmousedown = function(e) { //滑鼠點選的座標位置 var x = e.offsetX; var y = e.offsetY; switch (self.smNumber) { case 1: break; case 2: //2號場景 var left = (game.canvas.width - 114) / 2 var right = (game.canvas.width - 114) / 2 + 114; var up = 250; var down = 350; if (x >= left && x <= right && y <= down && y >= up) { //點選進去2號場景 self.smNumber = 3; self.init(3); } break; case 3: //3號場景:遊戲的主場景 self.bird.fly(); break; } } }
4號場景:
4號場景init方法
SceneManager.prototype.init = function(number) { //init中只有一個初始化引數,不要涉及到運動 switch(number){ case 1: break; case 2: break; case 3: break; case 4: //4號場景:死亡場景 //紅色邊框圖片的透明度 this.GameOverBg = 1; //小鳥落地死亡的爆炸動畫初始化圖片編號 this.boom = 0; break; } }
碰撞檢測死亡,進入4號場景: //碰撞檢測 if (game.sm.bird.x1 < this.x2 && game.sm.bird.x2 > this.x1 && (game.sm.bird.y1 < this.y1 || game.sm.bird.y2 > this.y2)){ //死亡之後,進入4號場景(小鳥下墜) document.getElementById("die").play(); game.sm.smNumber = 4; game.sm.init(4); return; }else if(!this.isScore && game.sm.bird.x1 > this.x2){ // 這裡是記分,條件就是把是否加分的true或false給管子身上 this.isScore = true; game.sm.score++; document.getElementById("score").play(); }
4號場景render渲染方法: //場景渲染方法 SceneManager.prototype.render = function () { switch (this.smNumber) { case 1: ... break; case 2: break; case 3: ... break; case 4: // 讓所有的物體靜止,只渲染,不更新(update不用呼叫了) this.background.render(); this.land.render(); for (var i = 0; i < game.pipeArr.length; i++) { game.pipeArr[i].render(); } this.bird.render(); // 讓鳥急速下降 this.bird.y += 10; //播放聲音 document.getElementById("down").play(); // 保證鳥頭朝下掉 this.bird.deg += 0.5; if (this.bird.deg > 1.57){ this.bird.deg = 1.57; } //撞擊地面產生爆炸動畫,並且小鳥飛昇 if(this.bird.y > game.canvas.height - 112 - 17){ //小鳥撞地停留在原位 this.bird.y = game.canvas.height - 112 - 17; game.f % 2 == 0 && this.boom++; if (this.boom >= 11) { //清空管子陣列和分數,為下一回合準備 game.pipeArr = []; this.score = 0; //回到1號場景 this.smNumber = 1; this.init(1); this.boom = 5; // clearInterval(game.timer); //停止遊戲主迴圈 } //渲染爆炸動畫 game.ctx.drawImage(game.R["b" + this.boom], this.bird.x - 50, this.bird.y - 100); } //渲染紅色邊框 this.GameOverBg -= 0.03; if(this.GameOverBg < 0){ this.GameOverBg = 0; } game.ctx.save() game.ctx.globalAlpha = this.GameOverBg; game.ctx.drawImage(game.R["gameoverbg"],0,0,game.canvas.width,game.canvas.height) game.ctx.restore(); break; } }
Bird.prototype.update = function(){ this.y += this.dy; //下降的速度 this.deg += 0.08; if(this.deg > 1.57){ this.deg = 1.57; } //掉地死亡 if(this.y > game.canvas.height - 112){ document.getElementById('die').play(); game.sm.smNumber = 4; game.sm.init(4); } }
新增鍵盤事件和聲音、分數:
<audio src="music/die.ogg" id="die"></audio> <audio src="music/down.ogg" id="down"></audio> <audio src="music/fly.ogg" id="fly"></audio> <audio src="music/score.ogg" id="score"></audio> <canvas id="canvas" width="414" height="650" tabindex="1"></canvas>
寫在bindEvent裡:
ame.canvas.onkeydown = function(e){ switch(self.smNumber){ case 3: if(e.keyCode == 32){ self.bird.fly(); } break; } } game.canvas.focus();
七、Canvas動畫
讓元素在canvas上運動,需要使用定時器。
canvas使用了一個特殊的模式,上畫布的元素,立刻被畫素化。也就是說,上畫布的元素,你將得不到這個“物件”的引用。比如,一個圓形畫到了畫布上面,此時就是一堆的畫素點,不是一個整體的物件了,你沒有任何變數能夠得到這個物件,改變這個物件的屬性。也就是說,這種改變的思路在canvas中是行不通的。
要實現動畫,必須:每幀重新畫一個。
所以,canvas的畫圖原理是:
清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →
清屏 → 重繪 →...
注意:千萬不要將得到的Canvas和ctx放到init中,因為new一個球,來一個定時器得到一個Canvas畫布。
7.1小球反彈
var canvas = document.querySelector("canvas"); var ctx = canvas.getContext("2d"); function Ball(x,y,r){ //傳入小球的屬性,x和y表示小球left和top的位置 this.x = x; this.y = y; this.r = r; //當增量的dx、dy都為0的時候再隨機一次 do{ this.dx = parseInt(Math.random() * 18) - 9; this.dy = parseInt(Math.random() * 18) - 9; }while(this.dx == 0 || this.dy == 0); //隨機顏色 var colors = ['#f90','#ff0','#09c','#c06','#F99','#9c3','#6cc','#9cc']; this.color = colors[parseInt(Math.random() * colors.length)] arr.push(this); //將小球的例項放進陣列 } //渲染方法,畫一個小球 Ball.prototype.render = function(){ ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0,Math.PI * 2 ,false); ctx.fillStyle = this.color; ctx.fill(); } Ball.prototype.update = function(){ this.x += this.dx; this.y += this.dy; if(this.x >= canvas.width - this.r || this.x <= this.r){ this.dx = -this.dx } if(this.y >= canvas.height - this.r || this.y <= this.r){ this.dy = -this.dy; } } var arr = []; var f = 0; //開始定時器,每一幀清屏、更新、渲染所有小球 setInterval(function(){ f++; //清屏 ctx.clearRect(0, 0, canvas.width, canvas.height); //渲染 for(var i = 0;i < arr.length;i++){ arr[i].render(); arr[i].update(); } ctx.font = "18px 微軟雅黑"; ctx.fillStyle = "blue"; ctx.fillText(f, 10, 20) },20); var count = 60; while(count--){ new Ball(100,100,20); }
7.2炫彩小球
var canvas = document.querySelector("canvas"); var ctx = canvas.getContext("2d"); canvas.width = document.documentElement.clientWidth; canvas.height = document.documentElement.clientHeight; function Ball(x,y,r){ //傳入小球的屬性,x和y表示小球left和top的位置 this.x = x; this.y = y; this.r = r; //當增量的dx、dy都為0的時候再隨機一次 do{ this.dx = parseInt(Math.random() * 18) - 9; this.dy = parseInt(Math.random() * 18) - 9; }while(this.dx == 0 || this.dy == 0); //隨機顏色 var colors = ['#f90','#ff0','#09c','#c06','#F99','#9c3','#6cc','#9cc']; this.color = colors[parseInt(Math.random() * colors.length)] arr.push(this); //將小球的例項放進陣列 } //渲染方法,畫一個小球 Ball.prototype.render = function(){ ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0,Math.PI * 2 ,false); ctx.fillStyle = this.color; ctx.fill(); } Ball.prototype.update = function(){ this.x += this.dx; this.y += this.dy; this.r--; if(this.r <= 0){ this.goDie(); } if(this.x >= canvas.width - this.r || this.x <= this.r){ this.dx = -this.dx } if(this.y >= canvas.height - this.r || this.y <= this.r){ this.dy = -this.dy; } } Ball.prototype.goDie = function(){ for(var i = arr.length-1; i >= 0; i--){ if(arr[i] == this){ arr.splice(i,1); //從陣列第i項,刪除一項 } } } var arr = []; var f = 0; setInterval(function(){ f++; //清屏 ctx.clearRect(0, 0, canvas.width, canvas.height); //渲染 for(var i = 0;i < arr.length;i++){ arr[i].render(); arr[i].update(); } ctx.font = "18px 微軟雅黑"; ctx.fillStyle = "blue"; ctx.fillText(f, 10, 20) },20); canvas.onmousemove = function(e){ new Ball(e.offsetX,e.offsetY, 30); }