以前看到一個指尖冒險遊戲,以爲挺有意思,就想學習一下怎麼實現,畢竟當產經提出相似的需求時,問我等開發可不能夠實現的時候,不至於回答不知道。
本文的主要思路,參考的是凹凸實驗室的這篇文章:H5遊戲開發:指尖大冒險,經過這篇文章和代碼,學習遊戲搭建的總體思路和關鍵技術點。經過CreateJS的中文教程,學習CreateJS的基礎,而後不清楚的api,就翻文檔。
點擊這裏能夠試玩遊戲css
想大概知道CreateJS的構成、各個部分的功能以及經常使用的api,能夠參看這篇文章。
CreateJS 中包含如下四個部分:html
EaselJS是對canvas api的封裝,便於咱們操做canvas繪製圖形圖案。EaselJS定義了不少類型供咱們使用。git
Stage類,是用來實例化一個舞臺,實際上是對canvas元素的包裝,一個canvas元素對應這個一個stage,咱們最終的元素都要使用addChild方法,添加到stage上面。github
const canvas = document.querySelector('#canvas'); //建立舞臺 const stage = new createjs.Stage(canvas);
Shape類用來繪製圖形,每繪製一個圖形都要new一個Shape對象,對象繼承不少方法能夠鏈式調用,使用起來至關方便,好比咱們要繪製一個圓形,只須要以下簡單的代碼便可完成canvas
//建立一個Shape對象 const circle = new createjs.Shape(); //用畫筆設置顏色,調用方法畫矩形,矩形參數:x,y,w,h circle.graphics.beginFill("#f00").drawCircle(0, 0, 100); //添加到舞臺 stage.addChild(circle); //刷新舞臺 stage.update();
其中graphics實際上是Graphics類的一個實例,包含了後面的諸多方法。api
這兩個類都是用來操做圖片的,Bitmap用來繪製單張圖片到stage,SpriteSheet能夠比做css裏的雪碧圖,能夠用來在一張圖片裏提取出多個sprite圖,也能夠方便製做圖片幀動畫。
好比遊戲中咱們要使用樹葉圖片,就以下加入數組
const img = new Image(); img.src = './imgs/leaf.png'; let leaf = null; img.onload = () => { leaf = new Createjs.Bitmap('./imgs/leaf.png'); stage.addChild(leaf); stage.update(); }
上面由於要確保圖片加載以後再渲染到stage上,因此步驟比較麻煩,PreloadJS提供給咱們更加易用的預加載方法,上面代碼就能夠修改以下:app
const queue = new createjs.LoadQueue(); queue.loadManifest([ { id: 'leaf', src: require('./imgs/leaf.png') }, ]); let leaf = null; queue.on('complete', () => { leaf = new createjs.Bitmap(preload.getResult('leaf')); stage.addChild(leaf); stage.update(); });
SpriteSheet則能夠用來方便操做雪碧圖,好比遊戲中,障礙物和階梯其實都在一張雪碧圖上,經過以下的方式,咱們能夠方便的獲取到想要的sprite,以下咱們要獲取階梯:dom
const spriteSheet = new createjs.SpriteSheet({ images: [preload.getResult('stair')], frames: [ [0, 0, 150, 126], [0, 126, 170, 180], [170, 126, 170, 180], [340, 126, 170, 180], [510, 126, 170, 180], [680, 126, 170, 180], ], animations: { stair: [0], wood: [1], explosive: [2], ice: [3], mushroom: [4], stone: [5], }, }); const stair = new createjs.Sprite(spriteSheet, 'stair');
同時使用它能夠方便製做幀動畫,好比機器人的跳躍動畫:ide
const spriteSheet = new createjs.SpriteSheet({ images: [prelaod.getResult('player')], frames: { width: 150, height: 294, count: 17, }, animations: { work: [0, 9, 'walk', 0.2], jump: [10, 16, 0, 0.5], }, }); const sprite = new createjs.Sprite(spriteSheet); sprite.gotoAndPlay('jump');
Container類,用來新建一個容器對象,它能夠包含 Text 、 Bitmap 、 Shape 、 Sprite 等其餘的 EaselJS 元素,多個元素包含在一個 Container 中方便統一管理。好比遊戲中floor對象和robot對象,其實會被添加進同一個container,保證floor和robot始終在屏幕的中央。
const contain = new createjs.Container(); contain.addChild(floor, robot); stage.addChild(contain);
舞臺的刷新要調用update,但始終手動調用不太可能,咱們通常在createjs裏面的ticker事件中調用,每觸發一次tick事件,就update一下舞臺
createjs.Ticker.addEventListener(「tick」, tick); function tick(e) { if (e.paused !== 1) { //處理 stage.update(); //刷新舞臺 }else {} } createjs.Ticker.paused = 1; //在函數任何地方調用這個,則會暫停tick裏面的處理 createjs.Ticker.paused = 0; //恢復遊戲 createjs.Ticker.setFPS(60); // 用來設置tick的頻率
tweenjs主要是負責動畫處理,好比遊戲中樹葉的位移動畫以下:
createjs.Tween.get(this.leafCon1, { override: true }) .to({ y: this.nextPosY1 }, 500) .call(() => { this.moving = false; });
overrider設置爲true,是爲了保證該對象在執行當前動畫的時候沒有別的動畫在執行,to將leafCon1的y座標設爲nextPosY1,call是動畫執行完畢後的回調。
在編寫遊戲過程成,經常使用到的api大概就這麼多,還有不少用法,須要的時候查閱文檔就好了。
整個遊戲按照渲染層次劃分爲景物層、階梯層、背景層。每一個層面上,只需關注自身的渲染,以及暴露給控制層的邏輯接口。
咱們將遊戲拆分紅4個對象,樹葉類Leaves用來負責渲染無限滾動效果的樹葉背景;階梯類Floor用來渲染階梯和障礙物,自身實現階梯的生成和掉落方法;機器人類Robot用來渲染機器人,自身實現左跳、右跳、掉落和撞上障礙物的邏輯處理;Game類用來控制整個遊戲的流程,負責整個舞臺的最終渲染,組合各個對象的邏輯操做。
對於景物層,用來渲染兩邊的樹葉,樹葉的渲染比較簡單,只是將2張樹葉圖片渲染到canvas,在createjs裏面咱們全部的實例,都是經過addchild的方法,添加到stage上面。2張圖片咱們分別用Bitmap建立,設置好相應的x座標(一個緊貼屏幕左邊,一個緊貼右邊),同時將2個bitmap實例,添加到container裏面,以便做爲一個總體進行操做。由於景物層須要作出無限延伸的效果,因此須要拷貝一個container製造不斷移動的假象,具體原理參看指尖大冒險。在每次點擊事件裏,調用translateY(offset),就可讓樹葉移動一段距離。
class Leaves { constructor(options, canvas) { this.config = { transThreshold: 0, }; Object.assign(this.config, options); this.moving = false; this.nextPosY1 = 0; this.nextPosY2 = 0; this.canvas = canvas; this.leafCon1 = null; // 樹葉背景的容器 this.leafCon2 = null; this.sprite = null; this.leafHeight = 0; this.init(); } init() { const left = new createjs.Bitmap(preload.getResult('left')); const right = new createjs.Bitmap(preload.getResult('right')); left.x = 0; right.x = this.canvas.width - right.getBounds().width; this.leafCon1 = new createjs.Container(); this.leafCon1.addChild(left, right); this.leafHeight = this.leafCon1.getBounds().height; this.nextPosY1 = this.leafCon1.y = this.canvas.height - this.leafHeight; // eslint-disable-line this.leafCon2 = this.leafCon1.clone(true); // //某些createjs版本這個方法會報 圖片找不到的錯誤 this.nextPosY2 = this.leafCon2.y = this.leafCon1.y - this.leafHeight; // eslint-disable-line this.sprite = new createjs.Container(); this.sprite.addChild(this.leafCon1, this.leafCon2); } tranlateY(distance) { if (this.moving) return; this.moving = true; const threshold = this.canvas.height || this.config.transThreshold; const curPosY1 = this.leafCon1.y; const curPosY2 = this.leafCon2.y; this.nextPosY1 = curPosY1 + distance; this.nextPosY2 = curPosY2 + distance; if (curPosY1 >= threshold) { this.leafCon1.y = this.nextPosY2 - this.leafHeight; } else { createjs.Tween.get(this.leafCon1, { override: true }) .to({ y: this.nextPosY1 }, 500) .call(() => { this.moving = false; }); } if (curPosY2 >= threshold) { this.leafCon2.y = this.nextPosY1 - this.leafHeight; } else { createjs.Tween.get(this.leafCon2, { override: true }) .to({ y: this.nextPosY2 }, 500) .call(() => { this.moving = false; }); } } }
階梯類用來負責階梯的生成,以及障礙物的生成,同時也要負責階梯掉落的邏輯。
class Floor { constructor(config, canvas) { this.config = {}; this.stairSequence = []; //階梯渲染對應的序列 this.barrierSequence = []; //障礙物渲染對應的序列 this.stairArr = []; //階梯的spite對象數組 this.barrierArr = []; //障礙物的spite對象數組 this.barrierCon = null; // 障礙物容器 this.stairCon = null; // 階梯容器 this.canvas = canvas; this.lastX = 0; // 最新一塊階梯的位置 this.lastY = 0; this.dropIndex = -1; Object.assign(this.config, config); this.init(); } init() { this.stair = new createjs.Sprite(spriteSheet, 'stair'); this.stair.width = this.stair.getBounds().width; this.stair.height = this.stair.getBounds().height; let barriers = ['wood', 'explosive', 'ice', 'mushroom', 'stone']; barriers = barriers.map((item) => { const container = new createjs.Container(); const st = this.stair.clone(true); const bar = new createjs.Sprite(spriteSheet, item); bar.y = st.y - 60; container.addChild(st, bar); return container; }); this.barriers = barriers; const firstStair = this.stair.clone(true); firstStair.x = this.canvas.width / 2 - this.stair.width / 2; //eslint-disable-line firstStair.y = this.canvas.height - this.stair.height - bottomOffset;//eslint-disable-line this.lastX = firstStair.x; this.lastY = firstStair.y; this.stairCon = new createjs.Container(); this.barrierCon = new createjs.Container(); this.stairCon.addChild(firstStair); this.stairArr.push(firstStair); this.sprite = new createjs.Container(); this.sprite.addChild(this.stairCon, this.barrierCon); } addOneFloor(stairDirection, barrierType, animation) { //stairDirection -1 表明前一個階梯的左邊,1右邊 //逐一添加階梯,每一個添加一個階梯,對應選擇添加一個障礙物 } addFloors(stairSequence, barrierSequence) { stairSequence.forEach((item, index) => { this.addOneFloor(item, barrierSequence[index], false); // 批量添加無動畫 }); } dropStair(stair) { //掉落摸一個階梯,同時掉落障礙物數組中y軸座標大於當前掉落階梯y軸座標的障礙物 } drop() { const stair = this.stairArr.shift(); stair && this.dropStair(stair); // eslint-disable-line while (this.stairArr.length > 9) { this.dropStair(this.stairArr.shift()); //階梯數組最多顯示9個階梯 } } }
Robot類用來建立機器人對象,機器人對象須要move方法來跳躍階梯,同時也須要處理踏空和撞到障礙物的狀況。
class Robot { constructor(options, canvas) { this.config = { initDirect: -1, }; Object.assign(this.config, options); this.sprite = null; this.canvas = canvas; this.lastX = 0; //上一次x軸位置 this.lastY = 0;// 上一次y軸位置 this.lastDirect = this.config.initDirect; //上一次跳躍的方向 this.init(); } init() { const spriteSheet = new createjs.SpriteSheet({ /* 機器人sprites */ }); this.sprite = new createjs.Sprite(spriteSheet); const bounds = this.sprite.getBounds(); this.sprite.x = this.canvas.width / 2 - bounds.width / 2; this.lastX = this.sprite.x; this.sprite.y = this.canvas.height - bounds.height - bottomOffset - 40; this.lastY = this.sprite.y; if (this.config.initDirect === 1) { this.sprite.scaleX = -1; this.sprite.regX = 145; } // this.sprite.scaleX = -1; } move(x, y) { this.lastX += x; this.lastY += y; this.sprite.gotoAndPlay('jump'); createjs.Tween.get(this.sprite, { override: true }) .to({ x: this.lastX, y: this.lastY, }, 200); } moveRight() { if (this.lastDirect !== 1) { this.lastDirect = 1; this.sprite.scaleX = -1; this.sprite.regX = 145; } this.move(moveXOffset, moveYOffset); } moveLeft() { if (this.lastDirect !== -1) { this.lastDirect = -1; this.sprite.scaleX = 1; this.sprite.regX = 0; } this.move(-1 * moveXOffset, moveYOffset); } dropAndDisappear(dir) {// 踏空掉落 處理 const posY = this.sprite.y; const posX = this.sprite.x; this.sprite.stop(); createjs.Tween.removeTweens(this.sprite); createjs.Tween.get(this.sprite, { override: true }) .to({ x: posX + dir * 2 * moveXOffset, y: posY + moveYOffset, }, 240) .to({ y: this.canvas.height + this.sprite.y, }, 800) .set({ visible: false, }); } hitAndDisappear() {// 撞擊障礙物處理 createjs.Tween.get(this.sprite, { override: true }) .wait(500) .set({ visible: false, }); } }
Game類是整個遊戲的控制中心,負責用戶點擊事件的處理,負責將各個對象最終添加到舞臺,
class Game { constructor(options) { // this.init(); this.config = { initStairs: 8, onProgress: () => {}, onComplete: () => {}, onGameEnd: () => {}, }; Object.assign(this.config, options); this.stairIndex = -1; // 記錄當前跳到第幾層 this.autoDropTimer = null; this.clickTimes = 0; this.score = 0; this.isStart = false; this.init(); } init() { this.canvas = document.querySelector('#stage'); this.canvas.width = window.innerWidth * 2; this.canvas.height = window.innerHeight * 2; this.stage = new createjs.Stage(this.canvas); createjs.Ticker.setFPS(60); createjs.Ticker.addEventListener('tick', () => { if (e.paused !== true) { this.stage.update(); } }); queue.on('complete', () => { this.run(); this.config.onComplete(); }); queue.on('fileload', this.config.onProgress); } getInitialSequence() {// 獲取初始的階梯和障礙物序列 const stairSeq = []; const barrSeq = []; for (let i = 0; i < this.config.initStairs; i += 1) { stairSeq.push(util.getRandom(0, 2)); barrSeq.push(util.getRandomNumBySepcial(this.config.barrProbabitiy)); } return { stairSeq, barrSeq, }; } createGameStage() { //渲染舞臺 this.background = new createjs.Shape(); this.background.graphics.beginFill('#001605').drawRect(0, 0, this.canvas.width, this.canvas.height); const seq = this.getInitialSequence(); this.leves = new Leaves(this.config, this.canvas); this.floor = new Floor(this.config, this.canvas); this.robot = new Robot({ initDirect: seq.stairSeq[0], }, this.canvas); this.stairs = new createjs.Container(); this.stairs.addChild(this.floor.sprite, this.robot.sprite); // robot 與階梯是一體,這樣才能在跳躍時保持robot與stair的相對距離 this.stairs.lastX = this.stairs.x; this.stairs.lastY = this.stairs.y; this.floor.addFloors(seq.stairSeq, seq.barrSeq); this.stage.addChild(this.background, this.stairs, this.leves.sprite); // 全部的container 從新 add,才能保證stage clear有效,舞臺從新渲染,不然restart後有重複的 } bindEvents() { this.background.addEventListener('click', this.handleClick.bind(this)); // 必須有元素纔會觸發,點擊空白區域無效 // this.stage.addEventListener('click', this.handleClick); // 必須有元素纔會觸發,點擊空白區域無效 } run() { this.clickTimes = 0; this.score = 0; this.stairIndex = -1; this.autoDropTimer = null; this.createGameStage(); this.bindEvents(); createjs.Ticker.setPaused(false); } start() { this.isStart = true; } restart() { this.stage.clear(); this.run(); this.start(); } handleClick(event) { if (this.isStart) { const posX = event.stageX; this.stairIndex += 1; this.clickTimes += 1; let direct = -1; this.autoDrop(); if (posX > (this.canvas.width / 2)) { this.robot.moveRight(); direct = 1; this.centerFloor(-1 * moveXOffset, -1 * moveYOffset); } else { this.robot.moveLeft(); direct = -1; this.centerFloor(moveXOffset, -1 * moveYOffset); } this.addStair(); this.leves.tranlateY(-1 * moveYOffset); this.checkJump(direct); } } centerFloor(x, y) { // 將階梯層始終置於舞臺中央 this.stairs.lastX += x; this.stairs.lastY += y; createjs.Tween.get(this.stairs, { override: true }) .to({ x: this.stairs.lastX, y: this.stairs.lastY, }, 500); } checkJump(direct) { //機器人每次跳躍檢查 是否掉落消失 const stairSequence = this.floor.stairSequence; // like [-1, 1,1,-1], -1表明左,1表明右 if (direct !== stairSequence[this.stairIndex]) {// 當前跳到的樓層的階梯方向與跳躍的方向不一致,則表明失敗 this.drop(direct); this.gameOver(); } } drop(direct) { const barrierSequence = this.floor.barrierSequence; if (barrierSequence[this.stairIndex] !== 1) { this.robot.dropAndDisappear(direct); } else { this.shakeStairs(); this.robot.hitAndDisappear(); } } shakeStairs() { createjs.Tween.removeTweens(this.stairs); createjs.Tween.get(this.stairs, { override: true, }).to({ x: this.stairs.x + 5, y: this.stairs.y - 5, }, 50, createjs.Ease.getBackInOut(2.5)).to({ x: this.stairs.x, y: this.stairs.y, }, 50, createjs.Ease.getBackInOut(2.5)).to({ x: this.stairs.x + 5, y: this.stairs.y - 5, }, 50, createjs.Ease.getBackInOut(2.5)).to({ // eslint-disable-line x: this.stairs.x, y: this.stairs.y, }, 50, createjs.Ease.getBackInOut(2.5)).pause(); // eslint-disable-line } addStair() { //添加隨機方向的一個階梯 const stair = util.getRandom(0, 2); const barrier = util.getRandomNumBySepcial(this.config.barrProbabitiy); this.floor.addOneFloor(stair, barrier, true); } autoDrop() { //階梯自動掉落 if (!this.autoDropTimer) { this.autoDropTimer = createjs.setInterval(() => { this.floor.drop(); if (this.clickTimes === this.floor.dropIndex) { createjs.clearInterval(this.autoDropTimer); this.robot.dropAndDisappear(0); this.gameOver(); } }, 1000); } } gameOver() { createjs.clearInterval(this.autoDropTimer); this.isStart = false; this.config.onGameEnd(); setTimeout(() => { createjs.Ticker.setPaused(true); }, 1000); } }
本文只是在H5遊戲開發:指尖大冒險的基礎上,將代碼實現了一遍,在這個過程不只學到了createjs的一些基本用法,也知道了遊戲開發問題的解決能夠從視覺層面以及邏輯底層兩方面考慮。createjs在使用過程也會遇到一些問題,好比clear舞臺以後,舞臺上的元素並無清空,這些我在代碼裏也作了註釋。感興趣的同窗能夠看一下源碼 https://github.com/shengbowen...