HTML5遊戲開發進階 2 :建立基本的遊戲世界

CopperDong發表於2018-02-09

https://github.com/apress/pro-html5-games

    需要用到的所有關鍵元件---啟動畫面、載入畫面、預載入器、主選單、視差滾動、聲音、基於Box2D引擎的物理模擬、記分牌。有了這樣一個基本框架,你就能夠在自己的遊戲中反覆使用它們。

2.1 基本HTML佈局

   包括以下圖層:

  • 啟動畫面:頁面載入時顯示。
  • 遊戲開始畫面:包含主選單,允許玩家開始遊戲或進行遊戲設定。
  • 載入/程式畫面:包含載入進度條,當遊戲正在載入資源(如影像和聲音檔案)時顯示。
  • 主畫面:實際的遊戲畫面。
  • 計分板:主畫面頂部的小塊區域,顯示若干個按鈕和計分情況。
  • 結束畫面:每一關結束時的畫面。

2.2 建立啟動畫面和主選單

    基本框架(index.html)及新增的圖層,定義了id為gamecontainer的div元素作為遊戲容器。遊戲容器包含了每一個遊戲圖層(gamelayer class):gamestartscreen(開始選單)、levelselectscreen(關卡選擇介面)、loadingscreen(載入畫面)、計分板(scorescreen)、結束畫面(endingscreen)和主畫面(gamecanvas)。

<!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/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">
        <img src="images/icons/prev.png">
        <span id="score">Score: 0</span>
      </div>
      <div id="gamestartscreen" class="gamelayer">
        <img src="images/icons/play.png" alt="Play Game"><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"><img src="images/icons/prev.png" >Replay Current Level</p>
          <p id="playnextlevel"><img src="images/icons/next.png"> Play Next Level </p>
          <p id="showLevelScreen"><img src="images/icons/return.png"> Return to Level Screen</p>
        </div>
      </div>
    </div>
  </body>
</html>

    style.css,樣式表:

  • 定義遊戲容器以及其中所有的遊戲圖層的尺寸為640px X 480px
  • 保證所有的遊戲圖層以絕對位置佈局(它們互相重疊),這樣就可以根據需要顯示/隱藏和重疊這些圖層。預設情況下,這些圖層都是隱藏。
  • 將遊戲容器的背景設定為啟動畫面影像,當頁面載入時,玩家第一眼看到的就是該影像。
  • 為遊戲開始畫面新增一些樣式,開始畫面中有“開始遊戲”、“更改設定”等選單項。

#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;
}
   js/game.js

var game = {
    //開始初始化物件,預載入資源,並顯示開始畫面
    init: function(){
        //隱藏所有的遊戲圖層,顯示開始畫面
        $('.gameplayer').hide();
        $('#gamestartscreen').show();
        //獲取遊戲畫布及其繪圖環境的引用
        game.canvas = $('#gamecanvas')[0];
        game.context = game.canvas.getContext('2d');
    },
}

$(window).load(function() {
    game.init();
});

2.3 關卡選擇

   當玩家單擊PLAY按鈕後,遊戲應當顯示一個關卡選擇畫面和一系列可玩的關卡。

   建立一個物件來處理關卡。這個物件既包含了關卡中的資料,又提供了一些簡單的函式來對關卡進行初始化。

2.4 載入影像

   完成關卡之前,需要實現圖形載入器和載入畫面。

   設計一個簡單的載入畫面,它包含一張進度條GIF動畫圖片和顯示已載入影像數目的文字。

2.5 載入關卡

   首先載入遊戲的背景、前景和彈弓影像

/* 關卡選擇畫面 */
#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;
}

2.6 動畫

   使用requestAnimationFrame,並在一秒內多次呼叫繪圖和動畫程式碼

   視差滾動是一種利用背景影像移動得比前景影像慢,而產生立體錯覺的技術。這項技術說明了一個事實,即遠處的物體看上去比近處的移動得快。

2.7 處理滑鼠輸入

    利用JavaScript中的幾個事件來捕捉滑鼠輸入---mousedown, mouseup和mousemove。為了簡單,我們利用jQuery建立一個獨立的mouse物件,以處理所有的滑鼠事件

     mouse物件的init()方法為滑鼠移動、滑鼠按下、滑鼠鬆開和滑鼠離開畫布區域等事件繫結了響應方法

