HTML5 2D平台游戲開發#1


  在Web領域通常會用到一組sprite來展示動畫,這類動畫從開始到結束往往不會有用戶參與,即用戶很少會用控制器(例如鼠標、鍵盤、手柄、操作桿等輸入設備)進行操作。但在游戲領域,sprite動畫與控制器的操作是密不可分的。最近在寫一個小游戲,涉及到很多知識點,於是打算把這些內容通過一些Demo總結出來備忘。

  這是第一階段的運行效果,用鍵盤A、D來控制人物左右移動,空格/K控制人物跳躍,U鍵沖刺:

 

 

動畫幀播放器

  要生成一組動畫,首先需要一個能夠播放各個動畫幀的方法。新建一個構造函數Animation:

/**
 *@param frames {Array} 元數據  
 *@param options {Object} 可選配置
 */
function Animation(frames,options) {
    this.frames = frames || [{ x: 0, y: 0, w: 0, h: 0, duration: 0 }];
    this.options = options || {
        repeats:false,    //是否重復播放
        startFrame:0    //起始播放的位置
    };
}

   說明一下上面的代碼,函數有兩個參數,其中frames為元數據(metaData),用於標識一組sprite的坐標信息,為何需要這個數據呢,先來看一張圖:

  可以發現每一幀的sprite大小都不一致,特別是第二排,並不是規則的sprite,因此需要將各幀的位置大小等信息標識出來。例如這是第一排的元數據:

        //人物站立時的幀動畫
        var idle = [
            {x:0,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:2000},
            {x:40,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:80,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:120,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:160,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:200,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:240,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:280,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120},
            {x:320,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}
        ];    

其中x,y代表所使用sprite的位置,w,h表示該sprite的寬高,offset用於修正sprite的位置,duration表示該sprite持續的時間。

題外話:如果手工處理這些sprite信息是相當繁瑣的,有一款軟件叫做TexturePacker專門用來生成sprite sheets。

  接着新建一個AnimationPlayer來管理Animation

//@param animation {Object} Animation的實例
function AnimationPlayer(animation) {
    var ani = animation || new Animation();
    this.length = 0; //標記該組sprite中一共有幾個動作
    //當前組的sprite中正在執行的動作,例如idle[1]表示正在進行idle組中的第二個動畫幀
    this.frame = undefined;    
    this.index = 0;
    this.elapsed = 0;    //標記每幀的運行時間

    this.setAnimation(ani);
    this.reset();
}
//重置動畫
AnimationPlayer.prototype.reset = function() {
    this.elapsed = 0;
    this.index = 0;
    this.frame = this.animation.frames[this.index];
};

AnimationPlayer.prototype.setAnimation = function(animation) {
    this.animation = animation;
    this.length = this.animation.frames.length;
};

AnimationPlayer.prototype.update = function(dt) {
    this.elapsed += dt;

    if (this.elapsed >= this.frame.duration) {
        this.index++;
        this.elapsed -= this.frame.duration;
    }

    if (this.index >= this.length) {
        if (this.animation.options.repeats) this.index = this.animation.options.startFrame;
        else this.index--;
    }

    this.frame = this.animation.frames[this.index];
};

 最后在使用的時候將其實例化:

//站立
var animation = new Animation(idle, {
    repeats: true,
    startFrame: 0
});
var playerIdle = new AnimationPlayer(animation);

//移動
var animation2 = new Animation(move, {
    repeats: true,
    startFrame: 0
});
var playerMove = new AnimationPlayer(animation2);

 

游戲循環

  游戲運行的機制就是在每一次GameLoop中更新所有游戲元件的狀態,例如更新元件的位置,碰撞檢測,銷毀元件等等。大體來說代碼一般都具有以下結構:

