HTML5“愛心魚”遊戲總結

目錄

  1.頁面搭建

  2.畫藍色的海洋

     3.畫隨海水擺動的漂浮物

     4.畫隨海水擺動的海葵

     5.畫靜態的大魚和小魚

     6.鼠標控制大魚的遊向

     7.給大魚、小魚加基本動畫(眼睛眨動,尾巴不停的搖擺)

     8.小魚跟隨大魚遊動

     9.畫果實

     10.大魚吃果實、大魚喂小魚

     11.特效(大魚吃果實產生白色漣漪,大魚喂小魚產生橙色漣漪)

     12.遊戲分值計算和小魚生命值判斷

     13.遊戲開始前的處理

     14.遊戲結束後的處理

點擊這裏

點擊這裏

  本文是對用HTML5開發的“愛心魚”遊戲總結,主要涉及的知識點有:HTML5 canvas API、輪播序列幀動畫、“物體池”、碰撞檢測、JS中的Math對象、setInterval()、貝塞爾曲線、模塊化開發、面向對象編程思想。

頁面搭建

  整個頁面由三部分組成,都採用絕對定位的方式,使其水平居中。第一部分包含兩個canvas,分為上下兩層,上層即canvas1繪製fishes,text,dust,wave,下層即canvas2繪製background,ane,fruit;第二部分包含一個div,主要用於頁面一加載時,顯示遊戲規則;第三部分也包含一個div,主要用於遊戲結束時,顯示玩家的得分情況。

  建立tinyHeart.css文件用於給html添加樣式,美化頁面。

  建立tinyHeart.js文件用於實現該遊戲的功能。裏面包含基本數據、定義三種狀態、各種圖片的創建、數據對象、業務對象、功能性函數、主函數、事件綁定函數這8大模塊。

畫藍色的海洋

  主要用到canvas API中的drawImage(),將images中的背景圖片畫到canvas2上

畫隨海水擺動的漂浮物

  指定漂浮物的總個數,通過for循環、隨機數、正弦函數為每個漂浮物指定圖片、顯示位置、擺動幅度來營造海水流動的氛圍。

  關鍵點:a.給漂浮物定義一個角度屬性,使其隨事間變化

this.alpha+=deltaTime*0.0008;//注意這個角度需要和海葵擺動的角度相同,deltaTime為每幀畫面的間隔時間

      b.利用正弦函數,可以得到一個[-1,1]之間的隨機值,用來控制漂浮物左右擺動

var l=Math.sin(this.alpha);//[-1,1]

      c.給每個漂浮物定義一個幅度屬性,用隨機數來初始化,用來控制漂浮物左右擺動的幅度

this.amp[i]=Math.random()*15+30;//第i個漂浮物擺動的幅度

      d.在canvas1上畫漂浮物

for(var i=0;i<this.num;i++){
   var curImgIndex=this.index[i];
   ctx.drawImage(dustImg[curImgIndex],this.x[i]+l*this.amp[i],this.y[i]);//將指定圖片畫到指定位置上
}

畫隨海水擺動的海葵

    實現海葵擺動的大致思路是:利用二次貝塞爾曲線來畫海葵,在二次貝塞爾曲線的結束點處使用正弦函數控制從而形成擺動效果。  

    關於貝塞爾曲線的簡單學習:

    先看下維基百科上的圖解:

      

   

二次貝塞爾曲線的結構

    

       

pokemonai_pokemonai

          二次貝塞爾曲線的動態演示

    可以看到,二次貝塞爾曲線由幾部分構成:

      P0到P1的連續點Q0,描述一條線性貝塞爾曲線

      P1到P2的連續點Q1,描述一條線性貝塞爾曲線

      Q0到Q1的連續點B(t),描述一條二次貝塞爾曲線   

    上述中,P0稱為起始點,P2稱為結束點,P1稱為控制點,我們需要做的就是把海葵的底部的x軸座標當做起始點,控制點在該起始點的垂直上方,結束點是由正弦曲線控制的,把結束點的x軸座標值交給正弦函數處理。在正弦函數中,將隨時間變化的一個角度值alpha 當做正弦函數的x,把海葵結束點的x當做正弦函數的y,這樣海葵的x值就可由正弦函數控制,從而營造出隨海水左右擺動的效果。

    關於Math.sin(x)方法,返回的是一個-1到1之間的值,參數x是一個弧度表示的角,將角度乘以 0.017453293 (2PI/360)即可轉換為弧度。

    關鍵代碼:

this.alpha+=deltaTime*0.0008;//海葵擺動的角度值隨時間變化
var l=Math.sin(this.alpha);//使得海葵可以左右擺動
ctx.moveTo(this.rootx[i],this.rooty);//將筆觸移動到海葵生長的位置
this.headx[i]=this.rootx[i]+l*this.amp[i];//當前海葵頭部的具體位置(若想讓果實跟着頭部一起動,這句必不可少)
ctx.quadraticCurveTo(this.rootx[i],canHeight-150,this.headx[i],this.heady[i]);//畫二次貝塞爾路徑

   海葵的繪製還使用了canvas API中的漸變對象,畫出了一條條漸變的海葵,除此之外還設置了透明度,使得海葵看起來更漂亮。

var gradient=ctx.createLinearGradient(this.rootx[i],canHeight,this.headx[i]+l*this.amp[i],this.heady[i]);//創建漸變對象
gradient.addColorStop(0,'#003300');//設置顏色斷點值
gradient.addColorStop(0.3,'#336600');
gradient.addColorStop(0.6,'#339933');
gradient.addColorStop(0.8,'#99CC33');
gradient.addColorStop(1,'#00CC00');
ctx.strokeStyle=gradient;//使線條的顏色為漸變色

畫靜態的大魚和小魚

    繪製靜態的大魚、小魚時,整體分為魚的眼睛、身體、尾巴這三部分,將預先準備好的大魚和小魚的各部分的圖片用drawImage()函數畫到canvas1上,為了方便後期實現大魚、小魚隨意遊動的特效,在繪製圖片之前先用ctx.translate(newX,newY)重新映射canvas1座標原點的位置[即將canvas1的座標原點映射到(this.x,this.y)處],在繪製大、小魚身體各部分圖片時的座標位置永遠是相對於canvas1座標原點的位置

    注:此處重新映射了canvas1的座標原點位置,為了不影響其他功能的實現,故在映射之前先用ctx.save()保存畫布當前的狀態,在繪圖完成之後再用ctx.restore()恢復到最近一次保存的canvas狀態

    關鍵代碼:

ctx.save();//因為要重新映射畫布圓點的位置和旋轉畫布,故需要先保存畫布的當前狀態
ctx.translate(this.x,this.y);//將畫布的原點位置重新映射到(this.x,this.y)處
ctx.drawImage(this.momTail[tailCount],-this.momTail[tailCount].width*0.5+30,-this.momTail[tailCount].height*0.5);
ctx.drawImage(pic,-pic.width*0.5,-pic.height*0.5);
ctx.drawImage(this.momEye[eyeCount],-this.momEye[eyeCount].width*0.5,-this.momEye[eyeCount].height*0.5);
ctx.restore();

鼠標控制大魚的遊向

    實現鼠標控制大魚,可以在上層畫布canvas1上(繪製魚的層)使用addEventListener()函數偵聽onMouseOver事件:

can1.addEventListener('mousemove',onMouseMove, false);

    這裏又涉及到addEventListener()方法,引用MDN的解釋:

    addEventListener() 方法將指定的事件監聽器註冊到目標對象上,當該對象觸發指定的事件時,指定的回調函數就會被執行。 事件目標可以是一個文檔上的元素, document 本身, window , 或者 XMLHttpRequest.

    也可給上層畫布canvas1上(繪製魚的層)綁定onmousemove事件(本遊戲中我採用的方法)

can1.onmousemove= getMousePos;//getMousePos()為獲得鼠標位置的函數

    獲得鼠標位置的函數:

