Python游戏引擎开发(五):动画研究
接下来让我们来研究下动画,其实这个动画就是一个Sprite+Bitmap的结合体。不知道什么是Sprite和Bitmap就看Python游戏引擎开发系列中的文章。
动画的原理
一般而言,我们的动画是用的这样一种图片:
播放动画的时候,像播放电影一样,这张图就是胶卷。我们可以弄一个放映机,放映机的镜头大小就是每个动作小图的大小。如果我们的胶卷不停地移动,那么就会连成动画,如下图:
如何实现这个效果呢?我们在第三章中学到了如何显示图片,其中提到了BitmapData
类(不懂?良辰劝你去读读前几章),这个类中有个两个方法:setCoordinate
和setProperty
用于设置图片显示的位置和大小:
1 2 | bmpd.setCoordinate(x, y) bmpd.setProperty(x, y, width, height) |
参数图解如下:
Bitmap
图片显示对象,其中包含了一个BitmapData
对象,我们通过调用这个对象的上述两个方法,就能实现动画播放。不过到此好像还是少了什么?也许你会问,动画是个连续的过程,且每帧动画之间需要间隔一点时间,是不是少了一个计时器?是的,是的,是的,重要的事情说三遍,我们的确少了一个计时器类似物。不过别急。大家还记得第三章中提到的显示对象的_show
方法吗?这个方法是在窗口的paintEvent
中被调用的,paintEvent
又是在一个计时器中被调用的(涉及第二章内容)。等等……计时器……所以我们其实已经有计时器了,差了个进入计时器的接口罢了。
时间轴事件
既然少了个接口,那么加个不就完了嘛。更改DisplayObject
的_show
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def _show(self, c): if not self.visible: return # 加入时间轴事件入口 self._loopFrame() c.save() c.translate(self.x, self.y) c.setOpacity(self.alpha * c.opacity()) c.rotate(self.rotation) c.scale(self.scaleX, self.scaleY) self._loopDraw(c) c.restore() |
就加入了时间轴事件入口那一行代码。这个方法在子类Sprite
中具体实现:
1 2 3 4 5 | def _loopFrame(self): e = object () e.currentTarget = self s.__enterFrameListener(e) |
其中__enterFrameListener
为新加入Sprite
的属性,是时间轴事件的监听器。与鼠标事件相同,我们向监听器传入一个参数,用于获取事件信息。
更改Sprite
的addEventListener
使其能加入时间轴事件:
1 2 3 4 5 6 7 8 | def addEventListener(self, eventType, listener): if eventType == Event.ENTER_FRAME: self.__enterFrameListener = listener else : self.mouseList.append({ "eventType" : eventType, "listener" : listener }) |
再在Event
类中定义一下时间轴事件ENTER_FRAME
:
1 2 3 4 | class Event(Object): ENTER_FRAME = "enter_frame" # more code ... |
使用时,这么写就OK:
1 2 3 4 5 | layer = Sprite() layer.addEventListener(Event.ENTER_FRAME, onframe) def onframe(e): print( "enter frame" ) |
简单的动画类
动画类:
1 2 3 | class Animation(Sprite): def __init__(self, bitmapData = BitmapData(), frameList = [[AnimationFrame()]]): super(Animation, self).__init__() |
这个类需要一个BitmapData
对象作为参数,还需要一个list
对象,这个list
是用来装AnimationFrame
对象的帧列表,AnimationFrame
顾名思义是一个保存每帧数据的类。代码实现如下:
1 2 3 4 5 6 7 8 | class AnimationFrame( object ): def __init__(self, x = 0, y = 0, width = 0, height = 0): super(AnimationFrame, self).__init__() self.x = x self.y = y self.width = width self.height = height |
其中x
,y
属性储存了每帧位于图片上的位置,width
和height
储存每帧的宽高。
对于一般的动画图片(上文中的示例图片),每帧都是均匀分布在图片上的,所以我们可以加入一个函数进行帧的均匀裁剪,这样一来,我们获取帧列表就会方便很多。为Animation
类添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def divideUniformSizeFrames(width = 0, height = 0, col = 1, row = 1): result = [] frameWidth = width / col frameHeight = height / row for i in range(row): rowList = [] for j in range(col): frame = AnimationFrame(j * frameWidth, i * frameHeight, frameWidth, frameHeight) rowList.append(frame) result.append(rowList) return result |
接下来,我们调用这个函数,传入相应参数就可以切割出帧列表了。如下:
1 2 3 4 5 6 7 8 9 | l = Animation.divideUniformSizeFrames(160, 160, 4, 4) # 得到如下列表: [ [AnimationFrame(0, 0, 40, 40), AnimationFrame(40, 0, 40, 40), AnimationFrame(80, 0, 40, 40), AnimationFrame(120, 0, 40, 40)], [AnimationFrame(0, 40, 40, 40), AnimationFrame(40, 40, 40, 40), AnimationFrame(80, 40, 40, 40), AnimationFrame(120, 40, 40, 40)], [AnimationFrame(0, 80, 40, 40), AnimationFrame(40, 80, 40, 40), AnimationFrame(80, 80, 40, 40), AnimationFrame(120, 80, 40, 40)], [AnimationFrame(0, 120, 40, 40), AnimationFrame(40, 120, 40, 40), AnimationFrame(80, 120, 40, 40), AnimationFrame(120, 120, 40, 40)] ] |
接下来就是实现播放动画了,修改Animation
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Animation(Sprite): def __init__(self, bitmapData = BitmapData(), frameList = [[AnimationFrame()]]): super(Animation, self).__init__() self.bitmapData = bitmapData self.frameList = frameList self.bitmap = Bitmap(bitmapData) self.currentRow = 0 self.currentColumn = 0 self.addEventListener(Event.ENTER_FRAME, self.__onFrame) def __onFrame(self, e): currentFrame = self.frameList[self.currentRow][self.currentColumn] self.bitmap.bitmapData.setProperty(currentFrame.x, currentFrame.y, currentFrame.width, currentFrame.height) self.currentColumn += 1 if self.currentColumn >= len(self.frameList[self.currentRow]): self.currentColumn = 0 |
由于这个类继承自Sprite
,所以就继承了加入事件的addEventListener
方法。以上代码实现的是播放一排动画,大家可以自行拓展为播放一列动画或者播放整组动画。
这样一来,写入以下代码就能播放动画了:
1 2 3 4 5 6 7 8 9 10 11 | # 加载图片 loader = Loader() loader.load( "./player.png" ) # 动画数据 bmpd = BitmapData(loader.content) l = Animation.divideUniformSizeFrames(160, 160, 4, 4) # 加入动画 anim = Animation(bmpd, l) addChild(anim) |
Python游戏引擎开发系列: