HGE系列之九 管中窥豹(精灵动画)

HGE系列之九管中窥豹(精灵动画)

 

 

  这次的HGE之旅,让我们来看看精灵及动画的实现,毕竟对于一款2D游戏引擎来说,恐怕精灵和动画不是最重要的,也可算是最重要之一了吧:)

 

HGE内部对于精灵以及动画的实现其实相对简单,主要都是有hgeSprite(精灵)和hgeAnimation(动画)这两个类来完成所需的操作,内部使用的接口也都是hge基类所提供的(具体细节请参看源码实现),基本的原理也并无什么特殊的地方:精灵也便是一张贴图,动画也是传统的逐帧动画 :)

 

  好了,让我们闲话少叙,先来看一看hgeSprite的内部构造:

 

  类名 hgeSprite

  功能 精灵类

  头文件 hge/hge181/include/hgeSprite.h

  实现文件 hge/hge181/src/helpers/hgeSprite.cpp

 

  先来看看hgeSprite提供的各项功能(对外接口):

 

class hgeSprite

{

public:

    // 参数构造函数,注意一下各个参数

       hgeSprite(HTEXTURE tex, float x, float y, float w, float h);

       hgeSprite(const hgeSprite &spr);

       ~hgeSprite() { hge->Release(); }

      

       // 渲染函数集

       void        Render(float x, float y);

       void        RenderEx(float x, float y, float rot, float hscale=1.0f, float vscale=0.0f);

       void        RenderStretch(float x1, float y1, float x2, float y2);

       void        Render4V(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3);

   

    // 设置纹理

       void        SetTexture(HTEXTURE tex);

    // 设置纹理使用尺寸

       void        SetTextureRect(float x, float y, float w, float h, bool adjSize = true);

    // 设置颜色

       void        SetColor(DWORD col, int i=-1);

    // 设置Z Buffer

       void        SetZ(float z, int i=-1);

    // 设置混合方式

       void        SetBlendMode(int blend) { quad.blend=blend; }

    // 设置“热点”(中心点)

       void        SetHotSpot(float x, float y) { hotX=x; hotY=y; }

    // 设置纹理翻转

       void        SetFlip(bool bX, bool bY, bool bHotSpot = false);

   

    // 获取纹理

       HTEXTURE   GetTexture() const { return quad.tex; }

// 获取纹理使用尺寸   void        GetTextureRect(float *x, float *y, float *w, float *h) const { *x=tx; *y=ty; *w=width; *h=height; }

// 获取颜色

       DWORD        GetColor(int i=0) const { return quad.v[i].col; }

    // 获取Z Buffer

       float        GetZ(int i=0) const { return quad.v[i].z; }

    // 获取混合模式

       int                  GetBlendMode() const { return quad.blend; }

    // 获取“热点”(中心点)

       void        GetHotSpot(float *x, float *y) const { *x=hotX; *y=hotY; }

    // 获取纹理翻转值

       void        GetFlip(bool *bX, bool *bY) const { *bX=bXFlip; *bY=bYFlip; }

   

    // 获取宽度

       float        GetWidth() const { return width; }

    // 获取高度

       float        GetHeight() const { return height; }

    // 获取包围盒

       hgeRect* GetBoundingBox(float x, float y, hgeRect *rect) const { rect->Set(x-hotX, y-hotY, x-hotX+width, y-hotY+height); return rect; }

    // 获取包围盒(考虑缩放以及旋转)

       hgeRect* GetBoundingBoxEx(float x, float y, float rot, float hscale, float vscale,  hgeRect *rect) const;

 

protected:

    // 禁止默认构造函数

       hgeSprite();

    // hge基类接口,用于调用其开放的接口函数

       static HGE      *hge;

 

    // hgeQuad类,定义请见hge.h

       hgeQuad         quad;

    // 纹理坐标,使用宽高

       float        tx, ty, width, height;

    // 纹理宽高

       float        tex_width, tex_height;

       // 热点坐标

float        hotX, hotY;

// 翻转标志

       bool        bXFlip, bYFlip, bHSFlip;

};

 

以上便是hgeSprite的全部内容,看来并不复杂 :) 那么接下来就让我们来看看这hgeSprite的内部实现吧:

 

首先让我们来看看hgeSprite的构造函数:

 

hgeSprite::hgeSprite(HTEXTURE texture, float texx, float texy, float w, float h)

{

       float texx1, texy1, texx2, texy2;

   

    // 创建hge

       hge=hgeCreate(HGE_VERSION);

      

    // 根据构造参数设定相应的纹理参数

       tx=texx; ty=texy;

       width=w; height=h;

   

    // 如果纹理“句柄”存在

       if(texture)

       {

        // 调用hge API获取纹理宽高

              tex_width = (float)hge->Texture_GetWidth(texture);

              tex_height = (float)hge->Texture_GetHeight(texture);

       }

       else

       {

        // 否则设置为1.0

              tex_width = 1.0f;

              tex_height = 1.0f;

       }

 

    // 设定“热点”值为左上起点

       hotX=0;

       hotY=0;

    // 设置翻转标志为false

       bXFlip=false;

       bYFlip=false;

       bHSFlip=false;

    // 设置quad结构中的纹理(tex

       quad.tex=texture;

 

       texx1=texx/tex_width;

       texy1=texy/tex_height;

       texx2=(texx+w)/tex_width;

       texy2=(texy+h)/tex_height;

   

    // 设置quad结构中的纹理坐标

       quad.v[0].tx = texx1; quad.v[0].ty = texy1;

       quad.v[1].tx = texx2; quad.v[1].ty = texy1;

       quad.v[2].tx = texx2; quad.v[2].ty = texy2;

       quad.v[3].tx = texx1; quad.v[3].ty = texy2;

   

    // 设置Z Buffer为默认值0.5

       quad.v[0].z =

       quad.v[1].z =

       quad.v[2].z =

       quad.v[3].z = 0.5f;

       // 设置顶点颜色为默认值黑色

       quad.v[0].col =

       quad.v[1].col =

       quad.v[2].col =

       quad.v[3].col = 0xffffffff;

    // 设置混合方式为默认

       quad.blend=BLEND_DEFAULT;

}

 

接着让我们看一看hgeSprite的渲染函数,篇幅关系,在此仅以Render函数为例讲述,其余三个渲染函数原理类似,有兴趣的朋友请参看实现源码:

 

void hgeSprite::Render(float x, float y)

{

       float tempx1, tempy1, tempx2, tempy2;

    // 设置渲染坐标,注意“热点”(中心点)的处理

       tempx1 = x-hotX;

       tempy1 = y-hotY;

       tempx2 = x+width-hotX;

       tempy2 = y+height-hotY;

    // 根据计算的渲染坐标设置设置顶点坐标

       quad.v[0].x = tempx1; quad.v[0].y = tempy1;

       quad.v[1].x = tempx2; quad.v[1].y = tempy1;

       quad.v[2].x = tempx2; quad.v[2].y = tempy2;

       quad.v[3].x = tempx1; quad.v[3].y = tempy2;

    // 调用hge API完成渲染

       hge->Gfx_RenderQuad(&quad);

}

 

可以看到,渲染的核心便是Gfx_RenderQuad这个API函数了 :)

 

接着让我们来看看hgeSprite是如何获取包围盒的:

 

hgeRect* hgeSprite::GetBoundingBoxEx(float x, float y, float rot, float hscale, float vscale, hgeRect *rect) const

{

       float tx1, ty1, tx2, ty2;

       float sint, cost;

    // 清空rect中的坐标数值

       rect->Clear();

      

    // 根据缩放值重新计算顶点坐标

       tx1 = -hotX*hscale;

       ty1 = -hotY*vscale;

       tx2 = (width-hotX)*hscale;

       ty2 = (height-hotY)*vscale;

   

    // 如果旋转角度不为零

       if (rot != 0.0f)

       {

              cost = cosf(rot);

              sint = sinf(rot);

              // 根据旋转值“扩展”包围盒的大小

        // 关于Encapsulate的更多信息可以参看这里  

              rect->Encapsulate(tx1*cost - ty1*sint + x, tx1*sint + ty1*cost + y);

              rect->Encapsulate(tx2*cost - ty1*sint + x, tx2*sint + ty1*cost + y);

              rect->Encapsulate(tx2*cost - ty2*sint + x, tx2*sint + ty2*cost + y);

              rect->Encapsulate(tx1*cost - ty2*sint + x, tx1*sint + ty2*cost + y);

       }

       else

       {

        // 否则“扩展”包围盒时则不考虑旋转

              rect->Encapsulate(tx1 + x, ty1 + y);

              rect->Encapsulate(tx2 + x, ty1 + y);

              rect->Encapsulate(tx2 + x, ty2 + y);

              rect->Encapsulate(tx1 + x, ty2 + y);

       }

 

       return rect;

}

 

