0.前言
在MDN上面有一個彈球的例子,我們的小球會在屏幕上彈跳,當它們碰到彼此時會變色。
1.面向對象編程的實踐
官網講得太長,而且有一些漏洞,我改進一下
let canvas = document.querySelector('canvas'); let ctx = canvas.getContext('2d'); let width = canvas.width = window.innerWidth; let height = canvas.height = window.innerHeight; let balls = []; let ran = (min,max) =>parseInt((max-min)*Math.random())+min;//生成隨機數 let Ball = function(vx,vy,x,y,r,color){//Ball的類 this.vx = vx; this.vy = vy; this.x = x||1;//防止速度爲0 this.y = y||1; this.r = r; this.color = color; } Ball.prototype.draw = function(){//繪製的方法 ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x,this.y,this.r,0,2*Math.PI); ctx.fill(); } Ball.prototype.update = function(){//更新的方法 if((this.x + this.r) >= width) { this.x = width - this.r - 5;//防止半身進入邊緣,無限循環,黏住邊緣 this.vx = -(this.vx);//反彈 } if((this.x - this.r) <= 0) { this.x = this.r + 5; this.vx = -(this.vx); } if((this.y + this.r) >= height) { this.y = height - this.r - 5; this.vy = -(this.vy); } if((this.y - this.r) <= 0) { this.y = this.r + 5; this.vy = -(this.vy); } this.x += this.vx;//小球前進 this.y += this.vy; } Ball.prototype.isCollision = function() {//是否碰撞 for(var j = 0; j < balls.length; j++) { if(!(this === balls[j])) {//保證不自己和自己碰撞,因爲自己也在數組裏面,現在是遍歷數組 var dx = this.x - balls[j].x; var dy = this.y - balls[j].y; var dvx = this.vx - balls[j].vx; var dvy = this.vy - balls[j].vy; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= this.r + balls[j].r) { balls[j].x -= 7*balls[j].vx;//防止相互糾纏 balls[j].y -= 7*balls[j].vy; this.x -= 7*this.vx; this.y -= 7* this.vy; this.vx = -this.vx; this.vy = -this.vy; this.color = "#"+(~~(Math.random()*(1<<24))).toString(16); } } } }; let loop = function(){ ctx.fillStyle = 'rgba(0,0,0,.1)';//等於黑板擦,擦除前面動畫留下的痕跡 ctx.fillRect(0,0,width,height); while(balls.length<40){//生成40個球 let ball = new Ball(ran(-7,7),ran(-7,7),ran(0,width),ran(0,height), ran(10,20),"#"+(~~(Math.random()*(1<<24))).toString(16)); balls.push(ball); } for(let i = 0;i<balls.length;i++){//每一個球調用函數,保證動畫進行 balls[i].draw(); balls[i].update(); balls[i].isCollision(); } requestAnimationFrame(loop); } loop(); 複製代碼
2.相互糾纏的現象
在面對碰撞檢測後還有後續動作的情況,必須考慮一下相互糾纏的問題: 如果兩個小球被檢測到碰撞的時候,而且加上他們的速度下一步還是處於碰撞範圍內,就像引力一樣無法脫離,無限原地碰撞。這時候,需要其他小球碰撞來解散這種糾纏。有時候,可能3個小球都會一起進入無限糾纏的狀態。(判斷碰撞-是-速度反方向-遠離-判斷碰撞-速度反方向-靠近-判斷碰撞-是-速度反方向-遠離……無限循環)
3.解決方案
對於邊界,防止黏住邊界,我們可以重置它的位置,讓他剛剛好離開邊界,比如右邊界
this.x = width - this.r - 5//-5保證它絕對離開,-1有時候也會黏住,但1和5距離差別還是不大的
其他邊界同理
對於兩個小球,我們也是重置位置,這個重置的算法那個常數就看實際情況了。
this.x -= 7*this.vx; //我這裏,實踐證明大於6才比較低概率發生糾纏 //而且6幀也剛剛好是遊戲中的爆炸,那個瞬間有6幀,這樣我們才感覺到存在這個瞬間 //我直接讓他回退6幀,當然球的大小更大的,這個數字也更大 this.y -= 7* this.vy; 複製代碼
解決方案2: 可以給Ball構造函數再初始化一個值:this.isleave = true; 對於Ball.prototype.isCollision函數,我們改動一下,等到碰撞的時候,this.isleave變成false
if(!this.isleave){ if(distance> this.r + balls[j].r){ this.isleave =true;//遠離後 }else{ //do something return; } }else if(distance <= this.r + balls[j].r){ this.isleave = false; //前面的代碼 } 複製代碼
4.模擬核裂變
碰撞的時候,旁邊生成一個新的小球。 因爲鏈式反應,可能會一瞬間就把瀏覽器炸了,所以我們限制小球數量
//Ball.prototype.isCollision一部分更改 if (distance <= this.r + balls[j].r) { balls[j].x -= 7*balls[j].vx; balls[j].y -= 7*balls[j].vy; this.x -= 7*this.vx; this.y -= 7* this.vy; this.vx = -this.vx; this.vy = -this.vy; if(balls.length<30){//裂變到30個就停止 let ball = new Ball(ran(-7,7),ran(-7,7),this.x-17*this.vx,this.y-17* this.vy, this.r, "#"+(~~(Math.random()*(1<<24))).toString(16)); balls.push(ball); } } //loop一部分更改 let loop = function(){ ctx.fillStyle = 'rgba(0,0,0,.2)'; ctx.fillRect(0,0,width,height); while(balls.length<2){//初始兩個球 let ball = new Ball(ran(-7,7),ran(-7,7),ran(0,width),ran(0,height), ran(10,20),"#"+(~~(Math.random()*(1<<24))).toString(16)); balls.push(ball); } for(let i = 0;i<balls.length;i++){ balls[i].draw(); balls[i].update(); balls[i].isCollision(); } requestAnimationFrame(loop); } 複製代碼
5.大魚吃小魚
MDN上面說再生成一個eval(這裏指的是這個會吃掉小球的敵人),是吃掉小球的。我這裏把這個eval也設置成和小球是同一個類的,但是他的isCollision方法就有點不同,會把小球吃掉。爲了保證無限循環,當小球被吃剩5個,eval就會爆炸,又生成原本那麼多小球,繼續循環。
//對這個eval進行定義 Eval.prototype.draw = function(){ ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x,this.y,this.r,0,2*Math.PI); ctx.fill(); } Eval.prototype.update = Ball.prototype.update; Eval.prototype.isCollision = function(){ for(var j = 0; j < balls.length; j++) { var dx = this.x - balls[j].x; var dy = this.y - balls[j].y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= this.r + balls[j].r) { balls.splice(j,1) this.vx = -this.vx; this.vy = -this.vy; this.r += 1; } } } let e = new Eval(10,10,ran(0,width),ran(0,height),20,'#fff'); //初始30個球 while(balls.length<30){ let ball = new Ball(ran(-7,7),ran(-7,7),ran(0,width),ran(0,height), ran(10,20),"#"+(~~(Math.random()*(1<<24))).toString(16)); balls.push(ball); } //loop的改動 let loop = function(){ ctx.fillStyle = 'rgba(0,0,0,.2)'; ctx.fillRect(0,0,width,height); if(balls.length<5){//少於5個,eval又是一個新的eval e = new Eval(10,10,e.x,e.y,20,'#fff'); while(balls.length<30){//循環生成30個球 let ball = new Ball(ran(-7,7),ran(-7,7),ran(e.x,e.x),ran(e.x,e.x), ran(10,20),"#"+(~~(Math.random()*(1<<24))).toString(16)); balls.push(ball); } }else{ e.draw(); e.update(); e.isCollision(); } for(let i = 0;i<balls.length;i++){ balls[i].draw(); balls[i].update(); balls[i].isCollision(); } requestAnimationFrame(loop); } 複製代碼
更加壯觀,是不是?