主流的主流游戏引擎擎开发软件的脚本环境中,start和update等生命周期回调函数在运行中作用与两者关系是什么。

多图,注意流量. -------------------------------- 买几本好书,坐电脑前不停的写,废寝忘食的写,编程是世界上最容易的事,你还不用碰底层,先学学基本流程控制和调用API,然后理解下分层思想,这就能写脚本了,然后有兴趣有时间就研究底层,没兴趣就弄好一…

}

本文为引擎提炼第二次迭代的下篇,将会完成引擎中动画、集合和事件管理相关类的重构。

1、提高引擎的通用性,完善引擎框架。
2、对应修改炸弹人游戏。

一组图片(或一个图的不同位置)在同一位置以一定的时间间隔显示,就形成了动画。

我们可以将精灵的动画序列图合为一张大的图片,称之为精灵图片。
如炸弹人的精灵图片如下图所示:

动画的一“帧”指动画序列图片中的一张图片,如下图红框就是往左移动动画的一帧:

帧数据指帧图片左上顶点在精灵图片中的横轴、纵轴坐标x、y以及帧图片的宽width和高height。

精灵的动画数据定义在炸弹人精灵数据SpriteData中,动画的帧数据定义在炸弹人FrameData中。
下面介绍当前的创建动画和播放动画的机制。

创建精灵类实例时会注入炸弹人精灵数据SpriteData,从而获得动画数据,序列图如下所示:

在initData方法中,炸弹人Scene会创建精灵类实例,将封装了精灵图片的引擎Bitmap实例和精灵类数据SpriteData注入到实例中。

//获得包含精灵图片对象的bitmap实例

SpriteData定义了精灵的动画数据,其中一个动画对应一个引擎Animation实例,并注入了使用getFrames方法获得的帧数据FrameData。

//只有一帧的动画帧数据没有duration属性

当玩家按下W键时,炸弹人会播放向上走动画,序列图如下所示:

1、玩家按下移动方向键后,游戏会设置要播放的动画

//判断玩家是否按下了W键

2、主循环更新动画帧,播放动画的当前帧
draw方法负责绘制精灵,炸弹人PlayerSprite会在draw方法中访问引擎Sprite的currentAnim属性,获得当前动画实例(引擎Animation实例),调用它的getCurrentFrame方法,获得当前帧数据,然后结合bimap实例的img属性(精灵图片对象),绘制动画当前帧图片。

 //游戏主循环调用的方法
 


//从bitmap.img中获得精灵图片对象,绘制动画当前帧

当前设计有四个地方可以修改:
1、可提取帧数据FrameData的通用模式
炸弹人FrameData中每帧的数据都具有相同的结构,都包括帧在精灵图片中的位置属性x、y和帧的大小属性width、height以及帧播放时间duration属性:

//只有一帧的动画帧数据没有duration属性,其duration属性值可以看为0
 
可以将通用模式抽象出来,提出引擎Frame类,封装帧数据,提供帧操作的方法。
引擎Frame为实体类,一个Frame对应动画的一帧。


2、引擎Animation的职责不单一
现在引擎Animation既要负责帧数据的保存,又要负责帧的管理:
引擎Animation

//如果没有duration属性(表示只有一帧),则返回 //判断当前帧是否播放完成, //当前是最后一帧,则播放第0帧 //增加当前帧的已播放时间.
可将其分解为Animation、Animate 、AnimationFrame三个类:
Animation为动画容器类,负责保存一个动画的所有帧数据。
Animate为帧管理类,负责管理一个动画的所有帧。
AnimationFrame为精灵动画容器类,负责保存精灵所有的动画。





3、引擎应该封装动画,提供操作API
动画操作属于精灵的基本操作,应该由引擎来负责动画的管理,向用户提供操作动画的API。


4、修改用户创建动画的方式
提取了新的引擎动画类后,需要修改用户代码中创建动画的方式。
目前有两种方式:
(1)初始化方式跟之前一样,只是对应修改炸弹人SpriteData和FrameData,将创建引擎Animation改为创建引擎Frame和Animate实例。
(2)直接在炸弹人精灵类中创建动画,删除炸弹人FrameData,删除炸弹人SpriteData的动画数据。


