HTML5遊戲開發進階 4 :物理引擎整合

CopperDong發表於2018-02-10

   首先向關卡中新增物體,對這些物體進行Box2D物理模擬,然後讓它們在遊戲中動起來。我們將使用這些物體建立測試關卡,加入滑鼠互動,使遊戲真正具有可玩性。最後,再向測試關卡加入音效和背景音樂,製作出一個完整的遊戲。

4.1 定義物體

     裝載的物體:英雄、壞蛋、地面和環境中的障礙物。

     屬性type包含:"hero", "villain", "ground", "block"這樣的值。

     entities 物件

4.2 新增Box2D

      建立box2d物件

4.3 建立物體

      定義entities.create方法:

4.4 向關卡加入物體

      levels.array陣列

      levels.load

4.5 設定Box2D除錯繪圖

     <canvas id="debugcanvas" width="1000" height="480" style="border:1px solid block;"></canvas>

    僅僅是設計和測試關卡

4.6 繪製物體

    entities物件內部定義draw()方法

4.7 Box2D動畫

    時間步長

    box2d.step()

4.8 載入英雄

     第一個階段load-next-hero

     game.countHeroesAndVillains();

4.9 發射英雄

     game.mouseOnCurrentHero()

     使用以下三個階段實現發射英雄:

  • wait-for-firing:遊戲畫面平移至彈弓,等待滑鼠單擊並拖動英雄,然後切換至firing階段
  • firing:遊戲隨著滑鼠移動英雄,直到滑鼠按鍵鬆開,此時以特定的速度將英雄拋射出去,並切換到fired階段。拋射的速度基於英雄與彈弓的距離。
  • fired:遊戲畫面跟隨英雄移動,直至英雄靜止下來或者飛出關卡邊界之外,然後遊戲將英雄從遊戲世界中移除,並切換至load-next-hero階段。

4.10 結束關卡

     顯示關卡結束畫面

4.11 碰撞損壞

     新增listener事件

4.12 繪製彈弓橡膠帶

     game.drawSlingshotBand()

4.13 切換關卡

     重新開始關卡和開始下一個關卡的按鈕

4.14 新增聲音

     首先,新增一些音效,如彈弓被釋放、英雄或壞蛋彈跳、障礙物被摧毀的音效。

     小技巧:在http://www.ccMixter.com中可以為自己的遊戲找到一些免費又優質的音樂

     設定斷裂和反彈的音效

      碰撞時播放彈跳聲:listener.PostSolve  

      別摧毀時播放破壞聲:  

      被髮射時播放彈弓釋放的聲音 

      新增背景音樂:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Froot Wars</title>
    <script src="js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/Box2dWeb-2.1.a.3.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/game.js" type="text/javascript" charset="utf-8"></script>
    <link rel="stylesheet" href="style.css" type="text/css" media="screen" charset="utf-8">
  </head>
  <body>
    <div id="gamecontainer">
      <canvas id="gamecanvas" width="640" height="480" class="gamelayer"></canvas>
      <div id="scorescreen" class="gamelayer">
        <img id="togglemusic" src="images/icons/sound.png" onclick="game.toggleBackgroundMusic()">
        <img src="images/icons/prev.png" onclick="game.restartLevel();">
        <span id="score">Score: 0</span>
      </div>
      <div id="gamestartscreen" class="gamelayer">
        <img src="images/icons/play.png" alt="Play Game" onclick="game.showLevelScreen();"><br>
        <img src="images/icons/settings.png" alt="Settings">
      </div>
      <div id="levelselectscreen" class="gamelayer">
      </div>
      <div id="loadingscreen" class="gamelayer">
        <div id="loadingmessage"></div>
      </div>
      <div id="endingscreen" class="gamelayer">
        <div>
          <p id="endingmessage"> The Level Is Over Message<p>
          <p id="playcurrentlevel" onclick="game.restartLevel();"><img src="images/icons/prev.png" >Replay Current Level</p>
          <p id="playnextlevel" onclick="game.startNextLevel();"><img src="images/icons/next.png"> Play Next Level </p>
          <p id="showLevelScreen" onclick="game.showLevelScreen();"><img src="images/icons/return.png"> Return to Level Screen</p>
        </div>
      </div>
    </div>
    <canvas id="debugcanvas" width="1000" height="480" style="border:1px solid block;"></canvas>
  </body>