获取包围盒的基本思想便是根据目前quad的大小(考虑缩放以及旋转)来获取一个完整包围其尺寸的最小矩形,有点简化的AABB的味道 :)

 

接着让我们再来看看hgeSprite是如何设置翻转的:

 

void hgeSprite::SetFlip(bool bX, bool bY, bool bHotSpot)

{

       float tx, ty;

 

    // 如果设置了“热点”翻转以及X轴翻转,则重新计算“热点”X轴坐标

       if(bHSFlip && bXFlip) hotX = width - hotX;

// 如果设置了“热点”翻转以及Y轴翻转,则重新计算“热点”Y轴坐标

       if(bHSFlip && bYFlip) hotY = height - hotY;

   

    // 重新设置翻转“热点”标志

       bHSFlip = bHotSpot;

      

    // 重新计算“热点”坐标

       if(bHSFlip && bXFlip) hotX = width - hotX;

       if(bHSFlip && bYFlip) hotY = height - hotY;

   

    // 如果X轴设置翻转

       if(bX != bXFlip)

       {

        // 交换顶点(01顶点对以及23定点对)的纹理坐标

              tx=quad.v[0].tx; quad.v[0].tx=quad.v[1].tx; quad.v[1].tx=tx;

              ty=quad.v[0].ty; quad.v[0].ty=quad.v[1].ty; quad.v[1].ty=ty;

              tx=quad.v[3].tx; quad.v[3].tx=quad.v[2].tx; quad.v[2].tx=tx;

              ty=quad.v[3].ty; quad.v[3].ty=quad.v[2].ty; quad.v[2].ty=ty;

 

              bXFlip=!bXFlip;

       }

    // 如果Y轴设置翻转

       if(bY != bYFlip)

       {

        // 交换顶点(03顶点对以及12定点对)的纹理坐标

              tx=quad.v[0].tx; quad.v[0].tx=quad.v[3].tx; quad.v[3].tx=tx;

              ty=quad.v[0].ty; quad.v[0].ty=quad.v[3].ty; quad.v[3].ty=ty;

              tx=quad.v[1].tx; quad.v[1].tx=quad.v[2].tx; quad.v[2].tx=tx;

              ty=quad.v[1].ty; quad.v[1].ty=quad.v[2].ty; quad.v[2].ty=ty;

 

              bYFlip=!bYFlip;

       }

}

 

设置翻转的原理其实非常简单,交换纹理坐标而已 :)

 

最后让我们来看看hgeSprite是如何来重新设置纹理的:

 

void hgeSprite::SetTexture(HTEXTURE tex)

{

       float tx1,ty1,tx2,ty2;

       float tw,th;

    // 重新设置quad结构纹理

       quad.tex=tex;

 

    // 获取纹理高宽

       if(tex)

       {

              tw = (float)hge->Texture_GetWidth(tex);

              th = (float)hge->Texture_GetHeight(tex);

       }

       else

       {

              tw = 1.0f;

              th = 1.0f;

       }

   

    // 如果重新设置的纹理与原始纹理高宽不符

       if(tw!=tex_width || th!=tex_height)

       {

        // 还原原始纹理“绝对”坐标

              tx1=quad.v[0].tx*tex_width;

              ty1=quad.v[0].ty*tex_height;

              tx2=quad.v[2].tx*tex_width;

              ty2=quad.v[2].ty*tex_height;

        // 重新设置纹理高宽

              tex_width=tw;

              tex_height=th;

        // 重新设置纹理“相对”坐标

              tx1/=tw; ty1/=th;

              tx2/=tw; ty2/=th;

        // 设置quad结构纹理坐标

              quad.v[0].tx=tx1; quad.v[0].ty=ty1;

              quad.v[1].tx=tx2; quad.v[1].ty=ty1;

              quad.v[2].tx=tx2; quad.v[2].ty=ty2;

              quad.v[3].tx=tx1; quad.v[3].ty=ty2;

       }

}