考虑到这只是修改用户代码,跟引擎没有关系,因此为了节省时间来,我选择第2种方式,具体使用引擎时用户可以自行决定初始化后动画的方式。


初步设计的引擎领域模型
现在给出通过分析后设计的引擎领域模型:


 
依次实施“分析问题”中提出的解决方案。
 
首先来看下炸弹人FrameData中一帧数据的组成:
x、y为帧图片左上顶点在精灵图片中的位置,width、height为帧图片的大小,duration为每帧播放的时间。
因为一个动画中的所有帧的播放时间应该都是一样的,只是不同动画的帧播放时间不同,因此duration不应该放在引擎Frame中,而应该放到保存动画帧数据的Animation中。
 
目前动画的每帧都是从精灵的精灵图片中“切”出来的,而精灵图片保存在引擎Bitmap实例中,在创建精灵类实例时注入。
一个精灵只有一个bitmap实例,即一个精灵只对应1张精灵图片。
这样的设计导致精灵的所有动画的所有帧都只能来自同1张精灵图片,无法实现不同的动画的帧来自不同的精灵图片的需求。
因此,可以将精灵图片对象的引用保存到Frame中,从而一个Frame对应1张精灵图片。这样既可以实现每个动画对应不同的精灵图片,还可以实现每个动画的每帧对应不同的精灵图片,从而实现最大的灵活性。
现在可以这样创建引擎Frame实例:
 
此处传入create方法的参数过多,可以将后面与帧相关的4个数据提取为对象:
其中rect方法在新增的几何类Geometry中定义,负责将矩形区域的数据封装为对象。


//保存精灵图片对象的引用
  • 改造Animation为动画容器类,负责保存一个动画的所有帧数据。
 
//保存每帧的播放时间
  • 提出Animate,负责管理动画的所有帧
 
因为引擎Animate需要保存一个动画的所有帧,所以继承Collection,获得集合特性:
引擎Animate //判断当前帧是否播放完成, //当前是最后一帧,则播放第0帧 //增加当前帧的已播放时间.
 
因为引擎AnimationFrame需要保存多个动画,所以应该继承引擎Hash类,以动画名为key,动画实例为value的形式保存动画:
引擎AnimationFrame //提供操作动画API //加入动画时初始化动画
 

3、引擎封装动画,提供操作API
  • 应该由引擎Sprite提供addAnim等操作动画API,还是暴露引擎AnimationFrame实例给用户,用户直接访问它的操作动画API?
 
如果由引擎Sprite提供操作动画API,那它的示例代码为:
这种方式有下面的好处:
(1)对用户隐藏了AnimationFrame,减小了用户负担。
(2)增加了1层封装,可以更灵活地插入Sprite的逻辑。
但是也有缺点,如果AnimationFrame增加操作动画的API,则Sprite也要对应增加这些API,这样会增加Sprite的复杂度。
考虑到:
(1)Sprite提供的操作动画的API只是中间者方法,没有自己的逻辑。
(2)现在操作动画的API太少了,以后会不断增加。
因此目前来看,直接将AnimationFrame暴露给用户更加合适。

用户可这样调用动画操作API:
 
在“播放动画”中可以看到,用户参与了更新动画帧的机制,实现了绘制动画当前帧的逻辑:
(1)炸弹人CharacterLayer调用了炸弹人PlayerSprite的update方法(由引擎Sprite实现)。
炸弹人CharacterLayer
//从bitmap.img中获得精灵图片对象,绘制动画当前帧
这些属于底层机制和逻辑,应该由引擎负责。
因此,将其封装到引擎中。
具体来说引擎需要进行下面两个修改:
(1)封装“更新动画帧”的机制
引擎Layer增加update方法,在主循环中调用该方法:
引擎Layer的update方法负责调用层中所有精灵的update方法,而精灵update方法又调用引擎Animate的update方法,从而实现主循环中更新动画帧的机制。

 //游戏主循环调用的方法
 






(2)实现“绘制动画当前帧”逻辑
将炸弹人MoveSprite实现的“绘制动画当前帧”的逻辑提到引擎Sprite中。
由于不是所有的炸弹人精灵类的绘制逻辑都为该逻辑(如炸弹人MapElementSprite没有动画,不需要绘制动画帧,而是直接绘制图片对象),所以将提取的方法命名为drawCurrentFrame,与draw方法共存,供用户自行选择:
引擎Sprite

//保留绘制精灵图片对象方法


  • 清理引擎包含的用户逻辑
 
引擎Sprite包含的“设置精灵的初始动画” 逻辑属于用户逻辑,应该由用户类负责。
因此将引擎Sprite的defaultAnimId属性移到炸弹人MoveSprite中。
炸弹人MoveSprite

4、在炸弹人精灵类中创建动画,删除炸弹人FrameData,删除炸弹人SpriteData的动画数据。
修改炸弹人创建动画的方式,在炸弹人PlayerSprite和EnemySprite中创建动画,加入到精灵类的引擎AnimationFrame实例中,并设置精灵的默认动画。
炸弹人PlayerSprite //创建帧,传入精灵图片对象和帧图片区域大小数据 //创建动画帧数组,加入动画的帧 //创建动画,设置动画的持续时间

 

 

重构后的播放动画序列图

 
 

 
至少还有下面几点需要进一步修改:
1、引擎应该提供将动画数据和动画逻辑分离的方式,提供创建动画的高层API。
引擎应该定义动画数据格式,封装创建动画的逻辑,用户可以按照引擎定义的数据格式,将动画数据分离到单独的文件中,调用高层API读取动画数据并创建动画。
2、引擎应该增加更多的动画操作API,如增加开始动画、结束动画等。
现在需要停下来,回顾一下之前的重构,查找并解决遗漏的问题。

使用YE.rect方法重构炸弹人矩形区域数据为对象

 
 

 
在“修改动画”的重构中,提出了Geometry类,该类有YE.rect方法,负责将矩形区域的数据封装为对象。
引擎Geometry
炸弹人类中除了帧数据,还有其它的矩形区域数据。如炸弹人游戏中碰撞检测的数据:
炸弹人BombSprite

 //获得精灵的碰撞区域,
 

 
可以用YE.rect方法将矩形区域数据定义为对象。

 


 //根据rect的数据结构对应修改
 

封装引擎Sprite的位置属性x、y,提供操作API

 
 

 
用户创建精灵实例时传入精灵初始坐标:
引擎Sprite
用户可直接操作精灵的坐标属性x、y:
引擎Sprite

 
  • 应该封装引擎Sprite的位置属性x、y,向用户提供操作坐标的API。
 
这是因为:
1、便于以后扩展,在API中加入引擎Sprite的逻辑。
如可以在API中增加权限控制等逻辑。
2、引擎Sprite的坐标属性名为“x”、“y”,容易与其它object对象的属性名同名,影响可读性,相互干扰。
如引擎Sprite的getCellPosition方法返回包含x、y属性的方格坐标对象:
 //获得坐标对应的方格坐标(向下取值)
 
用户容易将该方法返回的坐标对象与精灵坐标混淆,从而误操作。

  • 用户应该使用操作坐标的方法来设置精灵的初始坐标,不应该在创建精灵实例时传入初始坐标。
 
因为这样可以:
1、增加灵活性
坐标与精灵改为关联关系,可以不强制用户在创建精灵实例时设置初始坐标,从而用户可自行决定何时设置。
2、减少复杂度
简化引擎Sprite的构造函数。

 


此处仅给出部分炸弹人类的对应修改:
引擎MoveSprite

 

 

现在炸弹人可以直接操作它来获得精灵图片的相关数据。
如炸弹人PlayerSprite可访问bitmap属性的img属性来获得精灵图片对象:
炸弹人PlayerSprite

 
应该封装引擎Sprite的bitmap属性,向用户提供操作bitmap的API,这样用户就不需要知道bitmap的存在,减少用户负担。
因为精灵图片与精灵属于组合关系,应该在创建精灵时就设置精灵图片,所以保留引擎Sprite构造函数中传入bitmap实例的设计。
引擎Sprite应该增加setBitmap和setImg方法,满足用户更改精灵图片的需求。

 

 

 
引擎Sprite的getCellPosition方法负责将精灵坐标转换为炸弹人游戏中使用的方格坐标:
引擎Sprite
 //获得坐标对应的方格坐标
 

 
该方法的逻辑与具体的游戏相关,属于用户逻辑,应该由用户实现。

 
 //获得坐标对应的方格坐标(向下取值)
 

 

 
在第一次迭代中,为了解除引擎和炸弹人Config的依赖,提出了引擎Config。
引擎Config

 
因为:
(1)删除了引擎Sprite的getCellPosition方法后,引擎不再依赖引擎Config类了。
(2)引擎Config应该放置与引擎相关的配置属性,而现在放置的配置属性“方格大小”和“画布大小”均属于用户逻辑。
所以应该删除引擎Config。

 

引擎类不应该依赖引擎collision

 
 

 

 
引擎collision为碰撞算法类,与游戏相关,应该只供用户使用,引擎Sprite不应该依赖引擎collision。

 
需要进行下面的重构:
1、删除引擎Sprite的getCollideRect方法
现在炸弹人和引擎均没有用到引擎Sprite的getCollideRect方法,故将其删除。
2、引擎collision的getCollideRect方法改为私有方法
完成第一个重构后,炸弹人和引擎都不会用到引擎collision的getCollideRect方法,因此将其设为私有方法。

领域模型
重构后引擎collision只供用户使用了:

 

 

引擎Scene实现了遍历集合的逻辑:

由于炸弹人BombLayer要在遍历集合时加入判断逻辑,不能直接使用引擎Layer的P_iterator方法,所以它调用引擎Collection的迭代器方法,实现了“遍历集合”的逻辑:
炸弹人BombLayer
//执行集合元素的setDir方法

 
当前设计有下面几个问题:
1、引擎Hash没有实现“遍历集合”的逻辑,导致需要继承Hash的引擎Scene自己实现。
2、引擎Layer封装的外观方法P_iterator不灵活,导致炸弹人BombLayer不能直接使用,只能调用引擎Collection的迭代器方法来实现。
因为:
1、“遍历集合”的逻辑与引擎集合类相关,应该统一由它们实现。
2、引擎Collection的迭代器方法属于实现“遍历集合”的底层方法,应该作为Collection的内部方法。外界只需要调用“遍历集合”的外观方法即可,不需要了解该方法是如何实现的。
3、用户不应该自己实现“遍历集合”的逻辑。
所以:
1、应该由引擎集合类统一实现“遍历集合”逻辑。
2、引擎集合类提供改进后的“遍历集合”的外观方法,隐藏引擎Collection的迭代器方法,其它引擎类和用户类直接调用外观方法即可。

 
引擎Collection删除迭代器接口,将迭代器方法设为私有方法,实现遍历集合的外观方法iterator:
引擎Collection //改进设计,handler既可以为方法名,又可以为回调函数,这样炸弹人BombLayer在遍历集合时就可以通过传入自定义的回调函数来加入判断逻辑了





 

 


引擎Layer通过继承引擎Collection来获得集合特性:
引擎Scene通过继承引擎Hash来获得集合特性:

 
回顾在“”中,进行了继承复用集合类Collection的设计。当时这样设计的原因如下:

1、通过继承来复用比起委托来说更方便和优雅,可以减少代码量。
2、从概念上来说,Collection和Layer都是属于集合类,应该属于一个类族。Collection是从Layer中提炼出来的,它是集合类的共性,因此Collection作为父类,Layer作为子类。

 
然而现在这个设计已经不合适了,因为:
1、这只适用于整体具有集合特性的情况,不适用于局部具有集合特性的情况。
如引擎Animate只是功能类,不是集合类,只是因为要操作动画的帧数据,需要一个内部容器来保存帧数据。
当前设计是让引擎Animate继承集合类Collection,造成整体与Collection耦合,只要两者有一个修改了,另外一个就可能受到影响。
更好的设计是Animate增加私有属性frames,它为Collection的实例。这样Animate就只有该属性与Collection耦合,当Animate的其它属性和方法修改时,不会影响到Collection;Collection修改时,也只会影响到Animate的frames属性。从而把影响减到了最小。
2、如果几个有集合特性的引擎类同属于一个类族,需要继承某个父类时,则会有冲突。
因为它们已经继承了集合类了,不能再继承另一个父类。
因此,将引擎Collection、Hash改成类,改为组合复用的方式来使用。

 
引擎Hash修改为类,增加create方法
//改为通过_layers来进行集合操作