</html>
style.css

#gamecontainer {
	width: 640px;
	height: 480px;
	background: url(images/splashscreen.png);
	border: 1px solid block;
}
.gamelayer {
	width: 640px;
	height: 480px;
	position: absolute;
	display: none;
}
/* 開始選單畫面 */
#gamestartscreen {
	padding-top:250px;
	text-align:center;
}
#gamestartscreen img{
	margin:10px;
	cursor:pointer;
}
/* 關卡選擇畫面 */
#levelselectscreen {
	padding-top: 150px;
	padding-left: 50px;
}
#levelselectscreen input {
	margin: 20px;
	cursor: pointer;
	background: url(images/icons/level.png) no-repeat;
	color: yellow;
	font-size: 20px;
	width: 64px;
	height: 64px;
	border: 0;
}
/* 載入畫面 */
#loadingscreen {
	background: rgba(100,100,100,0.3);
}
#loadingmessage {
	margin-top:400px;
	text-align:center;
	height: 48px;
	color:white;
	background:url(images/loader.gif) no-repeat center;
	font:12px Arial;
}
/* 計分板 */
#scorescreen {
	height: 60px;
	font: 32px Comic Sans MS;
	text-shadow: 0 0 2px #000;
	color: white;
}
#scorescreen img{
	opacity: 0.6;
	top: 10px;
	position: relative;
	padding-left: 10px;
	cursor: pointer;
}
#scorescreen #score {
	position: absolute;
	top: 5px;
	right: 20px;
}
/* 結束畫面 */
endingscreen {
	text-align: center;
}
#endingscreen div {
	height: 430px;
	padding-top: 50px;
	border: 1px;
	background: rgba(1,1,1,0.5);
	text-align: left;
	padding-left: 100px;
}
#endingscreen p {
	font: 20px Comic Sans MS;
	text-shadow: 0 0 2px #000;
	color: white;
}
#endingscreen p img {
	top: 10px;
	position: relative;
	cursor: pointer;
}
#endingscreen #endingmessage {
	font: 32px Comic Sans MS;
	text-shadow: 0 0 2px #000;
	color: white;
}

game.js

// Declare all the commonly used objects as variables for convenience
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;

