最近关于JS开发游戏的一些体会 – 战斗的喵喵!

 

JS骨子里就是一个事件驱动的语言,所有用户代码的开始执行都可以看作是一个加载事件触发的,把一次事件的调用直到返回看作一个执行过程,这个执行过程是不可中断的(意思就是单线程),在执行过程中新的事件回调触发会将下一个过程加入执行队列。

这个特性让我们不用担心数据同步问题,但是闭包的存在让我们需要关注另一个问题,就是在一次执行过程中,一次函数调用产生的所有闭包都是同一环境下的,他们引用的闭包变量也都是同一个。比如有的人会遇到这样的问题:

for (var i = 0; i < 10; i++) {
setTimeout(
function () {
console.log(i);
},
0);
}

最后会输出10个”10″,因为for语句所在的执行过程是不可中断的,setTimeout的回调即使立即触发(虽然浏览器的实现是即使第二个参数为0也有最小的延时),触发的回调的执行过程也会加入执行队列,当for语句所在的执行过程结束后,依次执行后续的执行过程,但因为回调的闭包函数是同一个环境下生成的,所以i其实是同一个变量,它在循环结束后就已经变成10了,所以后续的10次回调都会输出”10″。这个问题很多人都已经知道了,这里说的”执行过程”,”环境”只是便于理解,追求本质的话可以去看解释器的实现,也许不同的解释器会有区别,不过现在这么理解可以说得通。

其实回调只是”传递函数引用并使用”这种方法的一种昵称,本质上还是函数调用,就像C有函数指针,C#有委托。如果是”传递对象并使用其中的方法”,可以看作是这种模式的一种扩大化,这种用法也有个昵称叫”多态”。

如果说回调可以通过改变函数引用(或者指针或者委托)来调用不同的方法,多态就是可以通过改变对象引用来调用不同的许多方法。不过由于对象还携带了数据,可能是其他的对象,通过嵌套、组合就可以实现更多灵活的模式。

C语言常用的是第一种,虽然可以用结构体和函数指针来达到第二种的效果,但是由于繁冗的初始化过程往往很少使用。Java常用的第二种,虽然可以用反射机制来实现第一种的效果,但因为太奇葩也鲜有耳闻。而我们可爱的杰·艾斯!两种方法都可以,因为杰·艾斯从头到脚都是面向对象的,甚至函数都是对象,两种方法都很自然。当然C#,C++之类的自然也可以,不过杰·艾斯还有更多的灵活性,我已经不想回首过去了。233……

说这些是为什么呢,在过去写游戏的过程中我体会到了一点,如果编写代码不加思考,很容易写出串行的流程。比如在碰撞检测中触发碰撞事件,碰撞事件中调用血量计算,血量计算中调用怪物死亡,怪物死亡中调用播放特效……如果想在一系列的执行过程中增加一个环节,就要涉及到前后两个环节,而一旦出现涉及的环节中存在分支时,复杂度就会陡然提升。解决这个问题的一个方法就是,状态机模式。

通常状态机模式的实现是一个状态机对象维护状态,每个状态有切入、切出、维持三种事件中的一个或多个。在Java这种语言中,由于前面所说的原因,每个状态都要用一个类对象或枚举对象(实际上还是类)来实现,所以采用状态机会使代码变得冗长。其他语言如果采用这一模式也一样。而在支持”回调”(这里指的是前述定义的回调)的语言中,状态事件可以直接是函数,这样就可以把状态事件的逻辑写在同一个类里。

于是我在游戏里使用了这种方式实现的状态机,最终用起来是这样的:

var MyClass = cc.Class.extend({
state:
null,
ctor:
function () {
this.state = new StateMachine();
this.state.register(‘state1’, this.onEnterState1, this.onLeaveState1, this.onRunState1);
this.state.register(‘state2’, this.onEnterState2, this.onLeaveState2, this.onRunState2);
},
onEnterState1:
function () {},
onLeaveState1:
function () {},
onRunState1:
function () {},

onEnterState2: function () {},
onLeaveState2:
function () {},
onRunState2:
function () {}
});

上面的代码使用的是Cocos-js的类继承方式,其实就是extend方法用我们传入的Object对象帮我们构造一个函数,设置它的prototype等,并且提供this._super()方法调用父类函数,我自己写过一个类似的,其他库或框架中也有类似实现,实际用起来其实也挺方便。