//獲取鼠標的位置
function getMousePos(e){
    if(state!=GAMEOVER){
        if(e.offsetX||e.layerX){
            mx=e.offsetX ? e.offsetX : e.layerX;
            my=e.offsetY ? e.offsetY : e.layerY;
        }
    }
}

    在檢測鼠標位置時,需要考慮到瀏覽器的兼容性,使用layerX和layer來兼容Firefox瀏覽器,其他的瀏覽器都可以用offset來實現。這裏有一篇詳細介紹的文章

    檢測到鼠標的位置後,可以在畫大魚時將鼠標的移動位置傳遞進去也可將鼠標的位置設為全局變量(本遊戲我採用),然後需要用到lerpDis()函數將大魚座標趨近於鼠標的座標。

//讓一個數值趨向於另一個數值
//aim:被趨近於的值
//cur:當前值
//rate:趨近程度

function lerpDis(aim,cur,rate){
    var delta=aim-cur;
    return cur+delta*rate;
}

    通過上面函數就可以使cur(當前值)趨近於aim(目標值),radio表示趨近的程度,以百分比表示
可以舉個例子: aim(目標值)是6,cur( 當前值)是3, 趨近程度90%, 即:3+(6-3)*0.9=5.7, 返回的值可以趨近於當前值6. 它的作用是可以使得動畫變得更加平滑。      

   而在移動大魚時,只需用translate()這個函數來重新映射can1的座標原點位置即可。

   接着需要讓大魚隨着鼠標旋轉:這裏用到的第一個知識點就是Math.atan2(y, x)函數,即Javascript反正切函數:

   Math.atant2(y, x)返回的是正X軸和點 (x, y) 與原點連線 之間的角度,這個角度是一個 -PI到 PI之間的弧度,表示點 (x, y) 對應的偏移角度。這是一個逆時針角度,以弧度為單位。
        注意此函數接受的參數:先傳遞 y 座標,然後是 x 座標。還需要注意的是 atan2 接受單獨的 x 和 y 參數,而 atan 接受兩個參數的比值。

   由於 atan2 是 Math 的靜態方法,所以應該像這樣使用:Math.atan2(),下面是例子:

Math.atan2( ±0, -0 ) // ±PI. 
Math.atan2( ±0, +0 ) // ±0.

    用到的第二個知識點就是rotate()函數,使用方法:ctx.rotate(angle)旋轉當前畫布,其中angle表示旋轉的角度,以弧度計算.

    檢測到鼠標的角度後,可以用lerpAngle()函數讓一個角度值趨近於另一個角度值,控制大魚朝着鼠標運動:

//讓一個角度值趨向於另一個角度值
//aimAngle:目標值的角度(將被趨近的那個值)
//curAngle:當前值
//rate:表示趨近程度,是一個百分比的值
function lerpAngle(aimAngle,curAngle,rate){
    var gapAngle=curAngle-aimAngle;
    if(gapAngle>Math.PI){
        gapAngle=gapAngle-2*Math.PI;
    }
    if(gapAngle<-Math.PI){
        gapAngle=gapAngle+2*Math.PI;
    }
    return aimAngle+gapAngle*rate;
}

    關鍵點:a.根據鼠標的位置與大魚當前的位置重新映射can1的座標原點,用ctx.translate(x,y)------遊動

        b.根據鼠標的角度和大魚的當前角度旋轉畫布,用ctx.rotate(degree);------旋轉

//lerp x,y(更新魚的當前位置)
this.y=lerpDis(my,this.y,0.019);
this.x=lerpDis(mx,this.x,0.019);
        
//lerp angle(更新魚的當前角度值)
var deltaY=my-this.y;//鼠標所在位置的y值與大魚所在的位置的y值之差
var deltaX=mx-this.x;//鼠標所在位置的x值與大魚所在的位置的x值之差
var aimAngle=Math.atan2(deltaY,deltaX)+Math.PI;//當前值(被趨近於點)的角度值(極座標系下),用反正切函數算得
this.angle=lerpAngle(aimAngle,this.angle,0.80);//更新大魚的當前角值
ctx.translate(this.x,this.y);//將畫布的原點位置重新映射到(this.x,this.y)處
ctx.rotate(this.angle);//旋轉畫布