// 建立requestAnimationFrame和cancelAnimationFrame以在遊戲程式碼中使用
(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for (var x=0; x<vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
           window[vendors[x] + 'CancelAnimationFrame'];
    }
    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16-(currTime-lastTime));
            var id = window.setTimeout(function() { 
                callback(currTime + timeToCall);},
                timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
    }
    if (!window.cancelAnimationFrame){
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
    }
}());
//
var game = {
    //開始初始化物件,預載入資源,並顯示開始畫面
    init: function(){
        // 初始化物件
        levels.init();
        loader.init();
        mouse.init();

        //載入所有的音效及背景音樂
        //由Gurdonark創作的"Kindergarten"
        game.backgroundMusic = loader.loadSound('audio/gurdonark-kindergarten');
        game.slingshotReleaseSound = loader.loadSound('audio/released');
        game.bounceSound = loader.loadSound('audio/bounce');
        game.breakSound = {
            "glass": loader.loadSound('audio/glassbreak'),
            "wood": loader.loadSound('audio/woodbreak')
        };

        //隱藏所有的遊戲圖層,顯示開始畫面
        $('.gamelayer').hide();
        $('#gamestartscreen').show();
        //獲取遊戲畫布及其繪圖環境的引用
        game.canvas = $('#gamecanvas')[0];
        game.context = game.canvas.getContext('2d');
    },
    showLevelScreen: function(){
        $('.gamelayer').hide();
        $('#levelselectscreen').show('slow');
    },
    //控制背景音樂的方法
    startBackgroundMusic: function() {
        var toggleImage = $("#togglemusic")[0];
        game.backgroundMusic.play();
        toggleImage.src = "images/icons/sound.png";
    },
    stopBackgroundMusic: function() {
        var toggleImage = $("#togglemusic")[0];
        toggleImage.src = "images/icons/nosound.png";
        game.backgroundMusic.pause();
        game.backgroundMusic.currentTime = 0;
    },
    toggleBackgroundMusic: function() {
        var toggleImage = $("#togglemusic")[0];
        if (game.backgroundMusic.paused) {
            game.backgroundMusic.play();
            toggleImage.src = "images/icons/sound.png";
        } else {
            toggleImage.src = "images/icons/nosound.png";
            game.backgroundMusic.pause();
        }
    },

    // 遊戲階段
    mode: "intro", //遊戲狀態(intro,waiting for firing, firing, fired)
    // 彈弓的x和y座標
    slingshotX: 140,
    slingshotY: 280,
    start: function() {
        //隱藏其他所有的圖層
        $('.gamelayer').hide();
        //顯示遊戲畫布和得分
        $('#gamecanvas').show();
        $('#scorescreen').show();

        game.startBackgroundMusic();

        game.mode = "intro";
        game.offsetLeft = 0; //記錄遊戲畫面在關卡中平移的距離
        game.ended = false;
        game.animationFrame = window.requestAnimationFrame(game.animate, game.canvas);
    },
    // 畫面最大平移速度,單位為畫素每幀
    maxSpeed: 3,
    // 畫面最大和最小平移範圍
    minOffset: 0,
    maxOffset: 300,
    // 畫面當前平移位置
    offsetLeft: 0,
    // 遊戲得分
    score: 0,
    // 畫面中心移動到newCenter
    panTo: function(newCenter) {
        if (Math.abs(newCenter - game.offsetLeft - game.canvas.width/4) > 0
            && game.offsetLeft <= game.maxOffset && game.offsetLeft >= game.minOffset) {
            var deltaX = Math.round((newCenter - game.offsetLeft - game.canvas.width/4)/2);
            if (deltaX && Math.abs(deltaX) > game.maxSpeed) {
                deltaX = game.maxSpeed*Math.abs(deltaX)/(deltaX);
            }
            game.offsetLeft += deltaX;
        } else {
            return true;
        }
        if (game.offsetLeft < game.minOffset) {
            game.offsetLeft = game.minOffset;
            return true;
        } else if (game.offsetLeft > game.maxOffset) {
            game.offsetLeft = game.maxOffset;
            return true;
        }
        return false;
    },
    countHeroesAndVillains:function(){
        game.heroes = [];
        game.villains = [];
        for (var body = box2d.world.GetBodyList(); body; body = body.GetNext()) {
            var entity = body.GetUserData();
            if(entity){
                if(entity.type == "hero"){              
                    game.heroes.push(body);         
                } else if (entity.type =="villain"){
                    game.villains.push(body);
                }
            }
        }
    },
    //滑鼠是否放置在當前英雄上
    mouseOnCurrentHero:function(){
        if(!game.currentHero){
            return false;
        }
        var position = game.currentHero.GetPosition();
        var distanceSquared = Math.pow(position.x*box2d.scale - mouse.x-game.offsetLeft,2) + Math.pow(position.y*box2d.scale-mouse.y,2);
        var radiusSquared = Math.pow(game.currentHero.GetUserData().radius,2);      
        return (distanceSquared<= radiusSquared);   
    },
    showEndingScreen: function(){
        game.stopBackgroundMusic();
        if (game.mode == "level-success"){
            if (game.currentLevel.number < levels.data.length-1){
                $('#endingmessage').html('Level Complete. Well Done!!!');
                $('#playnextlevel').show();
            } else {
                $('#endingmessage').html('All Levels Complete. Well Done!!!');
                $('#playnextlevel').show();
            }
        } else if (game.mode == "level-failure"){
            $('#endingmessage').html('Failed. Play Again?');
            $('#playnextlevel').show();
        } 
        $('#endingscreen').show();
    },
    handlePanning: function() {
        if (game.mode == "intro"){
            if (game.panTo(700)) {
                game.mode = "load-next-hero";
            }
        }
        if (game.mode == "load-next-hero"){
            game.countHeroesAndVillains();
            // 檢查是否有壞蛋還活著,如果沒有,結束關卡
            if (game.villains.length == 0){
                game.mode = "level-success";
                return;
            }
            // 檢查是否還有可裝填英雄,如果沒有,結束關卡
            if (game.heroes.length == 0){
                game.mode = "level-failure";
                return;
            }
            // 裝填英雄
            if (!game.currentHero){
                game.currentHero = game.heroes[game.heroes.length-1];
                game.currentHero.SetPosition({x:180/box2d.scale,y:200/box2d.scale});
                game.currentHero.SetLinearVelocity({x:0,y:0});
                game.currentHero.SetAngularVelocity(0);
                game.currentHero.SetAwake(true);    
            } else {
                //等待英雄結束彈跳並進入休眠
                game.panTo(game.slingshotX);
                if (!game.currentHero.IsAwake()){
                    game.mode = "wait-for-firing";
                }
            }
        }
        if (game.mode == "wait-for-firing"){
            if (mouse.dragging) {
                if (game.mouseOnCurrentHero()){
                    game.mode = "firing";
                } else {
                    game.panTo(mouse.x + game.offsetLeft);
                }
            } else {
                game.panTo(game.slingshotX);
            }
        }
        if (game.mode == "firing"){
            if (mouse.down) {
               game.panTo(game.slingshotX); 
               game.currentHero.SetPosition({x:(mouse.x + game.offsetLeft)/box2d.scale,
                 y:mouse.y/box2d.scale});
            } else {
                game.mode = "fired";
                game.slingshotReleaseSound.play();
                var impulseScaleFactor = 0.75;
                var impulse = new b2Vec2((game.slingshotX+35-mouse.x-game.offsetLeft)*
                    impulseScaleFactor, (game.slingshotY+25-mouse.y)*impulseScaleFactor);
                game.currentHero.ApplyImpulse(impulse, game.currentHero.GetWorldCenter());
            }
        }
        if (game.mode == "fired"){
            // 跟隨當前英雄移動畫面
            var heroX = game.currentHero.GetPosition().x*box2d.scale;
            game.panTo(heroX);
            //直到該英雄停止移動或移除邊界
            if (!game.currentHero.IsAwake() || heroX<0 
                || heroX>game.currentLevel.foregroundImage.width){
                //然後刪除舊的英雄
                box2d.world.DestroyBody(game.currentHero);
                game.currentHero = undefined;
                //載入下一個英雄
                game.mode = "load-next-hero";
            }
        }
        if (game.mode == "level-success" || game.mode == "level-failure"){
            if (game.panTo(0)) {
                game.ended = true;
                game.showEndingScreen();
            }
        }
    },
    animate: function() {
        //移動背景
        game.handlePanning();
        //使角色運動
        var currentTime = new Date().getTime();
        var timeStep;
        if (game.lastUpdateTime){
            timeStep = (currentTime - game.lastUpdateTime)/1000;
            box2d.step(timeStep);
        }
        game.lastUpdateTime = currentTime;

        //使用視差滾動繪製背景,背景影像和前景影像以不同的速度移動,
        //這個差異會造成一種錯覺:背景上的雲彩離我們更遠
        game.context.drawImage(game.currentLevel.backgroundImage, 
            game.offsetLeft/4, 0, 640, 480, 0, 0, 640, 480);
        game.context.drawImage(game.currentLevel.foregroundImage, 
            game.offsetLeft, 0, 640, 480, 0, 0, 640, 480);
        // 繪製彈弓
        game.context.drawImage(game.slingshotImage, game.slingshotX- 
            game.offsetLeft, game.slingshotY);
        // 繪製所有的物體
        game.drawAllBodies();
        //發射英雄時繪製橡膠帶
        if (game.mode == "firing"){
            game.drawSlingshotBand();
        }
        // 再次繪製彈弓的外側支架
        game.context.drawImage(game.slingshotFrontImage, game.slingshotX-
            game.offsetLeft, game.slingshotY);
        if (!game.ended) {
            game.animationFrame = window.requestAnimationFrame(game.animate, game.canvas);
        }
    },
    //繪製彈弓橡膠帶
    drawSlingshotBand: function() {
        game.context.strokeStyle = "rgb(68, 31, 11)"; //暗棕色
        game.context.lineWidth = 6; //
        //用英雄被拖拽的角度和半徑計算英雄的末端,相對於英雄的中心
        var radius = game.currentHero.GetUserData().radius;
        var heroX = game.currentHero.GetPosition().x * box2d.scale;
        var heroY = game.currentHero.GetPosition().y * box2d.scale;
        var angle = Math.atan2(game.slingshotY+25-heroY, game.slingshotX+50-heroX);

        var heroFarEdgeX = heroX - radius * Math.cos(angle);
        var heroFarEdgeY = heroY - radius * Math.cos(angle);
        game.context.beginPath();
        //從彈弓頂端開始繪製(背面)
        game.context.moveTo(game.slingshotX+50-game.offsetLeft, game.slingshotY+25);
        //畫到英雄的中心
        game.context.lineTo(heroX-game.offsetLeft, heroY);
        game.context.stroke();
        //再次繪製英雄
        entities.draw(game.currentHero.GetUserData(), game.currentHero.GetPosition(),
            game.currentHero.GetAngle());
        game.context.beginPath();
        //移動到英雄離彈弓頂部最遠的邊緣
        game.context.moveTo(heroFarEdgeX-game.offsetLeft, heroFarEdgeY+25);
        //將線畫回彈弓(正面)
        game.context.lineTo(game.slingshotX-game.offsetLeft+10,game.slingshotY+30);
        game.context.stroke();
    },
    //重新開始和下一關
    restartLevel: function() {
        window.cancelAnimationFrame(game.animationFrame);
        game.lastUpdateTime = undefined;
        levels.load(game.currentLevel.number);
    },
    startNextLevel: function() {
        window.cancelAnimationFrame(game.animationFrame);
        game.lastUpdateTime = undefined;
        levels.load(game.currentLevel.number+1);
    },
    drawAllBodies: function() {
        box2d.world.DrawDebugData();
        // 遍歷所有的物體,並在遊戲canvas上繪製出來
        for (var body=box2d.world.GetBodyList(); body; body=body.GetNext()){
            var entity = body.GetUserData();
            if (entity){
                //判斷生命值,是否要顯示
                var entityX = body.GetPosition().x * box2d.scale;
                if (entityX<0 || entityX>game.currentLevel.foregroundImage.width ||
                    (entity.health && entity.health<0)) {
                    box2d.world.DestroyBody(body);
                    if (entity.type == "villain"){
                        game.score += entity.calories;
                        $('#score').html('Score: ' + game.score);
                    }
                    if (entity.breakSound){
                        entity.breakSound.play();
                    }
                } else {
                    entities.draw(entity, body.GetPosition(), body.GetAngle());
                }
            }
        }
    },
}
$(window).load(function() {
    game.init();
});
// 關卡
var levels = {
    //關卡資料
    data: [
    {   //第一關
        foreground: 'desert-foreground',
        background: 'clouds-background',
        entities:[
            {type:"ground", name:"dirt", x:500,y:440,width:1000,height:20,isStatic:true},
            {type:"ground", name:"wood", x:185,y:390,width:30,height:80,isStatic:true},

            {type:"block", name:"wood", x:520,y:380,angle:90,width:100,height:25},
            {type:"block", name:"glass", x:520,y:280,angle:90,width:100,height:25},                             
            {type:"villain", name:"burger",x:520,y:205,calories:590},

            {type:"block", name:"wood", x:620,y:380,angle:90,width:100,height:25},
            {type:"block", name:"glass", x:620,y:280,angle:90,width:100,height:25},                             
            {type:"villain", name:"fries", x:620,y:205,calories:420},               

            {type:"hero", name:"orange",x:80,y:405},
            {type:"hero", name:"apple",x:140,y:405},
        ]
    },
    {   //第二關
        foreground: 'desert-foreground',
        background: 'clouds-background',
        entities:[
                {type:"ground", name:"dirt", x:500,y:440,width:1000,height:20,isStatic:true},
                {type:"ground", name:"wood", x:185,y:390,width:30,height:80,isStatic:true},
    
                {type:"block", name:"wood", x:820,y:380,angle:90,width:100,height:25},
                {type:"block", name:"wood", x:720,y:380,angle:90,width:100,height:25},
                {type:"block", name:"wood", x:620,y:380,angle:90,width:100,height:25},
                {type:"block", name:"glass", x:670,y:317.5,width:100,height:25},
                {type:"block", name:"glass", x:770,y:317.5,width:100,height:25},                

                {type:"block", name:"glass", x:670,y:255,angle:90,width:100,height:25},
                {type:"block", name:"glass", x:770,y:255,angle:90,width:100,height:25},
                {type:"block", name:"wood", x:720,y:192.5,width:100,height:25}, 

                {type:"villain", name:"burger",x:715,y:155,calories:590},
                {type:"villain", name:"fries",x:670,y:405,calories:420},
                {type:"villain", name:"sodacan",x:765,y:400,calories:150},

                {type:"hero", name:"strawberry",x:30,y:415},
                {type:"hero", name:"orange",x:80,y:405},
                {type:"hero", name:"apple",x:140,y:405},
        ]
    }
    ],
    // 初始化關卡選擇畫面
    init: function() {
        var html = "";
        for (var i=0; i<levels.data.length; i++) {
            var level = levels.data[i];
            html += '<input type="button" value="' + (i+1) + '">';
        };
        $('#levelselectscreen').html(html);
        //單擊按鈕時載入關卡
        $('#levelselectscreen input').click(function(){
            levels.load(this.value - 1);
            $('#levelselectscreen').hide();
        });
    },
    // 為某一關載入所有的資料和影像
    load: function(number){
        box2d.init();//
        //宣告一個新的當前關卡物件
        game.currentLevel = {number:number, hero:[]};
        game.score = 0;
        $('#score').html('Score: ' + game.score);
        game.currentHero = undefined;
        var level = levels.data[number];
        //載入背景、前景和彈弓影像
        game.currentLevel.backgroundImage = loader.loadImage("images/backgrounds/" + level.background + ".png");
        game.currentLevel.foregroundImage = loader.loadImage("images/backgrounds/" + level.foreground + ".png");
        game.slingshotImage = loader.loadImage("images/slingshot.png");
        game.slingshotFrontImage = loader.loadImage("images/slingshot-front.png");
        // 載入所有的物體
        for (var i=level.entities.length-1; i>=0; i--){
            var entity = level.entities[i];
            entities.create(entity);
        };
        // 一旦所有的影像載入完成,就呼叫game.start()函式
        if (loader.loaded){
            game.start();
        } else {
            loader.onload = game.start;
        }
    }
}
//
var entities = {
    //定義物體型別(玻璃、木材和地面),以及英雄和壞蛋(橙子、蘋果和漢堡)
    definitions:{
        "glass":{
            fullHealth:100,
            density:2.4,
            friction:0.4,
            restitution:0.15,
        },
        "wood":{
            fullHealth:500,
            density:0.7,
            friction:0.4,
            restitution:0.4,
        },
        "dirt":{
            density:3.0,
            friction:1.5,
            restitution:0.2,    
        },
        "burger":{
            shape:"circle",
            fullHealth:40,
            radius:25,
            density:1,
            friction:0.5,
            restitution:0.4,    
        },
        "sodacan":{
            shape:"rectangle",
            fullHealth:80,
            width:40,
            height:60,
            density:1,
            friction:0.5,
            restitution:0.7,    
        },
        "fries":{
            shape:"rectangle",
            fullHealth:50,
            width:40,
            height:50,
            density:1,
            friction:0.5,
            restitution:0.6,    
        },
        "apple":{
            shape:"circle",
            radius:25,
            density:1.5,
            friction:0.5,
            restitution:0.4,    
        },
        "orange":{
            shape:"circle",
            radius:25,
            density:1.5,
            friction:0.5,
            restitution:0.4,    
        },
        "strawberry":{
            shape:"circle",
            radius:15,
            density:2.0,
            friction:0.5,
            restitution:0.4,    
        },
    },
       //根據物體建立Box2D物體,並將其加入世界
    create:function(entity){
        var definition = entities.definitions[entity.name];
        if (!definition){
            console.log("undefined entity name", entity.name);
            return;
        }
        //console.log(entity);
        switch (entity.type){
            case "block":  //簡單的矩形
              entity.health = definition.fullHealth;
              entity.fullHealth = definition.fullHealth;
              entity.shape = "rectangle";
              entity.sprite = loader.loadImage("images/entities/"
                +entity.name+".png");
              entity.breakSound = game.breakSound[entity.name];
              box2d.createRectangle(entity, definition);
              break;
            case "ground":  //簡單的矩形
              entity.shape = "rectangle";
              box2d.createRectangle(entity, definition);
              break;
            case "hero":
            case "villain":
              entity.health = definition.fullHealth;
              entity.fullHealth = definition.fullHealth;
              entity.sprite = loader.loadImage("images/entities/"
                +entity.name+".png");
              entity.shape = definition.shape;
              entity.bounceSound = game.bounceSound;
              if (definition.shape == "circle"){
                entity.radius = definition.radius;
                box2d.createCircle(entity, definition);
              } else if (definition.shape == "rectangle") {
                entity.width = definition.width;
                entity.height = definition.height;
                box2d.createRectangle(entity, definition);
              }
              break;
            default:
              console.log("Undefined entity type", entity.type);
              break;
        }
    },
    //以物體、物體的位置和角度為引數,在遊戲畫面中繪製物體
    draw: function(entity, position, angle){
        game.context.translate(position.x*box2d.scale-game.offsetLeft,
            position.y*box2d.scale);
        game.context.rotate(angle);
        switch(entity.type){
            case "block":
              game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
                 entity.sprite.height, -entity.width/2-1, -entity.height/2-1,
                 entity.width+2, entity.height+2);
              break;
            case "villain":
            case "hero":
              if (entity.shape == "circle"){
                game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
                 entity.sprite.height, -entity.radius-1, -entity.radius-1,
                 entity.radius*2+2, entity.radius*2+2);
              } else if (entity.shape == "rectangle") {
                game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
                 entity.sprite.height, -entity.width/2-1, -entity.height/2-1,
                 entity.width+2, entity.height+2);
              }
              break;
            case "ground":
              break;
        }
        game.context.rotate(-angle);
        game.context.translate(-position.x*box2d.scale + game.offsetLeft, -position.y*box2d.scale);
    },
}
//建立box2d物件
var box2d = {
    scale: 30,
    init: function() {
        //建立Box2D世界,大部分物理運算將在其中完成
        var gravity = new b2Vec2(0, 9.8); //重力加速度9.8m/s^2
        var allowSleep = true;  //允許靜止的物體進入休眠狀態,休眠物體不參與物理模擬計算
        box2d.world = new b2World(gravity, allowSleep);
        //設定除錯繪圖
        var debugContext = document.getElementById('debugcanvas').getContext('2d');
        var debugDraw = new b2DebugDraw();
        debugDraw.SetSprite(debugContext);
        debugDraw.SetDrawScale(box2d.scale);
        debugDraw.SetFillAlpha(0.3);
        debugDraw.SetLineThickness(1.0);
        debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);    
        box2d.world.SetDebugDraw(debugDraw);
        //監聽事件
        var listener = new Box2D.Dynamics.b2ContactListener;
        //引數為接觸和衝擊力(法向和切向衝擊力)
        listener.PostSolve = function(contact, impulse){
            var body1 = contact.GetFixtureA().GetBody();
            var body2 = contact.GetFixtureB().GetBody();
            var entity1 = body1.GetUserData();
            var entity2 = body2.GetUserData();
            var impulseAlongNormal = Math.abs(impulse.normalImpulses[0]);
            // 監聽器被呼叫得有些太頻繁了,濾去非常小的衝擊
            // 嘗試不同的值後,5似乎比較好
            if (impulseAlongNormal > 5) {
                //如果物件有生命值,用衝擊值消弱生命值
                if (entity1.health) {
                    entity1.health -= impulseAlongNormal;
                }
                if (entity2.health) {
                    entity2.health -= impulseAlongNormal;
                }
                //如果物體具有彈跳音,則播放它
                if (entity1.bounceSound){
                    entity1.bounceSound.play();
                }
                if (entity2.bounceSound){
                    entity2.bounceSound.play();
                }
            }
        };
        box2d.world.SetContactListener(listener);
    },
    step: function(timeStep){
        if (timeStep > 2/60){
            timeStep = 2/60;
        }
        box2d.world.Step(timeStep, 8, 3);
    },
    createRectangle:function(entity, definition){
        var bodyDef = new b2BodyDef;
        if (entity.isStatic) {
            bodyDef.type = b2Body.b2_staticBody;
        } else {
            bodyDef.type = b2Body.b2_dynamicBody;
        }
        bodyDef.position.x = entity.x/box2d.scale;
        bodyDef.position.y = entity.y/box2d.scale;
        if (entity.angle){
            bodyDef.angle = Math.PI*entity.angle/180;
        }
        var fixtureDef = new b2FixtureDef;
        fixtureDef.density = definition.density;
        fixtureDef.friction = definition.friction;
        fixtureDef.restitution = definition.restitution;
        fixtureDef.shape = new b2PolygonShape;  //多邊形
        fixtureDef.shape.SetAsBox(entity.width/2/box2d.scale, 
           entity.height/2/box2d.scale); //60寬,100高

        var body = box2d.world.CreateBody(bodyDef);
        body.SetUserData(entity);
        var fixture = body.CreateFixture(fixtureDef);
        return body;
    },
    createCircle:function(entity, definition){
        var bodyDef = new b2BodyDef;
        if (entity.isStatic) {
            bodyDef.type = b2Body.b2_staticBody;
        } else {
            bodyDef.type = b2Body.b2_dynamicBody;
        }
        bodyDef.position.x = entity.x/box2d.scale;
        bodyDef.position.y = entity.y/box2d.scale;
        if (entity.angle){
            bodyDef.angle = Math.PI*entity.angle/180;
        }
        var fixtureDef = new b2FixtureDef;
        fixtureDef.density = definition.density;
        fixtureDef.friction = definition.friction;
        fixtureDef.restitution = definition.restitution;
        fixtureDef.shape = new b2CircleShape(entity.radius/box2d.scale);

        var body = box2d.world.CreateBody(bodyDef);
        body.SetUserData(entity);
        var fixture = body.CreateFixture(fixtureDef);
        return body;
    },
}

//影像/聲音資源載入器loader
var loader = {
    loaded: true,
    loadedCount: 0, //已載入的資源數
    totalCount: 0,  //需要被載入的資源總數

    init: function(){
        //檢查瀏覽器支援的聲音格式;
        var mp3Support, oggSupport;
        var audio = document.createElement('audio');
        if (audio.canPlayType){
            // 
            mp3Support = "" != audio.canPlayType('audio/mpeg');
            oggSupport = "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
        } else {
            // audio標籤不被支援
            mp3Support = false;
            oggSupport = false;
        }
        // 都不支援,就將soundFileExtn設定為undefined
        loader.soundFileExtn = oggSupport?".ogg":mp3Support?".mp3":undefined;
        console.log(loader.soundFileExtn);
    },
    loadImage: function(url){
        this.totalCount++;
        this.loaded = false;
        $('#loadingscreen').show();
        var image = new Image();
        image.src = url;
        image.onload = loader.itemLoaded;
        return image;
    },
    soundFileExtn: ".ogg",
    loadSound: function(url){
        this.totalCount++;
        this.loaded = false;
        $('#loadingscreen').show();
        var audio = new Audio();
        audio.src = url + loader.soundFileExtn;
        audio.addEventListener("canplaythrough", loader.itemLoaded, false);
        return audio;
    },
    itemLoaded: function() {
        loader.loadedCount++;
        $('#loadingmessage').html('Loaded ' + loader.loadedCount + ' of ' + loader.totalCount);
        if (loader.loadedCount === loader.totalCount) {
            // loader完成了資源載入
            loader.loaded = true;
            $('#loadingscreen').hide();
            if (loader.onload){
                loader.onload();
                loader.onload = undefined;
            }
        }
    }
}
//處理滑鼠事件
var mouse = {
    x: 0,
    y: 0,
    down: false,
    init: function() {
        $('#gamecanvas').mousemove(mouse.mousemovehandler);
        $('#gamecanvas').mousedown(mouse.mousedownhandler);
        $('#gamecanvas').mouseup(mouse.mouseuphandler);
        $('#gamecanvas').mouseout(mouse.mouseuphandler);
    },
    mousemovehandler: function(ev) {
        var offset = $('#gamecanvas').offset();
        mouse.x = ev.pageX - offset.left;
        mouse.y = ev.pageY - offset.top;
        if (mouse.down) {
            mouse.dragging = true;
        }
    },
    mousedownhandler: function(ev) {
        mouse.down = true;
        mouse.downX = mouse.x;
        mouse.downY = mouse.y;
        ev.originalEvent.preventDefault();
    },
    mouseuphandler: function(ev) {
        mouse.down = false;
        mouse.dragging = false;
    }
}


相關文章