在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的主要知識點,暫時先總結到這,后面有時間還會陸續更新。
更新日志
2017/4/9 更新角色跳躍
2017/4/21 更新角色沖刺
2017/5/1 更新角色狀態機
2017/5/16 更新角色攻擊動畫