以前看到一个指尖冒险游戏,以为挺有意思,就想学习一下怎么实现,毕竟当产经提出相似的需求时,问我等开发可不能够实现的时候,不至于回答不知道。
本文的主要思路,参考的是凹凸实验室的这篇文章: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...