給大魚、小魚加基本動畫(眼睛眨動,尾巴不停的搖擺)

    大魚和小魚的動畫包括:大、小魚眼睛的眨動,尾巴的搖擺,和身體顏色的變化。這些都是利用相同的原理,即輪播序列幀。

    首先説大小魚身體顏色動畫:預先準備好身體慢慢變色的素材圖片,建立一個數組,用for循環將這些素材的地址全都放進數組中,依次加載身體顏色的圖片,對大魚來説,用if語句判斷吃到藍色果實則身體逐漸變藍,否則變橙色。對小魚來説,在預計的時間內沒有和大魚碰撞或者發生碰撞時大魚沒有吃到果實,則小魚身體逐漸變白,完全變白即遊戲結束。

    至於眼睛眨動,和上面的不同,這個步驟需要準備眼睛睜開和閉合的兩張圖片,然後定義一個時間間隔的變量,因為睜開眼睛的時間較長且這個時間是在一定範圍內隨機的,而眨動眼睛的時間很短,所以可以這樣:

this.babyEyeTimer+=deltaTime;
if(this.babyEyeTimer>this.babyEyeInterval){
      //當達到某一時間值後,根據當前顯示的是哪張圖片,決定過多久以後,播放下一張圖片
      this.babyEyeCount=(this.babyEyeCount+1)%2;
      if(this.babyEyeCount==0){
          //當前顯示的是“睜眼”的那張圖片
          this.babyEyeInterval=Math.random()*1500+2000;//為了使魚眼睛“睜”,“閉”的有規律,故採用隨機數
      }else{
          //當前顯示的是“閉眼”的那張圖片
          this.babyEyeInterval=200;
      }
          this.babyEyeTimer=0;//計時器清0,為下一次計時做準備
}

小魚跟隨大魚遊動

    實現原理同大魚跟隨鼠標遊動

畫果實

    此處使用了“物體池”,“物體池”的概念見“特效”部分。因為“果實是不斷出生的,故果實對象除了init()方法、draw()方法,還要有dead()方法、sendFruit()方法、monitorFruit()方法、born()方法

    關鍵方法實現代碼:

    sendFruit()方法:----找到可以出生的果實

this.sendFruit=function(){
      //產生新的果實
      for(var i=0;i<this.num;i++){
          if(!this.alive[i]){
              //第i顆果實若在休眠狀態,就可以出生
              this.born(i);
              return;//一旦找到新的果實出生,就不再找新的果實
           }
      }
 };

    monitorFruit()方法:-----用於監控當前屏幕上果實的數量,從而在滿足一定條件時允許產生新果實

this.monitorFruit=function(){
     var number=0;//用於記錄當前活着的果實數量
     for(var i=0;i<this.num;i++){
         if(this.alive[i]){
             number++;
         }
     }
     if(number<15){
         //當活着的果實個數低於20個的話,產生新果實
         this.sendFruit();
         return;
     }
}

    born()方法:------產生新果實

this.born=function(i){
     //主要是確定每個果實將出生在哪株海葵上
     var aneID=Math.floor(Math.random()*ane.num);//得到一個隨機的下標
     this.alive[i]=true;//一旦該果實被重生,它就可以執行任務
     this.size[i]=0;//指定第i個果實出生時的大小
     this.aneId[i]=aneID;//第i顆果實將來要長在下標為aneId的那株海葵上(在果實出生時告訴他會長在哪株海葵上)
     var randomFruit=Math.random();//用於產生不同種類的果實(此值為:[0,1)之間的一個數)
     if(randomFruit<0.2){
         //產生藍色果實
         this.fruitType[i]='blue';
     }else{
         //產生黃色果實
         this.fruitType[i]='orange';
     }
};

