您的位置:奥门新浦京网址 > Wed前段 > 仙剑奇侠传的web移植版,不同Node版本导致的Dat

仙剑奇侠传的web移植版,不同Node版本导致的Dat

发布时间:2019-10-22 11:52编辑:Wed前段浏览(172)

    *为什么说对象组合能够避免脆弱基类问题

    要搞清楚这个问题,首先要知道脆弱基类是如何形成的:

    1. 假设有基类A
    2. B继承自基类A
    3. C继承自B
    4. D也继承自B

    C中调用super方法,该方法将执行类B中的代码。同样,B也调用super方法,该方法会执行A中的代码。

    CD需要从AB中继承一些无关联的特性。此时,D作为一个新用例,需要从A的初始化代码继承一些特性,这些特性与C的略有不同。为了应对以上需求,菜鸟开发人员会去调整A的初始化代码。于是乎,尽管D可以正常工作,但是C原本的特性被破坏了。

    上面这个例子中,ABCD提供各种特性。可是,CD不需要来自AB的所有特性,它们只是需要继承某些属性。但是,通过继承和调用super方法,你无法选择性地继承,只能全部继承:

    “面向对象语言的问题在于,子类会携带有父类所隐含的环境信息。你想要的是一个香蕉,但是最终到的却是一个拿着香蕉的大猩猩,以及整个丛林”——乔·阿姆斯特朗《编程人生》

    如果是使用对象组合的方式 设想有如下几个特性:

    JavaScript

    feat1, feat2, feat3, feat4

    1
    feat1, feat2, feat3, feat4

    C需要特性feat1feat3,而D 需要特性feat1, feat2, feat4

    JavaScript

    const C = compose(feat1, feat3); const D = compose(feat1, feat2, feat4);

    1
    2
    const C = compose(feat1, feat3);
    const D = compose(feat1, feat2, feat4);

    假如你发现D需要的特性与feat1**略有出入。这时候无需改动feat1只要创建一个feat1的定制化版本*,就可以做到保持feat2feat4特性的同时,也不会影响到C*,如下:

    JavaScript

    const D = compose(custom1, feat2, feat4);

    1
    const D = compose(custom1, feat2, feat4);

    像这样灵活的优点,是类继承方式所不具备的。因为子类在继承的时候,会连带着整个类继承结构

    这种情况下,要适应新的用例,要么复制现有类层划分(必然重复性问题),要么在现有类层结构的基础上进行重构,就又会导致脆弱基类问题

    而采用对象组合的话,这两个问题都将迎刃而解。

    问题总结

    回头看文章开头的用的日期构造函数导致的bug,就可以解释”1995-12-17T00:00:00″ 在低版本下输出1995-12-17T08:00:00,而高版本下输出1995-12-17T00:00:00的问题了。

    通过上述规范和源码,低版本由于会加默认偏移量Z,默认就解析成0时区的时间,而我们在东八区,所以最终我们本地的时间是1995-12-17T08:00:00,高版本下由于没有Z,默认会解析成本地时间,输出结果最终就是1995-12-17T00:00:00。

    问题解决方案就是只需要加上时间偏移量即可,如下new Date(‘1995-12-17T03:24:00+08:00’)。

    2.2. 这是什么程度的移植?

    原汁原味移植。h5pal从SDLPAL里平移(就是抄啦)了大量的代码。SDLPAL是一个基于SDL的跨平台版仙剑,它已经能顺利的运行在Windows、Linux、OS X、Symbian、PSP、Android等很多种平台上面。

    h5pal与SDLPAL有着相同的出发点,就是实现仙剑的主程序,你只需要有仙剑的资源文件就可以运行整个游戏。

    你真的了解原型了吗?

    采用先创建类和构造函数,然后再继承的方式,并不是正宗的原型继承,不过是使用原型来模拟类继承的方法罢了。这里有一些关于JavaScript中关于继承的常见误解,供君参考。

    JavaScript中,类继承模式历史悠久,而且建立在灵活丰富的原型继承特性之上(ES6以上的版本亦然)。可是一旦使用了类继承,就再也享受不到原型灵活强大的特性了。类继承的所有问题都将始终如影随形无法摆脱

    在JavaScript中使用类继承,是一种舍本逐末的行为。

    相关规范

    ISO8601标准[参考5]

    该标准指定了如果为指定偏移时间就默认为当前时间。

    图片 1

    [ES5 规范][参考6]

    指出了如果没有指定偏移量,默认偏移量为Z。

    图片 2

    [ES6 规范][参考7]

    为了和ISO8601标准一致,又对该规范做了更改,如果时区偏移量不存在,日期时间将被解释为本地时间。

    图片 3

    2.4. 使用了什么游戏引擎/框架/库/技术

    从思路上看的话,可以说使用了The-Best-JS-Game-Framework。

    最主要的,这个程序主要使用了co,使用co/yield/generator来改善异步开发的体验,让整个庞大的程序实现成为了可能——前言中说的去年的一次大重构就是干这个——这是一个非常重要的重构,过去的话一个异步的update/render loop就可以让人抓狂,以至于我现在根本不想再写异步的JS了T_T,也许有机会我会再写一篇文章来介绍JS“同步”编程以及js-csp这个非常好玩的东西。但你知道co其实是一个非常非常简单的库,所以即使没有co的话,自己造一个堪堪一用的轮子也非常容易,所以想解除这个依赖是很简单的。

    在这个坑之初,原生Promise还没普及,所以引入了q,但其实在整个项目中贯彻了co之后,很少用得着Promise,并且也可以很容易的向原生Promise迁移,当然因为懒我是没这么干的。

    其他方面可以说几乎没有依赖第三方的库了,可能还有jQuery啊这类的东西,只是用了一丁丁点,非常容易解除依赖。

    仙剑是一个很古老的游戏,使用现代游戏引擎重新实现仙剑的主程序并没有太直接的帮助。现代的2D游戏引擎围绕Sprite和场景管理为主,虽然在SDLPAL和h5pal中也有Sprite和场景模块,但具体到技术层面和现代游戏引擎里的还是差别比较大。再加上技(xīn)术(lǐ)洁(biàn)癖(tài)的原因,我没有用任何现代的游戏引擎,不过等到轮子造得差不多的时候,发现游戏引擎的思想果然是几十年没有太大变化……

    由于音乐和音效系统彻底坑了(原因见后文),所以WebAudio暂时不涉及。图形方面只涉及到canvas 2D,并且因为仙剑本身的资源都是像素级的,所以图形这一层也基本上都是在getImageData/putImageData的层次直接操作像素,并没有使用任何canvas的绘图API。因此如果后续把绘图层迁移到WebGL也会很简单,不过目前看来完全没有这个必要。

    h5pal使用GPLv3发布,我对开源协议几乎不懂,只知道GPL是比较严格的一种协议,而且SDLPAL是用GPLv3的,考虑到我抄了他很多代码,于是用了这个至少不比他宽松的协议,并且再次向SDLPAL表示敬意。

    Stamps:可组合式工厂函数

    多数情况下,对象组合是通过使用工厂函数来实现:工厂函数负责创建对象实例。如果工厂函数也可以组合呢?快查看Stamp文档找出答案吧。

    (译者注:感觉原文表达有些不尽兴。于是我自作主张地画了2个图便于读者理解。不足之处还请谅解和指正) 图片 4图:类继承

    说明:从图上可以直接看出单一继承关系、紧耦合以及层级分类的问题;其中,类8,只想继承五边形的属性,却得到了继承链上其它并不需要的属性——大猩猩/香蕉问题;类9只需要把五角星属性修改成四角形,导致需要修改基类1,从而影响整个继承树——脆弱基类/层级僵化问题;否则就需要为9新建基类——必然重复性问题。 图片 5图:原型继承/对象组合

    说明:采用原型继承/对象组合,可以避免复杂纵深的层级关系。当1需要四角星特性的时候,只需要组合新的特性即可,不会影响到其他实例。

    1 赞 8 收藏 评论

    图片 6

    深入分析

    结合问题,提炼出以下小示例,以供深入分析Date构造函数:

    JavaScript

    var d1 = new Date("1995/12/17 00:00:00"); var d2 = new Date("1995-12-17T00:00:00"); var d3 = new Date("1995-12-17T00:00:00Z"); console.log(d1.toString()); console.log(d2.toString()); console.log(d3.toString());

    1
    2
    3
    4
    5
    6
    var d1 = new Date("1995/12/17 00:00:00");  
    var d2 = new Date("1995-12-17T00:00:00");
    var d3 = new Date("1995-12-17T00:00:00Z");
    console.log(d1.toString());
    console.log(d2.toString());
    console.log(d3.toString());

    nodejs 10.3.0执行结果:

    JavaScript

    > console.log(d1.toString()); Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间) > console.log(d2.toString()); Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间) > console.log(d3.toString()); Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间)

    1
    2
    3
    4
    5
    6
    > console.log(d1.toString());
    Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间)
    > console.log(d2.toString());
    Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间)
    > console.log(d3.toString());
    Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间)

    nodejs 6.10.0执行结果:

    JavaScript

    > console.log(d1.toString()); Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间) > console.log(d2.toString()); Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间) > console.log(d3.toString()); Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间)

    1
    2
    3
    4
    5
    6
    > console.log(d1.toString());
    Sun Dec 17 1995 00:00:00 GMT+0800 (中国标准时间)
    > console.log(d2.toString());
    Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间)
    > console.log(d3.toString());
    Sun Dec 17 1995 08:00:00 GMT+0800 (中国标准时间)

    为什么在不同环境下Nodejs的解析行为不一样呢?这就要提下JS中涉及到时间的相关规范了。

    2.8. 有可能在手机上运行吗

    目前不可以,性能最好的iOS Safari尚未支持yield/generator,而Android Chrome我目前没有关注。

    性能方面没有明确的评价,在MacbookPro上CPU占用率并不高,但是内存很高(因为惨无人道的用内存,毫无优化之心),所以我觉得还是挺堪忧的。

    为什么搞清楚类继承和原型继承很重要?

    继承,本质上讲是一种代码重用机制——各种对象可以借此来共享代码。如果代码共享的方式选择不当,将会引发很多问题,如:

    使用类继承,会产生父-子对象分类的副作用

    这种类继承的层次划分体系,对于新用例将不可避免地出现问题。而且基类的过度派生,也会导致脆弱基类问题,其错误将难以修复。事实上,类继承会引发面向对象程序设计领域的诸多问题:

    • 紧耦合问题(在面向对象设计中,类继承是耦合最严重的一种设计),紧耦合还会引发另一个问题:
    • 脆弱基类问题
    • 层级僵化问题(新用例的出现,最终会使所有涉及到的继承层次上都出现问题)
    • 必然重复性问题(因为层级僵化,为了适应新用例,往往只能复制,而不能修改已有代码)
    • 大猩猩-香蕉问题(你想要的是一个香蕉,但是最终到的却是一个拿着香蕉的大猩猩,还有整个丛林)

    对于这些问题我曾做过深入探讨:“类继承已是明日黄花——探究基于原型的面向对象编程思想”

    “优先选择对象组合而不是类继承。” ~先驱四人,《设计模式:可复用面向对象软件之道》

    里面很好地总结了:

    问题排查

    按照一贯做法,出问题后先自己本地跑了一次测试用例,没有任何问题,初步就可以定位是开发环境问题。于是乎就看了下小伙伴nodejs版本号,版本号为6.10.0,而自己本地node版本号为10.3.0,于是在不同nodejs命令行下直接执行如下测试用例。

    JavaScript

    const defaultDate = new Date('1995-12-17T03:24:00'); console.log(defaultDate.toString());

    1
    2
    3
    const defaultDate = new Date('1995-12-17T03:24:00');
     
    console.log(defaultDate.toString());

    执行结果,

    Node 6.10.0:

    JavaScript

    > const defaultDate = new Date('1995-12-17T03:24:00') > console.log(defaultDate.toString()) Sun Dec 17 1995 11:24:00 GMT +0800(中国标准时间)

    1
    2
    3
    4
    > const defaultDate = new Date('1995-12-17T03:24:00')
    > console.log(defaultDate.toString())
     
    Sun Dec 17 1995 11:24:00 GMT +0800(中国标准时间)

    Node 10.3.0:

    JavaScript

    const defaultDate = new Date('1995-12-17T03:24:00') undefined console.log(defaultDatae.toString()) Sun Dec 17 1995 03:24:00 GMT+0800 (中国标准时间)

    1
    2
    3
    4
    const defaultDate = new Date('1995-12-17T03:24:00')
    undefined
    console.log(defaultDatae.toString())
    Sun Dec 17 1995 03:24:00 GMT+0800 (中国标准时间)

    到此基本确认了该问题是由Nodejs环境导致的问题。但是为什么会有这样的问题呢,跟着我继续深入探秘下Date构造函数。

    2.9. 所以总的完成度?

    直接搬GitHub上给(胡邹)的吧:

    模块 进度
    资源 90%
    读档 99%
    存档 40%
    Surface 90%
    位图 99%
    Sprite 99%
    地图 90%
    场景 90%
    调色盘 90%
    文本 99%
    脚本(天坑) 70%
    平常UI 90%
    战斗UI 90%
    战斗(天坑) 70%
    播片 90%
    结局 95%
    音乐 0%
    音效 0%

    三种不同的原型继承方式

    在深入探讨其他继承类型之前,还需要先仔细分析下我所说的类继承

    你可以在Codepen上找到并测试下这段示例程序

    BassAmp 继承自 GuitarAmp, ChannelStrip 继承自 BassAmpGuitarAmp。从这个例子我们可以看到面向对象设计发生问题的过程。ChannelStrip实际上并不是GuitarAmp的一种,而且它根本不需要一个cabinet的属性。一个比较好的解决办法是创建一个新的基类,供amps和strip来继承,但是这种方法依然有所局限。

    到最后,采用新建基类的策略也会失效。

    更好的办法就是通过类组合的方式,来继承那些真正需要的属性:

    修改后的代码

    认真看这段代码,你就会发现:通过对象组合,我们可以确切地保证对象可以按需继承。这一点是类继承模式不可能做到的。因为使用类继承的时候,子类会把需要的和不需要的属性统统继承过来。

    这时候你可能会问:“唔,是那么回事。可是这里头怎么没提到原型啊?”

    客官莫急,且听我一步步道来~首先你要知道,基于原型的面向对象设计方法总共有三种。

    1. 拼接继承: 是直接从一个对象拷贝属性到另一个对象的模式。被拷贝的原型通常被称为mixins。ES6为这个模式提供了一个方便的工具Object.assign()。在ES6之前,一般使用Underscore/Lodash提供的.extend(),或者 jQuery 中的$.extend(), 来实现。上面那个对象组合的例子,采用的就是拼接继承的方式。
    2. 原型代理:JavaScript中,一个对象可能包含一个指向原型的引用,该原型被称为代理。如果某个属性不存在于当前对象中,就会查找其代理原型。代理原型本身也会有自己的代理原型。这样就形成了一条原型链,沿着代理链向上查找,直到找到该属性,或者找到根代理Object.prototype为止。原型就是这样,通过使用new关键字来创建实例以及Constructor.prototype前后勾连成一条继承链。当然,也可以使用Object.create()来达到同样的目的,或者把它和拼接继承混用,从而可以把多个原型精简为单一代理,也可以做到在对象实例创建后继续扩展。
    3. 函数继承:在JavaScript中,任何函数都可以用来创建对象。如果一个函数既不是构造函数,也不是 class,它就被称为工厂函数。函数继承的工作原理是:由工厂函数创建对象,并向该对象直接添加属性,借此来扩展对象(使用拼接继承)。函数继承的概念最先由道格拉斯·克罗克福德提出,不过这种继承方式在JavaScript中却早已有之。

    这时候你会发现,拼接继承是JavaScript能够实现对象组合的秘诀,也使得原型代理和函数继承更加丰富多彩。

    多数人谈起JavaScript面向对象设计时,首先想到的都是原型代理。不过你看,可不仅仅只有原型代理。要取代类继承,原型代理还是得靠边站,对象组合才是主角

    参考

    [1]

    [2]

    [3]

    [4]

    [5]

    [6]

    [7]

    1 赞 1 收藏 评论

    图片 7

    3. 后记

    (呃,这个真的是流水账了,可能就长了)

    其实一开始让我发布h5pal的时候,我是拒绝的。因为我只想把它当做一个情怀的玩具,烂在自己的硬盘里面算了。而且心理洁癖造成我觉得没完成的东西就不要发布了吧。后来在@licstar的鞭策之下一点点推进,断断续续改了很多没头绪的BUG。突然有一天似乎流程能走通了(那时候还没实现战斗),而他竟然磕磕绊绊的就玩到通关了,我特么真是惊了,瞬间有种拨云见日的感觉。

    我知道即使发布了也估计没有人会用这个版本来玩,不过如标题所说,情怀之作。今年的仙剑6让很多玩家非常失望,而身为老仙剑迷的我其实从4代过后就已经弃坑了。尽管如此,我一直都认为如果想做一名合格的RPG玩家,从游戏评论的角度出发的话,仙剑1一定是必玩之作,因为在那个时候它是中文RPG游戏当中能和同期日系RPG有一战的一作,代表了当年RPG的最高水平,可以称为游戏发展史上的一个标志。选择仙剑很大一部分原因当然是有SDLPAL这个现成的对象可以抄,不过情怀满分这一点也是其他游戏不可取代的。

    我是一名游戏爱好者,也一直想着能做游戏,并且是想做出版级的“大”游戏。不过因为各种原因,似乎离这个目标越来越远了。其实游戏是一个非常大也非常复杂的软件工程,甚至有人说游戏是软件工程当中最难的一个分支。我一直非常佩服各种3A大厂,能够集结上千人,几千万美元的资金做出一部部牛逼的作品(每打通一个游戏我都要把制作群字幕看完),也非常佩服各路独立游戏神人,能在那么有限的资源下做出精彩的作品。虽然仙剑不是新IP,我想我也不太有可能做新IP,甚至说没有SDLPAL和PalResearch的基础的话也不可能做出h5pal,不过这也已经在很大程度上满足了我做游戏的梦想吧,能做到现在这个程度我还是很开心的。

    至于为什么是用HTML5/JS来实现呢?首先我本职是做前端的,对JS是非常熟悉,也可以当练手用呗(虽然整个h5pal的JS代码几乎没有任何技术难度可言吧……)其次就是因为SDLPAL本身已经做到跨很多很多平台了,惟独web这个炙手可热的平台还是个空缺。我在网上也没有找到仙剑1的完整web移植。另一方面,因为有别的一些老游戏的web移植中有很多(比如Diablo、星际)只是伪移植,也就是用原版游戏资源解包以后在web上做一个demo,根本没法玩的,这一点坚定了我做完整移植和资源文件不进行预处理的目标。

    最大的遗憾也是留下了音频这个无底天坑,因为仙剑1的经典的配乐很得人心,没有音乐的伴随,即使体验剧情也会觉得少了太多味道,可惜可惜。

    h5pal里面实现了一个用来读取C结构体指针的库,C里面通过指针转换,从文件里读取一段字节直接“铺开内存”就能转成一个结构体,这一点非常好用。这个JS库能把ArrayBuffer直接转成JS对象,利用getter/setter可以把对字段的操作落在ArrayBuffer(JS里的字节数组)上,这样一来还可以让不同对象共享内存(比如实现一个union什么的),在h5pal里是一个很核心的库了(重构的时候也是血虐啊)。我觉得还挺方便的,也许用在nodejs里的话实现一些native互访以及网络协议的时候会用得着吧。以后有时间的话可能会考虑把它重构一下,API弄弄更易用了单独发布一个库吧(有生之年

    最后感谢@licstar的鞭策(催)和积极的帮忙测试,如果不是这么催的话估计早就烂硬盘里了。

    最后的最后,我才发现仙剑里的女生都很积极主动啊,有的地方甚至还挺毁三观的……

    1 赞 收藏 1 评论

    图片 8

    本文由奥门新浦京网址发布于Wed前段,转载请注明出处:仙剑奇侠传的web移植版,不同Node版本导致的Dat

    关键词:

上一篇:通晓SVG坐标种类和转移,transform坐标转变

下一篇:没有了