至此我们基本便将hgeSprite简单剖分了一遭,但是单单的精灵有时还是缺乏不少动感,有时我们还需要动画的帮助,于是hgeAnimation便诞生了:

 

  类名 hgeAnimation

  功能 精灵动画类

  头文件 hge/hge181/include/hgeAnim.h

  实现文件 hge/hge181/src/helpers/hgeAnim.cpp

 

依例,让我们首先来看看hgeAnimation的头文件:

 

// hgeAnimation继承于hgeSprite

class hgeAnimation : public hgeSprite

{

public:

    // 参数构造函数

       hgeAnimation(HTEXTURE tex, int nframes, float FPS, float x, float y, float w, float h);

       hgeAnimation(const hgeAnimation &anim);

      

    // 播放动画

       void        Play();

    // 停止播放

       void        Stop() { bPlaying=false; }

    // 回复播放

       void        Resume() { bPlaying=true; }

    // 更新动画状态

       void        Update(float fDeltaTime);

    // 是否在播放

       bool        IsPlaying() const { return bPlaying; }

   

    // 重新设置纹理

       void        SetTexture(HTEXTURE tex) { hgeSprite::SetTexture(tex); orig_width = hge->Texture_GetWidth(tex, true); }

    // 重新设置使用纹理

       void        SetTextureRect(float x1, float y1, float x2, float y2) { hgeSprite::SetTextureRect(x1,y1,x2,y2); SetFrame(nCurFrame); }

    // 设置播放模式

       void        SetMode(int mode);

    // 设置播放速度

       void        SetSpeed(float FPS) { fSpeed=1.0f/FPS; }

    // 设置当前帧

       void        SetFrame(int n);

    // 设置动画帧数

       void        SetFrames(int n) { nFrames=n; }

   

   // 获取动画播放模式

       int                  GetMode() const { return Mode; }

    // 获取播放速度

       float        GetSpeed() const { return 1.0f/fSpeed; }

    // 获取当前帧

       int                  GetFrame() const { return nCurFrame; }

    // 获取动画帧数

       int                  GetFrames() const { return nFrames; }

 

private:

    // 隐藏默认构造函数

       hgeAnimation();

    

    // 原始纹理宽度?

       int                  orig_width;

    // 是否在播放

       bool        bPlaying;

   

    // 播放速度

       float        fSpeed;

    // 与上一帧播放的时间间隔

       float        fSinceLastFrame;

    // 播放模式

       int                  Mode;

    // 播放间隔帧数

       int                  nDelta;

    // 动画帧数

       int                  nFrames;

    // 当前帧

       int                  nCurFrame;

};

 

然我们来看看hgeAnimation的构造函数:

 

hgeAnimation::hgeAnimation(HTEXTURE tex, int nframes, float FPS, float x, float y, float w, float h)

       : hgeSprite(tex, x, y, w, h)

{

    // 获取原始纹理宽度

       orig_width = hge->Texture_GetWidth(tex, true);

   

    // 设置经过时间

       fSinceLastFrame=-1.0f;

    // 设置播放速度

       fSpeed=1.0f/FPS;

       // 设置是否播放(默认为否)

bPlaying=false;

// 设置动画帧数

       nFrames=nframes;

   

    // 设置播放模式(默认为向前并循环)

       Mode=HGEANIM_FWD | HGEANIM_LOOP;

    // 设置播放间隔(默认为1帧)

       nDelta=1;

    // 设置当前帧为第零帧

       SetFrame(0);

}

 

hgeAnimation的构造函数不无多少新奇的地方,所做的工作基本亦是初始化相关变量。

 

接着来看看hgeAnimation如何设置播放模式:

 

void hgeAnimation::SetMode(int mode)

{

       Mode=mode;

 

    // 如果播放模式为反向播放

       if(mode & HGEANIM_REV)

       {

        // 设置播放帧间隔为-1

              nDelta = -1;

        // 并设置当前帧为动画最后一帧

              SetFrame(nFrames-1);

       }

       else

       {

        // 否则设置播放间隔为1

              nDelta = 1;

        // 并设置当前帧为动画第一帧

              SetFrame(0);

       }

}

 

哈哈,是不是相当简单,那么让我们再来看看hgeAniamtion是如何设置当前帧的:

 

void hgeAnimation::SetFrame(int n)