大魚吃果實、大魚喂小魚

    大魚吃果實,大魚喂小魚的實現方法是一樣的,都是利用碰撞檢測的方法,即計算兩者之間的距離,當這個距離小於某個值時,就認為發生了碰撞,即大魚吃掉了果實,大魚給小魚餵食了。

    此遊戲利用的碰撞檢測方法(即計算兩者之間的距離):

//計算兩點之間的距離
//果實(對象1)的座標:(x1,y1)
//大魚(對象2)的座標:(x2,y2)
function distance(x1,y1,x2,y2){
    return Math.pow(x1-x2,2)+Math.pow(y1-y2,2);//此處用勾股定理計算的
}

    喂小魚時,需要檢測大魚是否吃到了(積累了)果實,如果那時大魚吃的有果實,那麼碰撞有效,產生特效,小魚身體顏色恢復紅色,並計分值。如果那時沒有吃到果實,則碰撞無效,沒有特效,小魚顏色繼續為白色,不計分值。這裏面主要是檢測喂小魚時,大魚是否吃到了(積累了)果實。
    檢測大魚吃到了(積累了)果實可以使用一個計數器,每當大魚和果實碰撞計數器就加一,當大魚喂小魚時,判斷計數器是否大於0,大於0就有效,然後執行和小魚碰撞後的一些列事情,並將計數器清零,不大於0就無效。

大魚吃果實,大魚喂小魚的具體實現:

//碰撞檢測
function crashTest(){
    //遍歷每一顆果實,看它與大魚之間的距離,若距離小於某值,則認為大魚將果實吃掉了
    for(var i=0;i<fruit.num;i++){
        if(fruit.alive[i]&&state==RUNNING){
            if(distance(fruit.x[i],fruit.y[i],mom.x,mom.y)<900){
                fruit.dead(i);//一旦被大魚吃掉,該果實就死去了
                data.eatFruitNum++;//大魚吃到的果實數增加
                mom.momBodyCount++;//大魚每吃到一顆果實身體顏色就會變化
                if(mom.momBodyCount>=7){
                    mom.momBodyCount=7;//因為圖片資源有限,故當大魚身體圖片播放到最後一張後,就不再變化
                }
                //判斷大魚吃到的果實類型
                if(fruit.fruitType[i]=='blue'){
                    data.double=2;//當大魚吃到“藍色”果實需要做記錄,用於最後的分數統計
                }
                whiteWave.bornWhite(i);//使白色漣漪出生
            }
        }    
    }
    //判斷大魚和小魚之間的距離,若距離小於某一個值,則認為大魚有喂小魚果實
    if(distance(mom.x,mom.y,baby.x,baby.y)<900){
        if(data.eatFruitNum!=0&&state==RUNNING){
            //當大魚有吃到果實時才能喂小魚,從而產生橙色漣漪
            orgWave.bornOrg();
            mom.momBodyCount=0;//大魚喂小魚之後,身體就變為白色
            baby.babyBodyCount=0;//當大魚喂小魚之後,小魚就恢復了生命
            data.upScore();//當大魚喂小魚果實後,更新分數
        }    
    }
}

特效(大魚吃果實產生白色漣漪,大魚喂小魚產生橙色漣漪)

  特效有大魚吃到果實時候的效果,和大魚喂小魚時候的效果:在每次大魚吃到果實時候就會產生白色漣漪,漣漪由小到大,由不透明到透明逐漸變化。它們實現原理相似:
首先,需要介紹一個”物體池“的概念:

  物體池:規定裏面的物體是有一定數量的,當需要從“物體池”中拿出一個物體時,先檢測是否有閒置的物體,如有閒着的,才能拿出來執行任務,用完後再放回去。

  設定漣漪的數量,比如15個,初始化半徑,還有狀態,狀態分為false(不顯示)和true(顯示),初始時狀態全部設為false,當大魚和果實碰撞時,遍歷漣漪的數量,找出狀態為false的,並將其修改為true,然後執行畫漣漪的動作,漣漪變大變透明後,再將其狀態修改為false,以便後面繼續使用。

  在畫漣漪的過程中,有個技巧是將漣漪的半徑和透明度設置為反比關係(漣漪的半徑越大,透明度越小),當半徑大於某個值時,將其狀態修改為false,並用break語句跳出:

for(var i=0;i<this.num;i++){
    if(this.alive[i]){
         //當第i個漣漪“活着”的時候,就可以執行任務(即被繪製)
         this.size[i]+=deltaTime*0.06;
         if(this.size[i]>this.maxR){
              this.size[i]=this.maxR;
              this.alive[i]=false;//當漣漪的尺寸大於某一特定值時,該漣漪就死亡
              break;
         }
            
         ctx.beginPath();
         this.alpha=1-this.size[i]/60;//隨着漣漪半徑的擴大,漣漪的透明度在逐漸減小
         ctx.strokeStyle='rgba('+this.colorArr[0]+','+this.colorArr[1]+','+this.colorArr[2]+','+this.alpha+')';
         ctx.arc(this.x[i],this.y[i],this.size[i],0,Math.PI*2,false);//順時針畫一個圓形路經
         ctx.stroke();//將路徑畫到畫布上
         ctx.closePath();    
    }
}

   注:因為橙色漣漪和白色漣漪,都是漣漪,故不需要創建兩個業務對象,只需要創建一個業務對象和兩個數據對象就可。其中業務對象包含的方法有:初始化漣漪、在canvas上畫漣漪、產生白色漣漪、產生橙色漣漪(因為兩種顏色的漣漪產生條件不同,故此處給漣漪對象創建兩個產生漣漪的方法)

this.bornWhite=function(k){
        //產生白色漣漪(產生的位置由被吃掉的果實的位置決定)
        for(var i=0;i<this.num;i++){
            if(!this.alive[i]){
                //當第i個漣漪處於休眠狀態,那麼它就可以執行任務
                this.alive[i]=true;//當第i個漣漪可以執行任務,就將它改為“活着”狀態
                this.size[i]=0;
                this.x[i]=fruit.x[k];
                this.y[i]=fruit.y[k];
                break;
            }
        }
    };
    this.bornOrg=function(){
        //產生橙色漣漪(產生的位置由小魚的位置決定)
        for(var i=0;i<this.num;i++){
            if(!this.alive[i]){
                this.alive[i]=true;
                this.size[i]=0;
                this.x[i]=baby.x;
                this.y[i]=baby.y;
                break;
            }
        }
    };
}

遊戲分值計算和小魚生命值判斷

  分值的計算和大魚吃到的果實有關,橙色果實是10分,藍色果實是20分,這要在大魚和果實碰撞的時候檢測,具體的方法是分值計算中,聲明一個用於判斷吃到果實類型的變量,默認為1,當吃到藍色果實則賦值為2,利用這個值來做分值計算,當做完計算後把吃到的果實的數量清零,即重新吃果實,並且把該變量重新賦值為1.為下一次的計算做準備。

  小魚生命值的判斷,和分值是直接相關的,當小魚沒有被大魚餵食物的時候,身體是逐漸變白的,當到全白的時候,生命結束,即遊戲結束。

  注:分值的計算是每次大魚喂小魚果實後計算。

  分值計算:

this.upScore=function(){
      //用於更新分數
      this.score+=this.eatFruitNum*this.baseScore*this.double;  //this.double就是用於判斷吃到果實類型的變量,默認為1,當吃到藍色果實則賦值為2
      //更新完分數後,恢復默認值,用於進行下一次分數的統計
      this.eatFruitNum=0;
      this.double=1;
};

  小魚生命值的判斷:

if(state==RUNNING){
     //body count
     this.babyBodyTimer+=deltaTime;
     if(this.babyBodyTimer>300){
         this.babyBodyCount++;
         if(this.babyBodyCount>=19){
             //當小魚身體圖片輪播完以後,遊戲結束
             this.babyBodyCount=19;//讓小魚顯示白色身體(即小魚死亡)
             data.historyScore=data.score;//保存當前的得分為最高歷史得分
             data.saveScore();//用localStorage將當前的得分保存
             showMyScore();//顯示我的成績
             state=GAMEOVER;
          }
             this.babyBodyTimer=0;//計時器清零,用於下一次計算
      }    
 }

  這中間涉及到小魚被餵食後身體又恢復的狀態,這個比較簡單,只要把身體的幀重置到第1幀,也就是小魚最開始的身體狀態就可以了。