(function render() {
    //清除畫布
    context.clearRect(0,0,canvas.width,canvas.height);

    //執行游戲邏輯
    //將更新狀態后的元件重新繪制到畫布上

    requestAnimationFrame(render);    //進入下一次游戲循環
})();

  在本Demo的GameLoop中主要執行的邏輯有:

  • 計算本次GameLoop與上次間隔的時間

    基於時間的運動(time-base)是保證游戲運行良好的關鍵,假設有兩台設備,一台每1秒執行一次游戲循環,另一台每2秒執行一次,並且物體以每次5px的速度移動,那么在2秒后第一台設備中的物體移動了2X5=10px,第二台設備中的物體移動了1X5=5px。很顯然,經過相同的時間但最終物體達到了不同的位置,這是不合理的。如果采用基於時間的運動,則通過公式s += vt可以發現,第一台設備在經過兩秒后移動的距離為5X1+5X1=10px,第二台設備移動的距離為5X2=10px,於是兩台設備達到了一致的效果。更新后的render方法代碼如下:

var lastAnimationFrameTime = 0,
    elapsed = 0,
    now;

(function render() {
    //清除畫布
    context.clearRect(0,0,canvas.width,canvas.height);

    now = +new Date;

    if (lastAnimationFrameTime !== 0) {
        elapsed = Math.min(now - lastAnimationFrameTime, 16);
    }
    lastAnimationFrameTime = now;

    //執行游戲邏輯
    //將更新狀態后的元件重新繪制到畫布上

    requestAnimationFrame(render);    //進入下一次游戲循環
})();
  • 檢測輸入並繪制元件
if (key[65]) { //按下A鍵
    playerState = 'move';
    direction = 0;
    x -= moveSpeed;
} else if (key[68]) { //按下D鍵
    playerState = 'move';
    direction = 1;
    x += moveSpeed;
} else {
    playerState = 'idle';
}

currentMotion = motion[playerState];
currentMotion.update(elapsed);

if (direction === 1) {
    ctx.drawImage(img, currentMotion.frame.x + currentMotion.frame.offsetX, currentMotion.frame.y, currentMotion.frame.w, currentMotion.frame.h, x, 300, currentMotion.frame.w * 1.5, currentMotion.frame.h * 1.5);
} else {
    //圖片翻轉,如有需要可以復習以前總結的知識點
    /*http://www.cnblogs.com/undefined000/p/flip-an-image-with-the-html5-canvas.html*/
    ctx.save();
    ctx.scale( - 1, 1);
    ctx.drawImage(img, currentMotion.frame.x + currentMotion.frame.offsetX, currentMotion.frame.y, currentMotion.frame.w, currentMotion.frame.h, -currentMotion.frame.w * 1.5 + currentMotion.frame.offsetX - x, 300, currentMotion.frame.w * 1.5, currentMotion.frame.h * 1.5);
    ctx.restore();
}

 

游戲暫停

  如果在游戲運行期間窗口失去焦點,則應當暫停游戲,因為此時瀏覽器會以低幀率運行游戲以節省開銷,這樣導致的結果就是當玩家返回窗口時,deltaTime會有爆炸性的增長,從而使元件更新異常。最常見的是一些碰撞檢測不能正常工作或者游戲人物高速移動。因此當窗口失去焦點時,應當暫停游戲。在主流瀏覽器中,可以用下面的代碼標識暫停:

document.addEventListener('visibilitychange',function() {
    if (document.visibilityState === 'hidden') {
        paused = true;
        console.log('游戲暫停中');
    } else {
        paused = false;
        console.log('游戲運行中');
    }
});

同時更新render方法:

(function render() {
    //省略部分代碼以節省篇幅
    if (paused) {
      setTimeout(function() {
         requestAnimationFrame(draw);
      },200);
    } else {
        //執行游戲邏輯
        requestAnimationFrame(render);    //進入下一次游戲循環
    }
})();

 

Summary

  以上就是這個Demo的主要知識點,暫時先總結到這,后面有時間還會陸續更新。

 

更新日志

Demo

  2017/4/9  更新角色跳躍

  2017/4/21  更新角色沖刺

  2017/5/1  更新角色狀態機

  2017/5/16    更新角色攻擊動畫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM