Python游戏引擎开发(六):动画的小小研究


今天我们来研究动画,其实这个动画就是一个Sprite+Bitmap的结合体。不造什么是SpriteBitmap?=__=#看来你是半路杀进来的,快去看看前几章吧:

动画的原理

一般而言,我们的动画是用的这样一种图片:

Image1

播放动画的时候,像播放电影一样,这张图就是胶卷。我们可以弄一个放映机,放映机的镜头大小就是每个动作小图的大小。如果我们的胶卷不停地移动,那么就会连成动画,如下图:
Image2

Image3vcHLyOe6zs/Uyr7NvMaso6zG5NbQzOG1vcHLPGNvZGU+Qml0bWFwRGF0YTwvY29kZT7A4KOosru2rqO/wbyzvciwxOPIpbbBtsHHsLy41cKjqaOs1eK49sDg1tDT0Lj2wb249re9t6ijujxjb2RlPnNldENvb3JkaW5hdGU8L2NvZGU+us08Y29kZT5zZXRQcm9wZXJ0eTwvY29kZT7Tw9PayejWw828xqzP1Mq+tcTOu9bDus2089Cho7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> bmpd.setCoordinate(x, y) bmpd.setProperty(x, y, width, height)

参数图解如下:
Image4
在播放动画时,我们的“胶卷”就是一个Bitmap图片显示对象,其中包含了一个BitmapData对象,我们通过调用这个对象的上述两个方法,就能实现动画播放。

不过到此好像还是少了什么?也许你会问,动画是个连续的过程,且每帧动画之间需要间隔一点时间,是不是少了一个计时器?是的,是的,是的,重要的事情说三遍,我们的确少了一个计时器类似物。不过别急。大家还记得第三章中提到的显示对象的_show方法吗?这个方法是在窗口的paintEvent中被调用的,paintEvent又是在一个计时器中被调用的(涉及第二章内容)。等等……计时器……所以我们其实已经有计时器了,差了个进入计时器的接口罢了。

时间轴事件

既然少了个接口,那么加个不就完了嘛。更改DisplayObject_show方法:

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中具体实现:

def _loopFrame(self):
    e = object()
    e.currentTarget = self

    s.__enterFrameListener(e)

其中__enterFrameListener为新加入Sprite的属性,是时间轴事件的监听器。与鼠标事件相同,我们向监听器传入一个参数,用于获取事件信息。

更改SpriteaddEventListener使其能加入时间轴事件:

def addEventListener(self, eventType, listener):
    if eventType == Event.ENTER_FRAME:
        self.__enterFrameListener = listener
    else:
        self.mouseList.append({
            "eventType" : eventType,
            "listener" : listener
        })

再在Event类中定义一下时间轴事件ENTER_FRAME

class Event(Object):
    ENTER_FRAME = "enter_frame"
    # more code
    ...

使用时,这么写就OK:

layer = Sprite()
layer.addEventListener(Event.ENTER_FRAME, onframe)

def onframe(e):
    print("enter frame")

简单的动画类

动画类:

class Animation(Sprite):
    def __init__(self, bitmapData = BitmapData(), frameList = [[AnimationFrame()]]):
        super(Animation, self).__init__()

这个类需要一个BitmapData对象作为参数,还需要一个list对象,这个list是用来装AnimationFrame对象的帧列表,AnimationFrame顾名思义是一个保存每帧数据的类。代码实现如下:

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

其中xy属性储存了每帧位于图片上的位置,widthheight储存每帧的宽高。

对于一般的动画图片(上文中的示例图片),每帧都是均匀分布在图片上的,所以我们可以加入一个函数进行帧的均匀裁剪,这样一来,我们获取帧列表就会方便很多。为Animation类添加如下代码:

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

接下来,我们调用这个函数,传入相应参数就可以切割出帧列表了。如下:

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类:

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方法。以上代码实现的是播放一排动画,大家可以自行拓展为播放一列动画或者播放整组动画。

这样一来,写入以下代码就能播放动画了:

# 加载图片
loader = Loader()
loader.load("./player.png")

# 动画数据
bmpd = BitmapData(loader.content)
l = Animation.divideUniformSizeFrames(160, 160, 4, 4)

# 加入动画
anim = Animation(bmpd, l)
addChild(anim)

预告:下一篇我们来绘制矢量图形。

评论关闭