追逐演算法之--牛鞭的子彈是怎樣練成的(4)--散射子彈
松江上死豬的餘香還未散去,昨天所有大型的禽類交易市場都被關閉了,撲殺了n萬隻生雞,哎!雞這種動物在中國真的有夠悲劇,一遇到什麼頭疼腦熱,掃黃打非,最先倒黴的總是這種生物,自己不檢點出了問題,關雞屁事啊!今天又感染了2個,病毒在上海這種密度的地方,都是以指數數量級傳播的,一場新的生化危機即將爆發,這已經不是什麼時候爆發的問題了,而是立刻爆發,還是明天爆發的問題了。我平時每天吃雞腿飯的時候都要加兩塊素雞的,今天嚇的連素雞也沒敢讓老闆加。。太悲劇了
要說悲劇這還不算悲劇,悲劇的是我每天加班到10點多,回來還得熬夜給你們這幫看了部落格不頂的白眼狼寫教程,我的命真是賤,我估計要不了多久,我就會過勞死了,等我死後,我會保佑所有頂了貼的朋友,沒有頂貼的朋友也不用害怕,我夜裡會經常去找你們玩的。。。。
今天的內容比較多,所以就不扯蛋蛋了,直插主題。
今天我們來做一個散射子彈的效果,因為是散射,所以就需要有子彈的方向和旋轉,這裡我們需要引入向量來進行描述,具體的原理如下:
假如我們要讓飛機或者子彈往A向量的方向飛,我們該怎麼做呢?如圖,a(x1,y1)點就可以代表當前的飛機或者子彈的位置, 我們用b點(目標點)的座標減去a點的座標(x2-x1,y2-y1)就可以得到A向量了,之後我們將A向量歸一化,如果有同學不知道歸一化是什麼的,那麼我覺得,傳銷或者公務員的工作會比較適合你,歸一化後,得到A'向量(dx,dy),此時我們用a點的座標分量,分別加上A'向量的模向量的分量(x1+dx,y1+dy)就可以使飛機向A向量的方向,移動一個單位的距離了,如果飛機有一個固定的速度speed,只要讓它乘以A'向量就可以了,即(x1+dx*speed,y1+dy*speed)就可以得到飛機往A向量方向移動speed個單位a'模向量的位置了!
ok下面我們來動手實現,恰好java自己有一個自帶的類叫Vector就是向量的意思,但是我怎麼看怎麼覺得這貨是個集合,擦!這貨還真他孃的是個集合,這個語法還真和c裡面的指標*有的一拼啊,這些java大神們的想法豈是吾等鼠輩可以理解的?難怪有本書叫做 Thinking in Java ,好吧,木有辦法,我只好自己寫一個簡單的向量類,並且實現一些向量中的基本演算法與功能,暫且叫它PVector類吧
package planet; public class PVector { public float x, y; public PVector(double x1,double x2) { this.x = (float) x1; this.y = (float) x2; } //向量相加 public PVector add(PVector v) { float x = this.x + v.x; float y = this.y + v.y; return new PVector(x, y); } //向量相減 public PVector sub(PVector v) { float x = this.x - v.x; float y = this.y - v.y; return new PVector(x, y); } //返回一個新的PVector的副本 public PVector newPVector(){ return new PVector(this.x,this.y); } //將向量按逆時針繞原點旋轉 public void rotateCW(double radian) { double rx = (this.x* Math.cos(radian))+ (this.y* Math.sin(radian)); double ry =(this.y* Math.cos(radian))- (this.x* Math.sin(radian)); x= (float) rx; y= (float) ry; this.x=x; this.y=y; } //將向量旋轉任意角度 public void rotateAngle(int angle){ if (angle >0){ this.rotateCWAngle(angle); }else{ angle =-angle; this.rotateCCWAngle(angle); } } //將向量逆時針繞原點旋轉 public void rotateCWAngle(double angle) { double radian = PVector.transAngle(angle); rotateCW(radian); } // 將向量逆時針繞原點旋轉 public void rotateCCW(double radian) { double rx = (this.x* Math.cos(radian))- (this.y* Math.sin(radian)); double ry = (this.x* Math.sin(radian))+ (this.y* Math.cos(radian)); x= (float) rx; y= (float) ry; this.x=x; this.y=y; } //將向量逆時針繞原點旋轉 public void rotateCCWAngle(double angle) { double radian = PVector.transAngle(angle); rotateCCW(radian); } //角度換成弧度 public static double transAngle(double angle){ return Math.PI/180*angle; } // 檢測兩個向量之間的夾角(返回角度) public double checkVectorAngle(PVector p2){ double n = this.x*p2.x+this.y*p2.y; double m =Math.sqrt(this.x*this.x+this.y*this.y)*Math.sqrt(p2.x*p2.x+p2.y*p2.y); return Math.acos(n/m); } //取模運算 public float getLength (){ float dis = Calculate.getDistance(this.x, this.y).floatValue(); return dis; } // 歸一化 public PVector normalize() { float dis =this.getLength(); if (dis == 0) dis = 0.00000000001f; float x1 = this.x / dis; float x2 = this.y / dis; return new PVector(x1, x2); } //設定向量的x,y分量 public void setVector(float x, float y){ this.x=x; this.y=y; } }
ok工具有了,下面我們來開始實現,我們首先在Bullet類中新增兩個屬性
//目標向量
public static PVector targetV = new PVector(0,0);
//初始速度向量(預設是垂直往上的向量)
private PVector initV=new PVector(0, -1);
在update函式中 我們按照上面的原理改寫原來的子彈移動演算法
public void update(){
this.checkDead();
this.checkEnemyCollision();
//將x,y座標,分別加上初始方向模向量的x,y分量*速度值
this.x = this.x+initV.x*speed;
this.y = this.y+initV.y*speed;
}
這樣一寫的話,我們發現一個問題,我們加入一種新的子彈的時候,原來的子彈就得刪除掉,而我們希望遊戲中能有多種多樣的子彈射擊方式和子彈樣式,這該咋辦呢?
這裡我們需要將Bullet類的update和draw函式改寫一下,以適應實現多種子彈的功能!我們首先建立一個介面TypeConst介面,在裡面定義各種子彈和射擊方式
package planet;
public interface TypeConst {
//我方普通子彈
int MY_BULLET_NORMAL_1 =1;
//我方直線向量子彈
int MY_BULLET_LINE =3;
//敵方的普通子彈
int ENEMY_BULLET_NORMAL_1 =2;
//發射一顆子彈的射擊方式
int MY_SHOT_SINGLE_BULLET_1 =1;
//發射多顆子彈的射擊方式
int MY_SHOT_MULITI_BULLET_1 =2;
}
上面的靜態變數MY_BULLET_NORMAL_1中,第一位MY-代表是敵方還是我方打出的子彈,第二位可以是BULLET 也可以是SHOT,如果是BULLET就代表是指的子彈型別,SHOT的話就代表射擊方式,第三位代表特點,比如我們這裡要做的子彈就是直線向量子彈,上節課的就叫普通子彈,最後一位就是編號了。
我們在Bullet類中新增一個屬性int bulletType = TypeConst.MY_BULLET_LINE;用來代表子彈的型別
下面我們將Bullet類中的邏輯迴圈如此改寫,
public void update(){
this.checkDead();
this.checkEnemyCollision();
this.updateBulletType();
//將x,y座標,分別加上初始方向模向量的x,y分量*速度值
this.x = this.x+initV.x*speed;
this.y = this.y+initV.y*speed;
}
//設定子彈的型別的更新內容
public void updateBulletType(){
switch (this.bulletType){
case TypeConst.MY_BULLET_NORMAL_1:
//設定初始方向向量
initV.setVector(0,0);
this.y-=speed;
break;
case TypeConst.ENEMY_BULLET_NORMAL_1:
initV.setVector(0,0);
this.y+=speed/2;
break;
case TypeConst.MY_BULLET_LINE:
//改變子彈速度
this.setSpeed(15);
break;
}
}
將渲染迴圈如此改寫 ,由於我們期望子彈不只是圓形的還可能是長方形的,我們把子彈直徑radius,改變為兩個變數rdiusW, rdiusH
public void draw(Graphics g){
this.drawBulletType(g);
}
//設定子彈的型別的渲染內容
public void drawBulletType(Graphics g){
switch (this.bulletType){
case TypeConst.MY_BULLET_NORMAL_1:
g.setColor(Color.blue);
g.fillOval((int)this.x,(int)this.y, rdiusW, rdiusH);
break;
case TypeConst.ENEMY_BULLET_NORMAL_1:
g.setColor(Color.YELLOW);
g.fillOval((int)this.x,(int)this.y, rdiusW, rdiusH);
break;
case TypeConst.MY_BULLET_LINE:
g.setColor(Color.blue);
g2d.drawRect((int)this.x,(int)this.y, rdiusW, rdiusH);
break;
}
}
ok,現在你只要改動Bullet類中的int bulletType = TypeConst.MY_BULLET_LINE;的型別就可以切換不同的子彈型別了。
此時執行,我們發現子彈垂直的往上打和之前的沒有神馬區別,現在咱們把initV改成(-1,-1),也就是讓初始的方向向量變為偏飛機左邊45度角的地方,再測試一下!ok子彈可以斜著飛了,不過看起來有一些奇怪,那是因為,你的發射角度旋轉了45度,而咱們的圖形沒有旋轉45度,只要我們把圖形也旋轉45度看著就很舒服了,由於Graphics 沒有旋轉圖形的功能,所以我要將它轉型為Graphics2d ,他是具有旋轉圖形一定角度的函式的,這個地方大家不必理會,也不用去研究,因為我不是在教大家如何用java編寫pc遊戲,大家肯定可以在自己開發的遊戲平臺或引擎裡輕鬆的找到這個旋轉圖形的函式或功能的,所以你只記住這裡計算出旋轉角度的方法和演算法就可以了!
這裡我們現在Bullet類中新增一個rotateAngle屬性,用來記錄圖形旋轉的角度,然後在update()函式中,通過求子彈向量與垂直向量的夾角,這個夾角也就是圖形需要旋轉的角度了,這裡要特別注意一下,向量是左偏於垂直向量,還是右偏。
public void update(){
this.checkDead();
this.checkEnemyCollision();
this.updateBulletType();
//將x,y座標,分別加上初始方向模向量的x,y分量*速度值
this.x = this.x+initV.x*speed;
this.y = this.y+initV.y*speed;
//區分左偏垂直線,還是右偏
if (initV.x<0)
//求出initV向量與垂直向量之間的夾角
this.rotateAngle=-new PVector(0,-1).checkVectorAngle(initV);
else
this.rotateAngle=new PVector(0,-1).checkVectorAngle(initV);
}
然後我們把drawBulletType()函式中的直線子彈改成這樣,主要我們旋轉的是畫筆,這個畫筆是所有Bullet公用的一個,並不是每一個Bullet子彈例項都有一個自己的畫筆,所以這裡我們旋轉完每一顆子彈的畫筆後,都要復原回去
case TypeConst.MY_BULLET_LINE:
g.setColor(Color.blue);
//先旋轉計算出的rotateAngle角度
g2d.rotate(this.rotateAngle,this.x, this.y);
//設定子彈的長寬
this.setRdius(5, 50);
g2d.drawRect((int)this.x,(int)this.y, rdiusW, rdiusH);
//復原rotateAngle角度
g2d.rotate(-this.rotateAngle,this.x, this.y);
break;
下面我們為Bullet類新增一個建構函式,讓外部建立的時候能夠直接指定,子彈的型別
public Bullet(double x,double y,boolean isEnemy,int bulletType){
this(x, y,isEnemy);
//傳入子彈的型別
this.bulletType = bulletType;
}
ok現在的子彈已經可以斜射並且旋轉了,不過只能打一顆子彈實在是不爽,我們下面來給我們的飛機添一種新的射擊方式,讓它可以自動產生多個方向不同角度的直線向量子彈,當然,我們還需要對MyPlanet類進行類似Bullet類的改造,讓它具有能夠擁有不同射擊方式的功能
我們需要給MyPlanet類增加一個public int shotType =2;屬性表示它的射擊方式,然後我們改寫fire()函式
public void fire(){
switch(this.shotType){
case TypeConst.MY_SHOT_SINGLE_BULLET_1:
//產生一顆子彈,位置就在自己飛機的正前方
if(this.fireCDTimer.act())
//這裡的+20和-5用來調整子彈的初始位置,讓它從飛機的正前方打出來
new Bullet(this.x+20,this.y-5);
break;
case TypeConst.MY_SHOT_MULITI_BULLET_1:
}
}
下面我們寫一個能夠自動生成多方向子彈的函式
/***
* 產生多顆子彈
* @param bulletNum 子彈數量
* @param amongAngle 子彈角度間隔
*/
public void makeMulitiBullet(int bulletNum,int amongAngle){
if(bulletNum<=1)return;
//先建一顆垂直向上的子彈
new Bullet(this.x+20,this.y-5,false,TypeConst.MY_BULLET_LINE).setInitV(new PVector(0,-1));
//用來判斷左右偏轉的標誌
boolean flag=false;
//用來計算偏移角度
int num=1 ;
while(bulletNum-->0){
int rotateNum=++num/2;
//設定初始向量
PVector initV = new PVector(0,-1);
//如果flag是true就將向量向左邊偏轉,否則向右偏轉
if(flag)
initV.rotateAngle(amongAngle*rotateNum);
else
initV.rotateAngle(-amongAngle*rotateNum);
//新建一個旋轉的直線向量子彈
new Bullet(this.x+20,this.y-5,false,TypeConst.MY_BULLET_LINE).setInitV(initV);
//轉換標記
flag = !flag;
}
}
這樣,我們只需要呼叫這個函式,並傳入子彈的數量,和子彈間的角度間隔,那麼它就會自動幫我們生成散射的子彈了!
最後別忘了,Enemy類建立子彈時,也要給它傳入初始的bulletType。這時我們再演示一下,看到了吧,我們的飛機變得凶殘起來了!