遊戲開始前的處理

  頁面一加載顯示與遊戲相關的信息,這部分分為上下兩部分,上面用於顯示相關的遊戲規則,下面顯示進入遊戲的入口。這部分在寫CSS樣式時使用了box-shadow、border-radius在填充背景顏色時填充為漸變色,美化了該界面。當用户點擊“開始遊戲”就進入了遊戲界面,可以玩遊戲。

遊戲結束後的處理

  當遊戲結束時使“GAMEOVER”漸顯出來,同時會有一部分處於半透明狀態浮在"GAMEOVER"上面,用於顯示本次遊戲玩家的得分、最高歷史得分、最下面添加了“再玩一次”和“不玩了”功能。

  此部分顯示效果同“遊戲開始前的處理”。其中 “GAMEOVER”是用canvas API 寫上去的,同時用了一些用於字體美化的API。

  • ctx1.shadowBlur: 設置或返回陰影的模糊級數,級數越大,模糊程度越大
  • ctx1.shadowColor:設置或返回用於陰影的顏色,配合shadowBlur,創建陰影效果,
  • ctx1.shadowOffsetX 和 ctx1.shadowOffsetY: 分別設置或返回陰影的水平和垂直距離,可以是正數,負數,0; 當設置為0時,兩者都是位於正下方。當為正數時,shadowOffsetX=20表示陰影位於形狀left位置右方的20像素處, shadowOffsetY=20表示陰影位於形狀top位置下方的20像素處,當為負數時,shadowOffsetX=-20表示陰影位於形狀left位置左側的20像素處, shadowOffsetY=-20表示陰影位於形狀top位置上側的20像素處。
  • ctx1.textAligin:根據錨點,設置或返回文本內容的當前對齊方式。有start(文本在指定位置開始,默認),end(文本在指定位置結束),center(文本的中心被放在指定位置),left(文本左對齊),right(文本右對齊)
  • ctx1.font: 設置或返回畫布上文本內容的當前字體屬性,可以指定字體樣式(font-style),字體粗細(font-weight),字體系列(font-family)等。

  "GAMEOVER"的顯示:

  Game Over的顯示主要涉及的知識點就是字體透明度的變化,由透明逐漸變為不透明。可以利用”rgba(R,G,B,A)”形式的字符串,A代表alpha,可以指定為0.0(完全透明)和1.0(完全不透明)之間的浮點數值。然後需要一個變量來從0.0逐步增加到1.0,這個變化過程可以使Game Over逐漸顯示出來。

  

//繪製GAMEOVER
function showGameOver(ctx){
    textAlpha+=deltaTime*0.0005;//透明度隨時間變化
    if(textAlpha>1){
        textAlpha=1;
    }
    ctx.save();//保存畫布的當前狀態
    ctx.shadowColor='#FFF';
    ctx.shadowBlur=10;
    ctx.globalAlpha=textAlpha;
    ctx.font='54px Verdana';
    ctx.textAlign='center';
    ctx.fillStyle='#FFF';
    ctx.textBaseline='bottom';//設置文字的對齊方式
    ctx.fillText('GAMEOVER',canWidth*0.5,canHeight*0.5);//在canvas上寫字
    ctx.restore();//恢復畫布的狀態到最近一次save()的狀態
}

   Game Over顯示後的界面控制:
    在遊戲結束後,需要使玩家失去對界面的控制,即鼠標不能控制大魚,大魚和小魚停止運動。回顧前面的鼠標控制大魚階段,已經做到了大魚響應鼠標移動,現在只需在小魚完全透明時候,設置state='GAMEOVER';即可,然後在getMousePos(e)函數中,if判斷一下,如果遊戲結束就不再獲取鼠標的位置,進而就不能控制大魚運動了。