javascript高仿热血传奇游戏实现代码


Posted in Javascript onFebruary 22, 2018

前言

游戏的第一个版本开发于14年,浏览器端使用html+css+js,服务端使用asp+php,通讯采用ajax,数据存储使用access+mySql。不过由于一些问题(当时还不会用node,用asp写复杂的逻辑真的会写吐;当时对canvas写的也少,dom渲染很容易达到性能瓶颈),已经废弃。后来用canvas重制了一版。本文写于18年。

javascript高仿热血传奇游戏实现代码 

1.开发前的准备

为什么要用Javascript来实现一款比较复杂的PC端游戏

1.js实现PC端网游是可行的。随着PC、手机硬件配置的升级和浏览器的更新换代,以及H5各种库的发展,js实现一款网游的难度越来越低。这里的难度主要是两方面:浏览器的性能;js代码是否足够易于扩展,以满足于一款逻辑极其复杂的游戏的迭代。

2.现阶段的js游戏里,很少有规模较大的可供参考。涉及到多人联机、服务端数据存储、复杂交互的游戏,大多数(几乎全部)都是用flash开发的。但是flash毕竟在衰落,而js发展迅速,并且只要有浏览器就可以运行。

为什么选择了一款2001年的热血传奇游戏

第一个原因是对老游戏的情怀; 当然更重要的另一个原因是,别的游戏要么我不会玩、要么我会玩但没有素材(图片、音效等)。花很大精力去收集一个游戏的地图、人物怪物模型、物品和装备图,然后去处理、解析一遍再用于js开发,我觉得是浪费时间。

由于我以前搜集了一些传奇游戏的素材,并且幸运地找到了提取热血传奇客户端资源文件的方法( github地址 ),所以可以直接开始写码,省去了一些准备时间。

可能的困难

1.浏览器的运行性能:这个应该是最困难的一点。假如游戏要保持40帧,那么每帧只有25ms的时间留给js计算。并且由于渲染通常比计算耗性能,实际上留给js的时间只有10毫秒左右。

2.防作弊:如何避免用户直接调用接口或者篡改网络请求数据?由于目标是用js实现比较复杂的游戏,并且任何网络游戏都需要考虑这一点,一定会有相对成熟的方案。此处不是本文重点。

2.整体设计

浏览器端

画面渲染使用canvas。

相比dom(div)+css,canvas可以处理比较复杂的场景渲染和事件管理,例如下面这个场景,涉及了四张图片:玩家、动物、地上的物品、最下层的地图图片。(实际还有地上的影子,鼠标指向人物、动物、物品时出现的相应名称,以及地面上的阴影。为了方便读懂,先不考虑这么多内容。)

javascript高仿热血传奇游戏实现代码 

这时,如果希望实现“点击动物、攻击动物;点击物品、捡起物品”的效果,那么需要对动物和物品进行事件监听。如果采用dom的方式,那么会出现几点难于处理的问题:

a.渲染的顺序和事件处理的顺序不同(有时候z-index小的需要先处理事件),需要额外处理。例如这个上面的例子里:点击怪物、点击物品的时候也容易点到人物,那么需要给人物做“点击事件穿透”的处理。而且事件处理的顺序不固定:假如我有一个技能(例如游戏里的治疗)需要点人物才可以释放,那么这时人物又需要有事件监听。所以一个元素是否需要处理事件、处理事件的先后,是随着游戏状态的不同而变化的,而 dom的事件绑定已经不能满足需要 。

b.有关联的元素难以放在同一个dom节点中:例如玩家的模型、玩家的名字和玩家身上的技能画效,理想情况下是放在一个 <div> 或者 <section> 容器里,便于管理(这样几个元素的定位就可以继承父元素,不用分别处理位置了)。但是这样,z-index会很难处理。例如玩家A在玩家B的上面,那么A会被B遮挡,因此需要A的z-index小一些,但是又需要让玩家A的名字不会被B的名字或者影子遮挡,就无法实现。简单点说, dom结构的可维护性会牺牲画面展示的效果,反之亦然 。

c.性能问题。即使牺牲了效果,用dom渲染,势必出现很多嵌套关系,所有元素的style都在频繁变化,连续触发浏览器的repaint甚至reflow。

canvas渲染逻辑与项目逻辑分离

如果将canvas的各种渲染操作(如 drawImage 、 fillText 等)与项目代码放在一起,那么势必导致项目后期无法维护。翻了一下几款现有的canvas库,结合vue的数据绑定+调试工具的方式,搞了一个全新的canvas库Easycanvas( github地址 ),并且像vue一样支持通过一个插件来调试canvas中的元素。

这样,整个游戏的渲染部分就容易很多,只需要管理游戏当前的状态、并且根据服务端从socket传回来的数据去更新数据就可以。 “数据的变化引起视图的变化”这个环节由Easycanvas负责 。例如下图的玩家包裹物品的实现,我们只需要给出包裹容器的位置、背包里每个元素的排布规则,然后将每个包裹的物品绑定到一个array上,然后去管理这个array即可(数据映射到画面的过程由Easycanvas负责)。

javascript高仿热血传奇游戏实现代码 

例如,5行8列共计40个物品的样式可以通过如下的形式传递给Easycanvas(index为物品索引,物品x方向间距36,y方向间距32)。而这个逻辑是一成不变的,无论物品的数组怎样变化、包裹被拖拽到什么位置,每个物品的相对位置都是固定的。至于canvas上的渲染则完全不需要项目本身来考虑,所以可维护性较好。

style: {
 tw: 30, th: 30,
 tx: function () {
 return 40 + index % 8 * 36;
 },
 ty: function () {
 return 31 + Math.floor(index / 8) * 32;
 }
}

canvas分层渲染

假设:游戏需要保持40帧,浏览器宽800高600,面积48万(后面称48万为1个屏幕面积)。

如果用同一个canvas来呈现,那么这个canvas的帧数40,每秒至少需要绘制40个屏幕面积。但是同一个坐标点很可能出现多个元素重叠的情况,例如底部的UI、血条、按钮就是重叠放置,他们又共同遮挡了场景地图。所以这些加在一起,每秒浏览器的绘制量很容易达到100个屏幕面积以上。

这个绘制是很难优化的,因为整个canvas画布的任何一处都在进行视图的更新:可能是玩家和动物的移动、可能是按钮的特效、可能是某个技能效果的变化。这样的话,即使玩家不动,由于衣服“随风飘飘”的效果(其实是精灵动画播放到下一张图),或者是地面上出现了一瓶药水,都要引起整个canvas的重绘。因为 游戏中几乎不可能出现某一帧的画面与上一帧毫无区别的情况,即使是游戏画面的一个局部,也很难保持不变 。整个游戏的画面永远在更新。

因为 游戏中几乎不可能出现某一帧的画面与上一帧毫无区别的情况 ,画面永远在更新。

因此,这次我采用了3个canvas重叠排布的方式。由于Easycanvas的事件处理支持传递,因此即使点到了最上面的canvas,如果没有任何元素结束了某一次点击,后面的canvas也可以接到这次事件。3个canvas分别负责UI、地面(地图)、精灵(人物、动物、技能特效等):

javascript高仿热血传奇游戏实现代码 

这样分层的好处是,每层最大帧数可以根据需要来调整:

例如UI层,因为很多UI平时是不动的,即使动也不会需要太精密的绘制,所以可以适当降低帧数,例如降低到20。这样假如玩家的体力从100降低到20,那么可以在50ms内更新视图,而50ms的切换是玩家感觉不出来的。因为像体力这种 UI层数据的变化很难在很短的时间内连续变化多次,而50ms的延迟是人很难感知的,所以不需要频繁的绘制 。假如我们每秒节约了20帧,那么很可能可以节约10个屏幕面积的绘制。

再如地面,只有玩家移动的时候,地图才会变化。这样,如果玩家不动,那么每帧可以省去1个屏幕面积。由于需要保证玩家移动时的流畅感,地面的最大帧数不宜太低。假如地面为30帧,那么玩家不动时,每秒就可以节约30个屏幕面积的绘制(这个项目中,地图是几乎绘满屏幕的)。而且其它玩家、动物的移动不会改变地面,也不需要重绘地面这一层。

精灵层最大帧数不能降低,这层会展示游戏的人物动作等核心部分,所以最大帧数设置为40.

这样,每秒绘制的面积,玩家移动时可能是80~100个屏幕面积,而玩家不移动时可能只有50个屏幕面积。游戏中,玩家停下来打怪、打字、整理物品、释放技能都是站立不动的,因此 大量的时间里都不会触发地面的绘制,对性能的节约很大 。

服务器端

由于目标是js实现一款多人网游,所以服务端使用Node,使用socket与浏览器通讯。这样做还有一个好处,就是一些 公用的逻辑可以在两端复用 ,例如判断地图上某个坐标点是否存在障碍物。

Node端的玩家、场景等游戏相关数据全部存储与内存中,定期同步至文件。每次Node服务启动时,将数据从文件读取至内存。这样可以玩家较多时,文件读写的频率成指数级上升,从而引发的性能问题。(后来为了提高稳定,为文件读写增加了一个缓冲,“内存-文件-备份”的方式,以免读写过程中服务器重启导致的文件损坏)。

Node端分接口、数据、实例等多层。“接口”负责和浏览器端交互。“数据”是一些静态数据,例如某个药品的名称和效果、某个怪物的速度和体力,是游戏规则的一部分。“实例”是游戏中的当前状态,例如某个玩家身上的一个药品,就是“药品数据”的一个实例。再举个例子,“鹿的实例”拥有“当前血量”这个属性,鹿A可能是10,鹿B可能是14,而“鹿”本身只有“初始血量”。

3.场景地图的实现

地图场景

下面开始介绍地图场景部分,仍然是依赖 Easycanvas 进行渲染。

思考

由于玩家是始终固定在屏幕中心的,所以玩家的移动,实际上是地图的移动。例如玩家像左跑,地图就向右平移即可。刚才已经提到,玩家处于3个canvas中的中间一层,而地图属于底层,因此玩家一定遮挡地图。

这样看起来是合理的,但是假如地图中有一棵树,那么“玩家的层次始终高于树”就不对了。这时,有2种大的解决方案:

地图分层,“地面”与“地上”拆开。将玩家处于两层之间,例如下图,左侧是地上、右侧是地面,然后重叠绘制,把人物夹在中间:

javascript高仿热血传奇游戏实现代码 

这样看似解决了问题,其实引入了2个新的问题:第一个是,玩家有时可能会被“地上”的东西遮挡(例如一棵树),有时又需要能够遮挡“地上”的东西(例如站在这棵树的下方,头部会遮挡住树)。另一个问题是渲染的性能消耗会增加。由于玩家是时刻在变的,“地上”这一层需要频繁重绘。这样做也打破了最初的设计——尽量节约地面大地图的渲染,从而导致canvas的分层更加复杂。

地图不分层,“地面”与“地上”在一起绘制。当玩家处于树后的时候,将玩家的透明度设置为0.5,例如下图:

javascript高仿热血传奇游戏实现代码 

这样做只有一个坏处:玩家的身体要么都不透明、要么都半透明(怪物在地图上行走也会有这个效果),不会完全真实。因为理想的效果是存在玩家的身体被遮挡住一部分的场景的。但是这样做对性能友好,并且代码易于维护,目前我也采用了这个方案。

那么如何判断“地图”这张图片哪些地方是树呢?游戏通常会有一个大的地图描述文件(其实就是一个Array),通过0、1、2这样的数字来标识哪些地方可以通过、哪些地方存在障碍物、哪些地方是传送点等等。热血传奇中的这个“描述文件”就是48x32为最小单位进行描述的,所以玩家在传奇中的行动会有一种“棋盘”的感觉。单位越小越流畅,但是占用的体积越大、生成这个描述的过程也就越耗时。

下面开始正题。

实现

我找了一个朋友帮我导出热血传奇客户端中“比奇省”的地图,宽33600、高22400,是我电脑的几百倍大。为了避免电脑爆炸,需要拆分成多块加载。由于传奇的最小单元是48x32,我们以480x320将地图拆成了4900(70x70)个图片文件。

canvas的尺寸我们设定为800x600,这样玩家只需要加载3x3共计9张图片就可以铺满整个画布。800/480=1.67,那么为什么不是2x2?因为有可能玩家当前的位置正好导致有的图片只展示了一部分。如下图:

javascript高仿热血传奇游戏实现代码 

总结

以上所述是小编给大家介绍的javascript高仿热血传奇游戏实现代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
动态修改DOM 里面的 id 属性的弊端分析
Sep 03 Javascript
IE6-IE9不支持table.innerHTML的解决方法分享
Sep 14 Javascript
浅谈jQuery异步对象(XMLHttpRequest)
Nov 17 Javascript
JavaScript列表框listbox全选和反选的实现方法
Mar 18 Javascript
jQuery中 attr() 方法使用小结
May 03 Javascript
jQuery实现的调整表格行tr上下顺序
Jan 10 Javascript
浅谈JavaScript对象与继承
Jul 10 Javascript
原生js实现网页顶部自动下拉/收缩广告效果
Jan 20 Javascript
详解angularJS+Ionic移动端图片上传的解决办法
Sep 13 Javascript
微信小程序导航栏跟随滑动效果的实现代码
May 14 Javascript
Vue.js实现备忘录功能
Jun 26 Javascript
Vue执行方法,方法获取data值,设置data值,方法传值操作
Aug 05 Javascript
实现jquery放大镜的两种方法
Feb 22 #jQuery
JavaScript正则表达式函数总结(常用)
Feb 22 #Javascript
Node Puppeteer图像识别实现百度指数爬虫的示例
Feb 22 #Javascript
原生js调用json方法总结
Feb 22 #Javascript
babel的使用及安装配置教程
Feb 22 #Javascript
vue-cli中的babel配置文件.babelrc实例详解
Feb 22 #Javascript
利用adb shell和node.js实现抖音自动抢红包功能(推荐)
Feb 22 #Javascript
You might like
表单提交错误后返回内容消失问题的解决方法(PHP网站)
2015/10/20 PHP
php验证身份证号码正确性的函数
2016/07/20 PHP
php计算给定日期所在周的开始日期和结束日期示例
2017/02/06 PHP
阿里云Win2016安装Apache和PHP环境图文教程
2018/03/11 PHP
基于jquery+thickbox仿校内登录注册框
2010/06/07 Javascript
运用js教你轻松制作html音乐播放器
2020/04/17 Javascript
Javascript oop设计模式 面向对象编程简单实例介绍
2016/12/13 Javascript
BootStrap实现鼠标悬停下拉列表功能
2017/02/17 Javascript
jQuery插件imgAreaSelect基础讲解
2017/05/26 jQuery
如何使用less实现随机下雪动画详解
2019/01/02 Javascript
Vue运用transition实现过渡动画
2019/05/06 Javascript
如何让微信小程序页面之间的通信不再变困难
2019/06/03 Javascript
Layui实现数据表格默认全部显示(不要分页)
2019/10/26 Javascript
JS实现手写 forEach算法示例
2020/04/29 Javascript
python检查序列seq是否含有aset中项的方法
2015/06/30 Python
python妹子图简单爬虫实例
2015/07/07 Python
CentOS安装pillow报错的解决方法
2016/01/27 Python
python3.0 模拟用户登录,三次错误锁定的实例
2017/11/02 Python
Python 统计字数的思路详解
2018/05/08 Python
Python给定一个句子倒序输出单词以及字母的方法
2018/12/20 Python
Python3.6+Django2.0以上 xadmin站点的配置和使用教程图解
2019/06/04 Python
python中的列表与元组的使用
2019/08/08 Python
什么是Python变量作用域
2020/06/03 Python
python关于倒排列的知识点总结
2020/10/13 Python
Python+OpenCV图像处理——打印图片属性、设置存储路径、调用摄像头
2020/10/22 Python
python中的对数log函数表示及用法
2020/12/09 Python
Jmeter调用Python脚本实现参数互相传递的实现
2021/01/22 Python
10个最常见的HTML5面试题 附答案
2016/06/06 HTML / CSS
html5视频媒体标签video的使用方法及完整参数说明详解
2019/09/27 HTML / CSS
《千年梦圆在今朝》教学反思
2014/02/24 职场文书
元旦晚会主持词
2014/03/24 职场文书
《老山界》教学反思
2014/04/08 职场文书
党员领导干部民主生活会批评与自我批评发言
2014/09/28 职场文书
红楼梦读书笔记
2015/06/25 职场文书
光之国的四大叛徒:第一贝利亚导致宇宙毁灭,赛文奥特曼在榜
2022/03/18 日漫
Nginx的基本概念和原理
2022/03/21 Servers