之前看到一个指尖冒险游戏,觉得挺有意思,就想学习一下怎么实现,毕竟当产经提出类似的需求时,问我等开发可不可以实现的时候,不至于回答不知道。
本文的主要思路,参考的是凹凸实验室的这篇文章:H5游戏开发:指尖大冒险,通过这篇文章和代码,学习游戏搭建的整体思路和关键技术点。通过CreateJS的中文教程,学习CreateJS的基础,然后不清楚的api,就翻文档。
点击这里可以试玩游戏

CreateJS介绍

想大概知道CreateJS的构成、各个部分的功能以及常用的api,可以参看这篇文章。
CreateJS 中包含以下四个部分:

  • EaselJS:用于 Sprites、图形和位图的绘制,是操作canvas的核心库
  • TweenJS:用于做动画效果
  • SoundJS:音频播放引擎
  • PreloadJS:网站资源预加载,提供加载的进度回调,以及资源获取

EaselJS常用方法

EaselJS是对canvas api的封装,便于我们操作canvas绘制图形图案。EaselJS定义了很多类型供我们使用。

Stage类

Stage类,是用来实例化一个舞台,其实是对canvas元素的包装,一个canvas元素对应这个一个stage,我们最终的元素都要使用addChild方法,添加到stage上面。

const canvas = document.querySelector('#canvas');
//创建舞台
const stage = new createjs.Stage(canvas);

Shape类

Shape类用来绘制图形,每绘制一个图形都要new一个Shape对象,对象继承很多方法可以链式调用,使用起来相当方便,比如我们要绘制一个圆形,只需要如下简单的代码即可完成

//创建一个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类的一个实例,包含了后面的诸多方法。

Bitmap、SpriteSheet

这两个类都是用来操作图片的,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提供给我们更加易用的预加载方法,上面代码就可以修改如下:

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,如下我们要获取阶梯:

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');

同时使用它可以方便制作帧动画,比如机器人的跳跃动画:

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类

Container类,用来新建一个容器对象,它可以包含 Text 、 Bitmap 、 Shape 、 Sprite 等其他的 EaselJS 元素,多个元素包含在一个 Container 中方便统一管理。比如游戏中floor对象和robot对象,其实会被添加进同一个container,保证floor和robot始终在屏幕的中央。

const contain = new createjs.Container();
contain.addChild(floor, robot);
stage.addChild(contain);

stage刷新

舞台的刷新要调用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

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类用来控制整个游戏的流程,负责整个舞台的最终渲染,组合各个对象的逻辑操作。

Leaves

对于景物层,用来渲染两边的树叶,树叶的渲染比较简单,只是将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-linethis.leafCon2 = this.leafCon1.clone(true); //  //某些createjs版本这个方法会报 图片找不到的错误this.nextPosY2 = this.leafCon2.y = this.leafCon1.y - this.leafHeight; // eslint-disable-linethis.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; });}}
}

Floor

阶梯类用来负责阶梯的生成,以及障碍物的生成,同时也要负责阶梯掉落的逻辑。

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-linefirstStair.y = this.canvas.height - this.stair.height - bottomOffset;//eslint-disable-linethis.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-linewhile (this.stairArr.length > 9) {this.dropStair(this.stairArr.shift()); //阶梯数组最多显示9个阶梯}}
}

Robot

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

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-linex: 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...

参考

  • H5游戏开发:指尖大冒险
  • 一篇文章带你快速入门 CreateJS
  • CreateJS的中文教程
  • createjs视频