{

       float tx1, ty1, tx2, ty2;

       bool bX, bY, bHS;

       // 设置动画列数

    int ncols = int(orig_width) / int(width);

 

    // 设置当前帧数(取余)

       n = n % nFrames;

    // 如果n为负,则代表从后往前计算帧数

       if(n < 0) n = nFrames + n;

       nCurFrame = n;

 

       // 为当前帧计算纹理坐标

       ty1 = ty;

       tx1 = tx + n*width;

   

    // 如果当前纹理宽度大于边界(threshold)值,则进行“换行"处理

       if(tx1 > orig_width-width)

       {

        // 减去所在纹理“第一行”的余帧,

        // 即得到当前的所在帧数(换行后)

              n -= int(orig_width-tx) / int(width);

        // 重新计算纹理X坐标

              tx1 = width * (n%ncols);

        // 重新计算纹理Y坐标

              ty1 += height * (1 + n/ncols);

       }

 

       tx2 = tx1 + width;

       ty2 = ty1 + height;

   

    // 计算“相对”纹理坐标

       tx1 /= tex_width;

       ty1 /= tex_height;

       tx2 /= tex_width;

       ty2 /= tex_height;

   

    // 重新设置quad结构纹理坐标

       quad.v[0].tx=tx1; quad.v[0].ty=ty1;

       quad.v[1].tx=tx2; quad.v[1].ty=ty1;

       quad.v[2].tx=tx2; quad.v[2].ty=ty2;

       quad.v[3].tx=tx1; quad.v[3].ty=ty2;

   

    // 重新设置翻转

       bX=bXFlip; bY=bYFlip; bHS=bHSFlip;

       bXFlip=false; bYFlip=false;

       SetFlip(bX,bY,bHS);

}

 

从源码实现可以看出,hgeAnimation内部使用的是一个“矩阵型”的动画纹理,随着播放帧数的改变,内建的这个SetFrame函数会正确的设置相应的纹理坐标。

 

最后,让我们以hgeAnimationUpdate函数进行结尾:

 

void hgeAnimation::Update(float fDeltaTime)

{

    // 如果不在播放则直接返回

       if(!bPlaying) return;

   

    // 如果是第一次更新,则重新设置经过时间为0

       if(fSinceLastFrame == -1.0f)

              fSinceLastFrame=0.0f;

       else

    // 否则便累计经过时间

              fSinceLastFrame += fDeltaTime;

   

    // 当达到播放时间间隔时间

       while(fSinceLastFrame >= fSpeed)

       {

        // 更新经过时间

              fSinceLastFrame -= fSpeed;

        // 如果达到了最后一帧

              if(nCurFrame + nDelta == nFrames)

              {

                     switch(Mode)

                     {

                // 向前模式

                            case HGEANIM_FWD:

                // 反向、往返(PingPong)模式

                            case HGEANIM_REV | HGEANIM_PINGPONG:

                // 则停止播放

                                   bPlaying = false;

                                   break;

                // 向前、往返模式

                            case HGEANIM_FWD | HGEANIM_PINGPONG:

                // 向前、往返、循环模式

                            case HGEANIM_FWD | HGEANIM_PINGPONG | HGEANIM_LOOP:

                // 反向、往返、循环模式

                            case HGEANIM_REV | HGEANIM_PINGPONG | HGEANIM_LOOP:

                // 更新播放间隔

                                   nDelta = -nDelta;

                                   break;

                     }

              }

        // 已经到达了第一帧

              else if(nCurFrame + nDelta < 0)

              {

                     switch(Mode)

                     {

                // 反向模式

                            case HGEANIM_REV:

                // 前向、往返模式

                            case HGEANIM_FWD | HGEANIM_PINGPONG:

                                   bPlaying = false;

                                   break;

                // 反向、往返模式

                            case HGEANIM_REV | HGEANIM_PINGPONG:

                // 反向、往返、循环模式

                            case HGEANIM_REV | HGEANIM_PINGPONG | HGEANIM_LOOP:

                // 前向、往返、循环模式

                            case HGEANIM_FWD | HGEANIM_PINGPONG | HGEANIM_LOOP:

                // 更新播放间隔

                                   nDelta = -nDelta;

                                   break;

                     }

              }

       

        // 最后设置当前帧

              if(bPlaying) SetFrame(nCurFrame+nDelta);

       }

}

 

好了,hge的精灵以及动画至此算是讲了一个梗概,更细节的问题大家可以参照源码以及文档,那么最后,下次再见吧 :)

 

你可能感兴趣的:(api,Class,buffer,扩展,float,Blend)