用户可指定绑定事件的对象target和处理方法的上下文handlerContext。

 
用户只能绑定全局事件,处理方法的this只能指向window:
引擎EventManager

 
实际的游戏开发中,用户不仅需要绑定全局事件,还需要绑定具体dom的事件,而且也可能需要指定事件处理方法的this。
因此,修改为由用户传入绑定事件的对象target和处理方法的上下文handlerContext。

 
//绑定事件到用户指定的target,默认为绑定全局事件

 


所有的引擎类已经重构完毕,我们需要站在整个引擎的层面进行进一步的重构。

 

将引擎依赖的jsExtend库引入到引擎中

 
 
现在引擎依赖了YOOP和jsExtend库。
根据引擎设计原则“尽可能少地依赖外部文件”,考虑到jsExtend是我开发的、没有发布的库,可以将其引入,作为引擎的内部库。
而YOOP是我开发的、独立发布库,引擎不应该引入该库。
划分引擎文件结构 现在所有引擎文件均在一个文件夹yEngine2D中,不方便维护,应该划分引擎包,每个包对应一个文件夹,把引擎文件移到对应包的文件夹中。
划分后的包图
划分后的文件结构

其中import文件夹放置引擎的内部库。

 

引擎类的私有和保护成员加上引擎专有前缀“ye_”

 
 

 
引擎类的私有和保护成员没有专门的前缀。
如引擎Sprite

 
为了防止继承引擎类的用户类的私有和保护成员与引擎类成员同名,可继承重写的引擎类(如引擎Sprite)的私有和保护成员需要加上“ye_”前缀。
另外,为了统一引擎类的成员命名,所有的引擎类的私有和保护成员都应该加上该前缀。
然而目前引擎类没有保护成员,因此只对引擎类私有成员加上前缀。

 

用户可直接创建抽象引擎类Scene、Layer、Sprite的实例

 
 

 

 
这几个类为抽象类,照理来说不能创建自身实例,但是为了减少用户负担,用户应该在没有自己的逻辑时,直接复用这几个抽象引擎类,创建它们的实例。

 



如果用户想要创建一个没有用户逻辑的精灵类,可以直接创建引擎Sprite的实例: //可以直接使用引擎Sprite自带的方法

将引擎类闭包中需要用于单元测试的内部成员设为引擎类的静态成员

 
 

 
现在常量、枚举值等是作为引擎类闭包的内部成员:
如引擎Director

 
单元测试需要操作闭包的内部成员,但是现在不能直接访问到它们,只能绕个弯,在引擎类中增加操作内部成员的测试方法,然后在单元测试中通过这些方法来操作闭包内部成员:
如引擎Director

在产品代码中增加了测试代码,这是个坏味道,应该只有测试代码知道产品代码,而产品代码不知道测试代码。
因此,将引擎类闭包中需要用于单元测试的内部成员设为引擎类的静态成员。
另外,因为静态成员不会被子类继承和覆盖,所以静态私有和保护成员不需要加上引擎专有前缀“ye_”。

 
将引擎类闭包中需要用于单元测试的内部成员设为引擎类的静态成员。
//静态方法中可通过this直接访问静态成员
引擎Layer将画布状态枚举值State设为静态变量

此处省略了炸弹人中与引擎类无关的类。

 
划分的包与文件结构对应:

 
 
 
     
     
     
     
  • 放置游戏动画的相关类。
  •  
     
     
     
     
     
  • 负责游戏资源的加载和管理。
  •  
     
     
  • 放置引擎的基础结构类。
  •  
     
     
     
     
     
  • 放置引擎通用的方法类。
  •  
     
     
     
     
     
     
     
     
     
     
    经过第二次迭代,基本消除了引擎包含的用户逻辑,从而能够在其它游戏中使用该引擎了。
    不过这只是刚开始而已,引擎还有很多待重构点,引擎的设计和功能也很不完善,相关的配套工具也没有建立,还需要应用到实际的游戏开发中,不断地修改引擎,加深对引擎的理解。


}

我要回帖

更多关于 主流游戏引擎 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信