这样就存在一个问题,我要为每个状态写三个方法并注册,但当前类中可能并不需要其中的逻辑,但如果子类需要父类就要定义一个空的来供子类重写,于是代码又变得像Java的又臭又长(迦哇:怪我咯?)。虽然这个问题可以通过子类重新注册事件来解决,但实际写起来还是挺麻烦的,至少触发了我的强迫症属性了。

然而杰·艾斯早已看穿了一切。杰·艾斯与众不同的是他自然而然与生俱来的天然反射机制(听起来……咳咳),其实注册状态的时候可以不必传递事件回调的引用,只要稍加约束,所有的事件回调函数都使用相同规范的命名即可,即用”onEnter”,”onLeave”,”onRun”加上状态名称。然后把回调函数所在的对象传给StateMachine,这样register函数只要用状态名分别加上三个前缀,并用“[]”操作符去取得函数对象,如果函数存在就与状态关联起来,否则就忽略。这样子类中通过this._super()调用父类的构造函数时,其中调用register方法的时候,子类对象上如果有某一状态相应的回调函数,就会与这个状态关联起来。

于是我就开始愉快的重构了,虽然状态机模式看起来只是把一大块代码分成了许多块代码,但是在写回调的逻辑时只需要关心对应的状态就可以了,很多时候还省去了一些控制变量。这样改变流程就只需要改变状态就可以了,由于一个状态有三个事件,切入、切出、维持,原本流程切换要考虑的逻辑的一次执行,持续执行,最后执行就不需要变量来控制了。比如:

var MyClass = cc.Node.extend({
firstTick:
true, // 用于判断执行一次性的操作
timer: 0, // 计时器
interval: 10, // 间隔
onEnter: function () {
this.scheduleUpdate();
},
update:
function (dt) {
if (this.firstTick) {
this.firstTick = false;
// 一些一次性操作
}
this.timer += dt;
if (this.timer > interval) {
this.timer = 0;
// 达到时间间隔时的操作
}
else {
// 未达到时间间隔时的操作
}
}
});

上面的代码光是一种状态下的逻辑就已经很难看了,而且一不小心就会出现问题,如果增加一种状态,两种状态就可能偶合在一起,大大增加了出错的几率。

另外,这里timer的用法常常用到,但是写起来麻烦,而且不精确,因为dt(这里就叫它时间片吧)可能没有用完,比如this.timer + dt > interval,就会有(this.timer + dt – interval)的误差,如果dt始终不会过大不会有明显问题,但是如果游戏因为卡顿导致两帧间隔过长,就可能会出现较大的误差。不过这种情况是应该避免的,通常的做法是对dt进行限制,最大不能超过一定值(比如1/30),这样也可以减少出现穿越的情况。不过我还是决定封装一个Counter,用来计时或计数,暂时就不说了。

还有一点体会就是游戏对象的初始化,在《Game Programming Gems》第一册中开头就提到了游戏的数据驱动,把对象的特性、表现数据化,用数据来初始化对象。在我重构游戏代码以后重新划分了对象的继承关系,所有动态、可碰撞的东西继承自同一个基类,玩家和怪物都继承自一个角色类,BOSS继承自怪物类。但是之前设计的数据却不是这样的结构,怪物、BOSS和玩家角色都是在同一个表中,拥有相同的字段,只是各自有的没有用到。我能想到的初始化对象的方法有两种,一是构造函数或者成员函数中初始化,二是用工厂方法初始化。这两种方法的区别在于,前一种方法子类可以通过调用父类函数来初始化通用的数据,而工厂方法可能要分别写一遍。似乎前一种方法更好,但是如果采用前一种方法就等于认同了现在的数据的结构关系,或者说一旦数据的结构改变,比如玩家数据和怪物分离开,并增改一些字段,那么原来的初始化过程就不可靠了。虽然工厂方法也需要修改,但和对象继承关系的关系不大,一种本能告诉我这种方法代表不妥协(→_→)。不过在复制粘贴几次以后我终于决定两种方法混用,对于在同一表中存储数据且有继承关系的对象使用成员函数和函数重写初始化,并为每种对象提供工厂方法来隐藏对初始化方法的调用。其实我也不知道为什么这么做,本能告诉我这样我心里比较舒服,虽然我也不知道为啥……

今天就总结这么多。

本文链接:最近关于JS开发游戏的一些体会,转载请注明。



You must enable javascript to see captcha here!

Copyright © All Rights Reserved · Green Hope Theme by Sivan & schiy · Proudly powered by WordPress

无觅相关文章插件,快速提升流量