createjs开发h5游戏: 指尖大冒险相关推荐

  1. 指尖大冒险、跳一跳升级版html5游戏全代码

    指尖大冒险.跳一跳升级版html游戏全代码 博主的话 文件目录 运行图片 指尖大冒险.html style.css 进行下一个游戏的开发! 博主的话 这是最后一个纯原生js代码编出来的游戏了.我连这个 ...

  2. 开发H5游戏引擎的选择:Egret或Laya?

    一.H5游戏开发的引擎介绍 开发H5游戏的引擎有很多,比如egret.laya.cocos-js等等.这里主要是分析的是egret和laya,因为我们团队是从as3转过来的.所以天然地在有as3基因的 ...

  3. 最赚钱H5游戏《大天使之剑H5》公测24天流水破亿!

    H5游戏自2016年<传奇世界>首破千万月流水以来,记录不断被刷新,据公开报道<传奇世界>月流水超过3000万,<决战沙城>月流水超过2000万.如今破亿的H5游戏 ...

  4. rust游戏解封了吗_又一款分手游戏!只只大冒险操作简单但需要默契

    B站(bilibili)即将联手NExT Studios发行由NExT Studios研发的新合作冒险游戏<只只大冒险>.此前,两家也曾联合发行过因游戏玩法新颖和推理精彩而广受好评的听觉探 ...

  5. 介绍一下我大二开发的游戏:地下城冒险

    前言:写来参加学校比赛的,当时写了一个月左右就期末考试了,后来就没写过了,单机游戏,也是一个烂尾工程. 介绍:这是一款3D的RPG游戏,那时候也是看到龙之谷手游是Unity做的,就想自己能不能来一发. ...

  6. cocoscreator开发h5游戏提升加载速度

    有个cocoscreator开发的游戏项目需要导出h5端.开发完毕,导出web-mobile端,然后部署到nginx容器里,问题来了,加载速度有点慢. 解决这个问题的直接方式,就是直接把游戏包放在cd ...

  7. 使用Cocos2d-JS开发H5游戏

    如何学习 如何开始学习Cocos2d-JS?我我觉得比较好的方式是: 1)看测试例:测试例 2)看API文档: 在线API索引(中文版) 下载版API索引 3)看源码 另外还有大神录制了进阶视频教程: ...

  8. php开发h5游戏,H5的canvas实现贪吃蛇小游戏

    这次给大家带来H5的canvas实现贪吃蛇小游戏,H5的canvas实现贪吃蛇小游戏注意事项有哪些,下面就是实战案例,一起来看一下. 本文介绍了H5 canvas实现贪吃蛇小游戏,分享给大家,具体如下 ...

  9. 开发H5游戏练手, 黑暗堡垒-炼狱传奇H5 (一) 登陆界面开发

    项目介绍 使用JS lufylegend框架编写RPG游戏练手项目,首先展示几项目的效果图: 项目地址 https://github.com/mangenotwork/HABL-H5 介绍 lufyl ...

最新文章

  1. 赠书 | 人工智能识万物:卷积神经网络的前世今生
  2. Cloudify — 系统架构
  3. python中flag的用法_python中“标志位”的使用
  4. MySQL的FROM_UNIXTIME()和UNIX_TIMESTAMP()函数
  5. layuiadmin上手好难_新手如何快速上手自媒体赚钱?送你4个好建议!记得收藏
  6. python强类型_python动态性强类型用法实例
  7. 盘一盘2020年上半年的微博热搜词条(文末提供获取热搜词条方法)
  8. 详解display:inline | block |inline-block的区别(转)
  9. UINavigationController 多次连续 Push Pop 问题
  10. GB28181协议常见几种信令流程(三)
  11. netty实战pdf下载,深度解密:Java与线程的关系
  12. ros运行cpp文件
  13. 4.2.6 图层、通道和蒙版
  14. excel 数组公式
  15. 关于Code Virtualizer pcode解密的一种方法
  16. Oracle 查询当前用户所有表以及统计表内数据行数
  17. Win10 出现“该文件没有与之关联的程序来执行该操作”解决方法
  18. android view设置按钮颜色_建议收藏!最全 Android 常用开源库总结!
  19. 免疫算法求解多元函数论文
  20. 2012年西安校园招聘会

热门文章

  1. Java大厂技术文档:Redis+Nginx+设计模式+Spring全家桶+Dubbo精选
  2. ISA2006实战系列之三:防火墙策略部署深度分析(附图)
  3. Go语言基础语法--注释、基础结构2
  4. Spring Cloud Edgware新特性之一:解决Eureka中Jersey 1.x版本过旧的问题-不使用Jersey
  5. 为centos选择国内yum软件库
  6. 在线视频解决方案供应商
  7. 配置Linux声卡,让Arch高歌
  8. 职场思想分享002 | 入职第一个月工资这样花,一年内工资至少再涨50%!
  9. Java 高级算法——数组中查询重复的数字
  10. [Android]打开eclipse报错:发现了以元素 'd:skin' 开头的无效内容。此处不应含有子元素。...