前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HGE系列之九 管中窥豹(精灵动画)

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

作者头像
用户2615200
发布2018-08-02 17:35:59
5440
发布2018-08-02 17:35:59
举报

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)

{

// 交换顶点(0、1顶点对以及2、3定点对)的纹理坐标

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)

{

// 交换顶点(0、3顶点对以及1、2定点对)的纹理坐标

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函数会正确的设置相应的纹理坐标。

最后,让我们以hgeAnimation的Update函数进行结尾:

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2011年01月02日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档