2.8 設定遊戲階段

     將遊戲的當前階段儲存到game.mode變數中:

  • intro:關卡剛剛載入,遊戲將在整個關卡範圍中平移遊戲畫面,向玩家展現關卡中的所有東西
  • load-next-hero:檢查是否有下一個英雄可以裝填到彈弓上去,如果有,裝填該英雄。如果我們的英雄耗盡了而壞蛋卻沒有被全部消滅,關卡就結束了。
  • wait-for-firing:將視野移回到彈弓,等待使用者發射“英雄”。此時,遊戲正在等待使用者單擊英雄。在這個階段,使用者可以也很有可能用滑鼠拖拽畫面,檢視整個關卡。
  • firing:這個階段中,使用者已經單擊了英雄,但還沒有釋放滑鼠按鍵。此時,遊戲正在等待使用者拖拽英雄,調整角度和位置並釋放英雄。
  • fired:使用者釋放了滑鼠按鍵併發射英雄之後進入這個階段。此時,遊戲將所有的事情交給物理引擎來處理,使用者僅僅在觀看。遊戲畫面會隨著發射出的英雄平移,這樣使用者就可以追蹤英雄的軌跡。
js/game.js

// 建立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();

        //隱藏所有的遊戲圖層,顯示開始畫面
        $('.gamelayer').hide();
        $('#gamestartscreen').show();
        //獲取遊戲畫布及其繪圖環境的引用
        game.canvas = $('#gamecanvas')[0];
        game.context = game.canvas.getContext('2d');
    },
    showLevelScreen: function(){
        $('.gamelayer').hide();
        $('#levelselectscreen').show('slow');
    },
    // 遊戲階段
    mode: "intro", //遊戲狀態(intro,waiting for firing, firing, fired)
    // 彈弓的x和y座標
    slingshotX: 140,
    slingshotY: 280,
    start: function() {
        //隱藏其他所有的圖層
        $('.gamelayer').hide();
        //顯示遊戲畫布和得分
        $('#gamecanvas').show();
        $('#scorescreen').show();
        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;
    },
    handlePanning: function() {
        if (game.mode == "intro"){
            if (game.panTo(700)) {
                game.mode = "load-next-hero";
            }
        }
        if (game.mode == "wait-for-firing"){
            if (mouse.dragging) {
                game.panTo(mouse.x + game.offsetLeft);
            } else {
                game.panTo(game.slingshotX);
            }
        }
        if (game.mode == "load-next-hero"){
            // 待完成
            // 檢查是否有壞蛋還活著,如果沒有,結束關卡
            // 檢查是否還有可裝填英雄,如果沒有,結束關卡
            // 裝填英雄
            game.mode = "wait-for-firing";
        }
        if (game.mode == "firing"){
            game.panTo(game.slingshotX);
        }
        if (game.mode == "fired"){
            // 待完成
            // 視野移動到英雄的當前位置
        }
    },
    animate: function() {
        //移動背景
        game.handlePanning();
        //使角色運動

        //使用視差滾動繪製背景,背景影像和前景影像以不同的速度移動,
        //這個差異會造成一種錯覺:背景上的雲彩離我們更遠
        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.context.drawImage(game.slingshotFrontImage, game.slingshotX-
            game.offsetLeft, game.slingshotY);
        if (!game.ended) {
            game.animationFrame = window.requestAnimationFrame(game.animate, game.canvas);
        }
    }
}
$(window).load(function() {
    game.init();
});
// 關卡
var levels = {
    //關卡資料
    data: [
    {   //第一關
        foreground: 'desert-foreground',
        background: 'clouds-background',
        entities:[]
    },
    {   //第二關
        foreground: 'desert-foreground',
        background: 'clouds-background',
        entities:[]
    }
    ],
    // 初始化關卡選擇畫面
    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){
        //宣告一個新的當前關卡物件
        game.currentLevel = {number:number, hero:[]};
        game.score = 0;
        $('#score').html('Score: ' + game.score);
        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");
        // 一旦所有的影像載入完成,就呼叫game.start()函式
        if (loader.loaded){
            game.start();
        } else {
            loader.onload = game.start;
        }
    }
}
//影像/聲音資源載入器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;
    }
}

相關文章