從MDN上的canvas例子受到的啟發

lhyt發表於2018-06-23

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個小球都會一起進入無限糾纏的狀態。(判斷碰撞-是-速度反方向-遠離-判斷碰撞-速度反方向-靠近-判斷碰撞-是-速度反方向-遠離……無限迴圈)

default

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);
}
複製程式碼

更加壯觀,是不是?

相關文章