日志 教程
当前位置: 教程  >  游戏开发  >  FLASH游戏开发  >  正文

Flash游戏制作:游戏开发中的错误之源

作者:宋姗姗 发表于 2011/6/13 18:45:18     评论(0)     阅读(3201)     
关于方法的讨论是无止境的,世界上并没有最好的方法,同一个方法的效果也取决于各人对方法的理解。下面的观点仅仅是本人一家之见,如果有不同见解大家讨论。
正如每个初生婴儿一样,大多数程序呱呱落地的时候是简单而可爱的。然而随着代码一行一行增加,程序开始变得敏感,性格古怪,脾气暴躁,不断出现出乎意料的事情;每一个轻微的修改都可能导致大面积崩溃——开始你会试图安抚它,然而最后你会抱着头大哭“我受够这该死的东西了!”
一个程序的bug为什么层出不穷呢?其根源在于——问题过于复杂。复杂的问题带来长的代码,他们是从最初代码基础上慢慢增改出来的。这自然而然会带来这样几个问题:
代码过长过于复杂--各个部分会产生交叉影响。程序越大,一个改动可能影响的范围就越广。
代码冗余--导致严重的维护困难。比如一旦你修改数据结构,那么你必须重复修改50种敌人身上的代码。
结构不合理--难于实现当前设计要求。于是程序员不得不绕弯路,产生额外的复杂性。
什么是好的代码?

有一个最简单的标准:最短、最清晰的代码就是最好的代码。
一个好的开端:写最少的代码

任何时候只写最有用的代码——他们应该是马上就能用得到得。
有些人着迷于结构和语言的优雅性,一开始的时候就设计很多东西:一开始就试图继承、封装,构建类关系图、写出大量的get/set属性——然而这些早期设计往往很快被证明是不适合系统的。如果你没有做过你目前做的系统,你不会知道它真正需要什么。所以,任何时候只为你需要的东西编写代码。

不要过早设计接口——除非你马上要用到它。
不要过早考虑代码重用——除非你已经多次使用这段代码。
早期设计应当详细到这样的程度——每个细节在完全可预见范围内是有用的。

当你需要添加新功能的时候,就扩展和复制已经有的代码:比如,需要添加一个新的动作,便添加新动作和代码;要添加一个新的敌人,就复制一份敌人,然后修改。
这样一步一步做下去,不久前述3个问题就会浮出水面:代码复杂、冗余和结构不合理。下面我依次告诉大家对付这些问题的思路。注意:任何解决方案都需要重写出问题那一部分代码。
对付交叉影响——工序分离

在不断的复杂化之下,可能需要40多个变量,500多行代码完成一个功能。然而人的处理能力是有限的——需要同时记忆过多的变量名本身就是一种负担,再加上他们之间错综复杂的关系....... 这个时候就应该从那500行代码中分出多个子工序来。把长的代码分隔成尽量不相关的段代码,定义之间的接口,隐藏复杂性。比如一个ACT主角身上的代码原来是这样的构架:
1 移动部分(包括双击的键盘响应、根据角色状态判断是否移动、根据地图确定移动状态修改状态、地图控制、角色动画)
2 出招部分(按下事件的键盘响应、根据角色状态判断是否出招和招式、修改状态、角色动画)
3 跳跃部分(按下事件的键盘响应、根据状态判断是否跳跃、修改状态、角色动画)
4 攻击、被攻击判定部分(决定是否命中、角色动画)

如果你一步一步添加功能,那么代码看起来就会是这个样子的:每一个功能(移动、出招、跳跃等等)都是分开实现的,他们各自的代码都很集中。然而问题在于——他们太长了,每一个部分都产生大量的状态——而且如同烂泥面条一般相互搅合在一起。比如跳跃攻击落地时不显示落地动画——从跳跃状态影响到了攻击部分代码,然后影响动画。

你可以修改成这样的方案,他将每个功能拆散,变成一道流水线:
1 键盘输入 -> 角色操作指令
2 一个状态机,接受角色指令,产生新的状态
3 攻击判定部分,产生状态
4 根据状态显示角色动画 这个方案把输入、核心和显示分开了。1 2 3 4 (2 3是同级的,然而他们交叉影响比较小,一个被攻击硬直状态即可)每一级只依赖上一级的输入,复杂程度大大减小了。另外一个好处就是,当你再次需要分离某一部分工序的时候,你只需要重写一部分代码,其它部分不需要改变。
冗余的解决方案——复用代码

很多地方都会产生冗余:比如STG大多数敌人越界都要删除。在大量位置出现重复的代码段,这就是一个冗余。
冗余的害处在于难于维护:原来所有敌人放到一个"敌人层MC"中,所以它的删除代码是这样的:
//sample 1
removeMovieClip(this);
后来发现设计需要,敌人需要和主角进行深度排序,必须与主角放到同一MC中,不再可以用"敌人层MC"区分,于是改成了加载时给定一个id,丢入一个enemyList堆,删除时从堆中删掉自己:
//sample 2
delete( _parent.enemyList[this.id])
removeMovieClip(this);
问题是...你需要找到所有的敌人并且修改这段AS。当数据结构再次变化,敌人需要在死亡的时候注销自己拥有的其它资源的时候,可能又变成这个样子:
//sample 3
for(var i in this.picList) _parent.releasePic( this.picList[i] );
delete( _parent.enemyList[this.id])
removeMovieClip(this);

这只是九牛一毛。一点点的改动需要动到所有的东西。一个地方忘了改,说不定就引起工作集泄漏。

想象一下如下情况:从simple1 -> simple2,忘记修改某一个敌人(姑且称为敌人S)的as,结果造成什么后果呢?游戏看上去正确无比——所有敌人死掉的时候在画面上确实被删除了,而且由于没有了MC,攻击判定自然也不会存在。但是每当错误的删除一个敌人S的时候,enemyList就会悄悄的产生一个数字的泄漏。最终enemyList将出现大量非法值,空耗CPU时间,于是游戏越来越慢。大家可以想像一下定位这个bug的难度——从你觉得一个飞机游戏速度似乎会悄悄变慢,到发现某一个敌人没有正确删除自己(其它敌人完全正常)需要多长时间?


解决方案: 1 过程分离
把重复的部分定义成函数、类以便重用。这样每次修改只需要修改那个对应的函数/对象的代码就可以了。

2 稍微讨论一下OOP
有些东西有明显的自然继承特点。这些东西适合OOP。
比如,可以这样设计一个游戏:所有敌人都需要添加、删除自己,有些敌人是单个活动块组成,而有些敌人比如BOSS是由多个活动块组成。 Enemy是基类,它管理一个全局enmeyList,并且调用删除接口。 Sprite extend Enemy,构造的时候只需要知道图片名,有自己一系列控制方法。 HugeEnemy extend Enemy,构造的时候需要给定一个图片数组,并且有一系列控制的方法。 那么可以构架出这样的树:
Enemy
/ \
Sprite HugeEnemy
/ \ / \
各个可实例化类各个可实例化类在这些设计中,同样要反复问自己:我是否过度设计了?代码是否最短、最少?正确的OOP可以缩短代码长度:他会减少冗余。
结构何时会令人失望?

当你实现代码的时候,你会不断发现原来设计的缺陷。有时候缺陷非常大——大到你无法完成设计——你只能选择放弃其中一部分设计,或者重新构建结构。什么时候抛弃原来的设计?感觉新的设计在思路中已经完善到可以摸的到的时候。修改结构是一个大工程,上面提到的分工序能够有效的分离出代码,这样你只需要修改其中的一部分。

评论
显示
悄悄话
汇众教育官网 | 联系方式 | 版权声明 | 友情链接
Copyright 2008© 汇众益智(北京)教育科技有限公司. All Rights Reserved
京ICP备09092043号 京公网安备11010802009023号