做者:Fernando Dogliojavascript
翻譯:瘋狂的技術宅css
原文:blog.bitsrc.io/writing-a-t…html
未經容許嚴禁前端
遊戲開發並不須要侷限於使用 Unity 或 Unreal Engine4 的用戶。 JavaScript 遊戲開發已經有一段時間了。實際上,最流行的瀏覽器(例如Chrome,Firefox和Edge)的最新版本提供了對高級圖形渲染(例如WebGL)的支持,從而帶來了很是有趣的遊戲開發機會。java
不過用 WebGL 進行遊戲開發沒有辦法在一篇文章中涵蓋其全部內容(有專門爲此編寫的完整書籍),而且出於我的喜愛,在深刻研究特定技術以前,我更傾向於依賴框架的幫助。node
這就是爲何通過研究後,我決定用 MelonJS 編寫此快速教程的緣由。react
你可能已經猜到了,MelonJS 是一個 JavaScript 遊戲引擎,與全部主流瀏覽器徹底兼容(從 Chrome 到 Opera,一直到移動版 Chrome 和 iOS Safari)。jquery
它具備一系列功能,在個人研究過程當中很是引人注目:git
該引擎還有其餘使人讚歎的功能,你能夠在其網站上進行查看,不過以上是本文中咱們最關注的功能。github
**提示:**使用 Bit(Github)能夠輕鬆共享和重用 JS 模塊,項目中的 UI 組件,建議更新。
Bit 組件:可以輕鬆地在團隊中跨項目共享
打字遊戲的目的是經過打字(或敲擊隨機鍵)爲玩家提供移動或執行某種動做的能力。
我記得小時候曾經學過如何打字(是的,好久之前)了,當時在「Mario Teaches Typing」 這個遊戲中,必須鍵入單個字母才能前進,要麼跳到烏龜上,要麼從下面打一個方塊。下圖爲你提供了遊戲外觀以及怎樣與之進行互動的想法。
儘管這是一個有趣的小遊戲,但它並非一個真正的平臺遊戲,Mario 所執行的動做始終對應一個按鍵,而且永遠不會失效。
不過,對於本文,我想讓事情變得更有趣,並非建立一個簡單的打字遊戲,例如上面的遊戲:
遊戲不會經過單個字母來決定下一步的行動,而是提供了五個選擇,而且每一個選擇都必須寫一個完整的單詞:
換句話說,你能夠經過輸入的單詞來移動角色,而不是經典的基於箭頭進行控制。
除此以外,該遊戲將是一個經典平臺遊戲,玩家能夠經過走動收集金幣。爲了簡潔起見,咱們會將敵人和其餘類型的實體排除在本教程以外(儘管你應該可以推斷出所使用的代碼,並能基於該代碼建立本身的實體)。
爲了使本文保持合理的長度,我將只關注一個階段,全方位的動做(換句話說,你將可以執行全部 5 個動做)、幾個敵人、一種收藏品,還有數量可觀的臺階供你跳來跳去。
儘管 melonJS 是徹底獨立的,但在此過程當中有一些工具能夠幫助你們,我建議你使用它們:
使用這些工具,你將能夠繼續學習並完成本教程,因此讓咱們開始編碼吧。
爲了開始這個項目,咱們可使用一些示例代碼。下載引擎時,它將默認附帶一組示例項目,你能夠檢出這些項目(它們位於 example 文件夾中)。
這些示例代碼是咱們用來快速啓動項目的代碼。在其中,你會發現:
data 文件夾,包含與代碼無關的全部內容。在這裏你能夠找到聲音、音樂、圖像、地圖定義甚至字體。
js文件夾,你將在這裏保存全部與遊戲相關的代碼。
index.html 和 index.css文件。這些是你的應用與外界互動所需的聯繫點。
如今暫時將資源留在 data 文件夾中,咱們須要瞭解該示例爲咱們提供了什麼。
要執行遊戲,你須要作一些事情:
dist
文件夾的內容。將其複製到任意文件夾中,並確保像其餘 JS 文件同樣,將其添加到 index.html
文件中。$ npm install -g http-server
複製代碼
安裝完成後,從項目文件夾中運行:
$ http-server
複製代碼
這時你能夠經過訪問 http://localhost:8080
來測試遊戲。
在遊戲中你會發現這是一個可以進行基本(很是尷尬)動做的平臺遊戲,幾個不一樣的敵人和一個收藏品。基本上這與咱們的目標差很少,但控制方案略有不一樣。
這裏要檢查的關鍵文件是:
其他文件也頗有用,但並非那麼重要,咱們會在須要時使用它們。
若是你提早作好了了功課,可能已經注意到了,沒有一行實例化玩家或敵人的代碼。他們的座標無處可尋。那麼,遊戲該如何理解呢?
這是關卡編輯器所起到的做用。若是你下載了Tiled,則能夠在 data/map
文件夾中打開名爲 map1.tmx
的文件,而後會看到相似下面的內容:
屏幕的中心部分向你顯示正在設計的關卡。若是仔細觀察,你會看到圖像和矩形形狀,其中一些具備不一樣的顏色和名稱。這些對象表明遊戲中的 東西,具體取決於它們的名稱和所屬的層。
在屏幕的右側,你會在其中看到圖層列表(在右上方)。有不一樣類型的層:
右下角包含此地圖的圖塊。 tileet 也能夠由 Tiled 建立,而且能夠在同一文件夾中以 tsx 擴展名找到該 tileet。
最後,在屏幕左側,你會看到「屬性」部分,在這裏你將看到有關所選對象或單擊的圖層的詳細信息。你將可以更改通用屬性(例如圖層的顏色,以便更好地瞭解其對象的位置)並添加自定義屬性(稍後將其做爲參數傳遞給遊戲中實體的構造函數)。
如今咱們已經準備好進行編碼了,讓咱們專一於本文的主要目的,咱們將以示例的工做版本爲例,嘗試對其進行修改,使其能夠用做打字遊戲。
這意味着,須要更改的第一件事是運動方案,或者換句話說:更改控制。
轉到 entities/player.js
並檢查 init
方法。你會注意到不少 bindKey
和 bindGamepad
調用。這些代碼本質上是將特定按鍵與邏輯操做綁定在一塊兒。簡而言之,它能夠確保不管你是按向右箭頭鍵,D 鍵仍是向右移動模擬搖桿,都會在代碼中觸發相同的「向右」動做。
全部這些都須要將其刪除,這對咱們沒什麼用。同時建立一個新文件,將其命名爲 wordServices.js
,並在此文件中建立一個對象,該對象將在每一個回合中返回單詞,這可以幫助咱們瞭解玩家到底選擇了哪一個動做。
/** * Shuffles array in place. * @param {Array} a items An array containing the items. */
function shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
ActionWordsService = {
init: function(totalActions) {
//load words...
this.words = [
"test", "hello", "auto", "bye", "mother", "son", "yellow", "perfect", "game"
]
this.totalActions = totalActions
this.currentWordSet = []
},
reshuffle: function() {
this.words = shuffle(this.words)
},
getRegionPostfix: function(word) {
let ws = this.currentWordSet.find( ws => {
return ws.word == word
})
if(ws) return ws.regionPostfix
return false
},
getAction: function(word) {
let match = this.getWords().find( am => {
return am.word == word
})
if(match) return match.action
return false
},
getWords: function() {
let actions = [ { action: "right", coords: [1, 0], regionPostfix: "right"},
{ action: "left", coords: [-1, 0], regionPostfix: "left"},
{ action: "jump-ahead", coords: [1,-0.5], regionPostfix: "upper-right"},
{ action: "jump-back", coords:[-1, -0.5], regionPostfix: "upper-left"},
{ action: "up", coords: [0, -1], regionPostfix: "up"}
]
this.currentWordSet = this.words.slice(0, this.totalActions).map( w => {
let obj = actions.shift()
obj.word = w
return obj
})
return this.currentWordSet
}
}
複製代碼
本質上,該服務包含一個單詞列表,而後將其隨機排列,而且每次請求該列表時(使用 getWords 方法),都會隨機獲取一組單詞,並將它們分配給上面提到的一種操做。還有與每一個操做相關的其餘屬性:
如今,讓咱們看看如何在遊戲過程當中請求用戶輸入。
注意:繼續前進以前,請記住,爲了使新服務可用於其他代碼,你必須將其包含在 index.html
文件中,就像其餘 JS 庫同樣:
<script type="text/javascript" src="js/wordServices.js"></script>
複製代碼
你能夠潛在地使用鍵綁定的組合來模仿使用遊戲元素的輸入字段的行爲,可是請考慮輸入字段默認提供的全部可能的組合和行爲(例如,粘貼文本、選擇、移動而不刪除字符等) ),必須對全部程序進行編程以使其可用。
相反,咱們能夠簡單地在 HTML 主頁面中添加一個文本字段,並使用 CSS 對其進行樣式設置,使其位於 Canvas 元素之上,它將成爲遊戲的一部分。
你只須要在 <body>
內的這段代碼便可:
<input type="text" id="current-word" />
複製代碼
儘管這徹底取決於你,但我仍是建議你使用 jQuery 來簡化將回調附加到 keypress
事件上所需的代碼。固然可使用原生 JS 完成此操做,但我更喜歡這個庫提供的語法糖。
如下代碼位於 game.js
文件的 load
方法中,負責捕獲用戶的輸入:
me.$input = $("#current-word")
let lastWord = ''
me.$input.keydown( (evnt) => {
if(evnt.which == 13) {
console.log("Last word: ", lastWord)
StateManager.set("lastWord", lastWord)
lastWord = ''
me.$input.val("")
} else {
if(evnt.which > 20) {
let validChars = /[a-z0-9]+/gi
if(!String.fromCharCode(evnt.which).match(validChars)) return false
}
setTimeout(_ => {
lastWord = me.$input.val() //String.fromCharCode(evnt.which)
console.log("Partial: ", lastWord)
}, 1)
}
setTimeout(() => {
StateManager.set("partialWord", me.$input.val())
}, 1);
})
複製代碼
本質上是咱們捕獲輸入元素並將其存儲在全局對象 me
中。這個全局變量包含遊戲所需的一切。
這樣,咱們能夠爲按下的任何按鍵設置事件處理程序。如你所見,我正在檢查鍵碼 13(表明ENTER鍵)以識別玩傢什麼時候完成輸入,不然我將確保他們輸入的是有效字符(我只是避免使用特殊字符,這樣能夠防止 melonJS 提供的默認字體出現問題)。
最後我在 StateManager
對象上設置了兩個不一樣的狀態,lastWord 瞭解玩家輸入的最後一個單詞,partialWord 解如今正在輸入的內容。這兩種狀態很重要。
如何在組件之間共享數據是不少框架中的常見問題。咱們將捕獲的輸入做爲 game
組件的一部分,那麼該如何與他人共享這個輸入呢?
個人解決方案是建立一個充當事件發送器(event emitter)的全局組件:
const StateManager = {
on: function(k, cb) {
console.log("Adding observer for: ", k)
if(!this.observers) {
this.observers = {}
}
if(!this.observers[k]) {
this.observers[k] = []
}
this.observers[k].push(cb)
},
clearObserver: function(k) {
console.log("Removing observers for: ", k)
this.observers[k] = []
},
trigger: function(k) {
this.observers[k].forEach( cb => {
cb(this.get(k))
})
},
set: function(k, v) {
this[k] = v
this.trigger(k)
},
get: function(k) {
return this[k]
}
}
複製代碼
代碼很是簡單,你能夠爲特定狀態設置多個「觀察者」(它們是回調函數),而且一旦設置了該狀態(即更改),便會用新值調用全部這些回調。
建立關卡以前的最後一步是顯示一些基本的 UI。由於咱們須要顯示玩家能夠移動的方向以及須要輸入的單詞。
爲此將使用兩個不一樣的UI元素:
ActionWordsService
上的 regionPostfix
屬性相關聯)ActionWordsService
上的 coords
屬性相關聯。咱們能夠在 js 文件夾內搭上現有的 HUD.js 文件。在其中添加兩個新組件。
第一個是 ActionControl
組件,以下所示:
game.HUD.ActionControl = me.GUI_Object.extend({
init: function(x, y, settings) {
game.HUD.actionControlCoords.x = x //me.game.viewport.width - (me.game.viewport.width / 2)
game.HUD.actionControlCoords.y = me.game.viewport.height - (me.game.viewport.height / 2) + y
settings.image = game.texture;
this._super(me.GUI_Object, "init", [
game.HUD.actionControlCoords.x,
game.HUD.actionControlCoords.y,
settings
])
//update the selected word as we type
StateManager.on('partialWord', w => {
let postfix = ActionWordsService.getRegionPostfix(w)
if(postfix) {
this.setRegion(game.texture.getRegion("action-wheel-" + postfix))
} else {
this.setRegion(game.texture.getRegion("action-wheel")
}
this.anchorPoint.set(0.5,1)
})
//react to the final word
StateManager.on('lastWord', w => {
let act = ActionWordsService.getAction(w)
if(!act) {
me.audio.play("error", false);
me.game.viewport.shake(100, 200, me.game.viewport.AXIS.X)
me.game.viewport.fadeOut("#f00", 150, function(){})
} else {
game.data.score += Constants.SCORES.CORRECT_WORD
}
})
}
})
複製代碼
看起來不少,可是它只是作了一點事情:
settings
屬性中提取其座標,在 Tiled 上設置地圖後,咱們將對其進行檢查。postfix
屬性用於當前編寫的單詞。第二個圖形部分,即要輸入的單詞,以下所示:
game.HUD.ActionWords = me.Renderable.extend({
init: function(x, y) {
this.relative = new me.Vector2d(x, y);
this._super(me.Renderable, "init", [
me.game.viewport.width + x,
me.game.viewport.height + y,
10, //x & y coordinates
10
]);
// Use screen coordinates
this.floating = true;
// make sure our object is always draw first
this.z = Infinity;
// create a font
this.font = new me.BitmapText(0, 0, {
font : "PressStart2P",
size: 0.5,
textAlign : "right",
textBaseline : "bottom"
});
// recalculate the object position if the canvas is resize
me.event.subscribe(me.event.CANVAS_ONRESIZE, (function(w, h){
this.pos.set(w, h, 0).add(this.relative);
}).bind(this));
this.actionMapping = ActionWordsService.getWords()
},
update: function() {
this.actionMapping = ActionWordsService.getWords()
return true
},
draw: function(renderer) {
this.actionMapping.forEach( am => {
if(am.coords[0] == 0 && am.coords[1] == 1) return
let x = game.HUD.actionControlCoords.x + (am.coords[0]*80) + 30
let y = game.HUD.actionControlCoords.y + (am.coords[1]*80) - 30
this.font.draw(renderer, am.word, x, y)
})
}
})
複製代碼
該組件的繁重工做是經過 draw
方法完成的。 init
方法只是初始化變量。在調用 draw
的過程當中,咱們將迭代選定的單詞,並使用與之相關的座標以及一組固定數字,將單詞定位在 ActionControl
組件的座標周圍。
這是建議的動做控制設計的樣子(以及座標如何與之關聯):
固然,它應該有透明的背景。
只需確保將這些圖像保存在 /data/img/assets/UI
文件夾中,這樣當你打開 TexturePacker 時,它將識別出新圖像並將其添加到紋理中地圖集。
上圖顯示瞭如何添加 action wheel 的新圖像。而後,你能夠單擊「Publish sprite sheet」並接受全部默認選項。它將覆蓋現有的地圖集,所以對於你的代碼無需執行任何操做。這一步驟相當重要,由於紋理地圖集將做爲資源加載(一分鐘內會詳細介紹),而且多個實體會將其用於動畫之類的東西。請記住,在遊戲上添加或更新圖形時,都務必這樣作。
好了,如今咱們已經介紹了基礎知識,讓咱們一塊兒玩遊戲。首先要注意的是:地圖。
經過使用 tiled 和 melonJS 中包含的默認 tileet,我建立了這個地圖( 25x16 tiles 地圖,其中 tile 爲 32 x 32px):
這些是我正在使用的圖層:
collision
開頭的全部層都假定爲碰撞層,這意味着其中的任何形狀都是不可遍歷的。在這裏你將定義地板和平臺的全部形狀。準備好以後,咱們能夠轉到 game.js
文件,並在 loaded
方法內添加如下幾行:
// register our objects entity in the object pool
me.pool.register("mainPlayer", game.PlayerEntity);
me.pool.register("CoinEntity", game.CoinEntity);
me.pool.register("HUD.ActionControl", game.HUD.ActionControl);
複製代碼
這些代碼用來註冊你的實體(你要使用 Tiled 直接放置在地圖上的實體)。第一個參數提供的名稱是你須要用 Tiled 進行匹配的名稱。
此外,在此文件中,onLoad
方法應以下所示:
onload: function() {
// init the video
if (!me.video.init(965, 512, {wrapper : "screen", scale : "auto", scaleMethod : "fit", renderer : me.video.AUTO, subPixel : false })) {
alert("Your browser does not support HTML5 canvas.");
return;
}
// initialize the "sound engine"
me.audio.init("mp3,ogg");
// set all ressources to be loaded
me.loader.preload(game.resources, this.loaded.bind(this));
ActionWordsService.init(5)
},
複製代碼
咱們的基本要求是 965x512
的分辨率(我發現,當屏幕的高度與地圖的高度相同時效果很好。在咱們的例子中爲 16*32 = 512
)以後,將使用5個單詞(這些是你能夠繼續前進的5個方向)初始化 ActionWordsService
。
onLoad
方法中另外一條有趣的代碼是:
me.loader.preload(game.resources, this.loaded.bind(this));
複製代碼
遊戲須要的全部類型的資源(即圖像、聲音、背景音樂、JSON 配置文件等)都須要添加到 resources.js
文件中。
這是你資源文件的內容:
game.resources = [
{ name: "tileset", type:"image", src: "data/img/tileset.png" },
{ name: "background", type:"image", src: "data/img/background.png" },
{ name: "clouds", type:"image", src: "data/img/clouds.png" },
{ name: "screen01", type: "tmx", src: "data/map/screen01.tmx" },
{ name: "tileset", type: "tsx", src: "data/map/tileset.json" },
{ name: "action-wheel", type:"image", src: "data/img/assets/UI/action-wheel.png" },
{ name: "action-wheel-right", type:"image", src: "data/img/assets/UI/action-wheel-right.png" },
{ name: "action-wheel-upper-right",type:"image", src: "data/img/assets/UI/action-wheel-upper-right.png" },
{ name: "action-wheel-up", type:"image", src: "data/img/assets/UI/action-wheel-up.png" },
{ name: "action-wheel-upper-left", type:"image", src: "data/img/assets/UI/action-wheel-upper-left.png" },
{ name: "action-wheel-left", type:"image", src: "data/img/assets/UI/action-wheel-left.png" },
{ name: "dst-gameforest", type: "audio", src: "data/bgm/" },
{ name: "cling", type: "audio", src: "data/sfx/" },
{ name: "die", type: "audio", src: "data/sfx/" },
{ name: "enemykill", type: "audio", src: "data/sfx/" },
{ name: "jump", type: "audio", src: "data/sfx/" },
{ name: "texture", type: "json", src: "data/img/texture.json" },
{ name: "texture", type: "image", src: "data/img/texture.png" },
{ name: "PressStart2P", type:"image", src: "data/fnt/PressStart2P.png" },
{ name: "PressStart2P", type:"binary", src: "data/fnt/PressStart2P.fnt"}
];
複製代碼
其中你可使用諸如圖塊集、屏幕映射之類的東西(請注意,名稱始終是不帶擴展名的文件名,這是強制性的要求,不然將找不到資源)。
遊戲中的硬幣很是簡單,可是當你與它們碰撞時,須要發生一些事情,它們的代碼以下所示:
game.CoinEntity = me.CollectableEntity.extend({
/** * constructor */
init: function (x, y, settings) {
// call the super constructor
this._super(me.CollectableEntity, "init", [
x, y ,
Object.assign({
image: game.texture,
region : "coin.png"
}, settings)
]);
},
/** * collision handling */
onCollision : function (/*response*/) {
// do something when collide
me.audio.play("cling", false);
// give some score
game.data.score += Constants.SCORES.COIN
//avoid further collision and delete it
this.body.setCollisionMask(me.collision.types.NO_OBJECT);
me.game.world.removeChild(this);
return false;
}
});
複製代碼
請注意,硬幣實體其實是擴展了 CollectibleEntity
(這給它提供了一個特殊的衝撞類型給實體,所以melonJS知道在玩家移過它時會調用碰撞處理程序),你要作的就是調用其父級的構造函數,而後當你拾起它時,在 onCollision
方法上會播放聲音,在全局得分中加 1,最後從世界中刪除對象。
將全部內容放在一塊兒,就有了一個能夠正常工做的遊戲,該遊戲可讓你根據輸入的單詞在 5 個不一樣的方向上移動。
它看起來應該像這樣:
而且因爲本教程已經太長了,你能夠在 Github 上查看該遊戲的完整代碼。