X264源代码分析——总纲入门最佳

http://www.cnblogs.com/xkfz007/articles/2616133.html

l       相关说明:

1.     使用版本: x264-cvs-2004-05-11

2.     这次的分析基本上已经将代码中最难理解的部分做了阐释,对代码的主线也做了剖析,如果这个主线理解了,就容易设置几个区间,进行分工阅读,将各个区间击破了.

3.     需要学习的知识:

a)       编码器的工作流程.

b)      H.264的码流结构,像x264_sps_t,x264_pps_t等参数的定义基本上都完全符合标准文档中参数集的定义,抓住主要参数,次要参数也应该有所了解.

c)       数学知识,对dct变换等与数学相关的知识的编程实现要有较好理解.

d)      C语言的知识.涉及到c语言的较多不经常用的特性,如函数指针数组,移位运算,结构体的嵌套定义等.

e)       耐心,对h.264的复杂性要有清醒的认识.

3.参考资料:

a)       新一代视频压缩编码标准-h.264/avc 毕厚杰主编,人民邮电出版社.

b)      网上的流媒体论坛,百度,google等搜索引擎.

4. 阅读代码的方法:

a)       较好的方法是利用vc的调试器,如果对某个函数感兴趣,可以将断点设置在它的前面.然后采用step into,step over等方法进去该函数一步步分析.当然本身要对程序执行流程要有较清楚认识,不然不知道何时step into,何时step over.

b)      建议应该先对照标准弄清各个结构体成员的意义.

l       源代码主要过程分析:

1.       进入x264.c中的main函数.

刚开始是读取默认参数,如果你设置了参数的话会修改param的.

i_ret = Encode( &param, fin, fout );

这条语句使过程进入x264.c中的Encode函数.

2.       X.264的encode函数.

A.

i_frame_total = 0;
if( !fseek( fyuv, 0, SEEK_END ) )
     {
        int64_t i_size = ftell( fyuv );
        fseek( fyuv, 0, SEEK_SET );
        i_frame_total = i_size / ( param->i_width * param->i_height * 3 / 2 )
}

上面这段计算出输入文件的总帧数.

B.      h = x264_encoder_open( param )这个函数是对不正确的参数进行修改,并对各结构体参数和cabac编码,预测等需要的参数进行初始化.

C.     pic = x264_picture_new( h );

该函数定义在/CORE/common.c中.首先分给能容纳sizeof(x264_picture_t)字节数的空间,然后进行初始化.

这里看一下x264_picture_t和x264_frame_t的区别.前者是说明一个视频序列中每帧的特点.后者存放每帧实际的象素值.注意区分.

D.

for( i_frame = 0, i_file = 0; i_ctrl_c == 0 ; i_frame++ )
{
    int         i_nal;
    x264_nal_t *nal;
    int         i;
    /* read a frame */
    if( fread( pic->plane[0], 1, param->i_width * param->i_height, fyuv ) <= 0 ||
            fread( pic->plane[1], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 ||
            fread( pic->plane[2], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 )
    {
        break;
    }
    //文件位置指示器自己变化了.
    if( x264_encoder_encode( h, &nal, &i_nal, pic ) < 0 )
    {
        fprintf( stderr, "x264_encoder_encode failed/n" );
    }
    ……
}

凡是出现for循环的地方都应该认真对待,这是我的一个体会,也是进入分层结构认真分析的好方法.

fread()函数一次读入一帧,分亮度和色度分别读取.这里要看到c语言中的File文件有一个文件位置指示器,调用fread()函数会使文件指示器自动移位,这就是一帧一帧读取的实现过程.

E.      然后进入x264_encoder_encode( h, &nal, &i_nal, pic )函数,该函数定义在/Enc/encoder.c中.

开始进入比较复杂的地方了.

这个函数前面有一段注释(如下):

****************************************************************************
 * x264_encoder_encode:
 * XXX: i_poc   : is the poc of the current given picture
 *       i_frame : is the number of the frame being coded
 * ex: type frame poc
 *       I      0   2*0//poc是实际的帧的位置.
 *       P      1   2*3//frame是编码的顺序.
 *       B      2   2*1
 *       B      3   2*2
 *       P      4   2*6
 *       B      5   2*4
 *       B      6   2*5
 ****************************************************************************/

要搞清poc和frame的区别.

假设一个视频序列如下:

I     B    B    P     B     B   P

我们编码是按I P B B P B B的顺序,这就是frame的编号.

而我们视频序列的播放序号是POC的序号,这里是乘以了2.

函数中先定义了如下三个参数:

int     i_nal_type;

nal存放的数据类型, 可以是sps,pps等多种.

int     i_nal_ref_idc;

nal的优先级,nal重要性的标志位.

前面两个参数虽然简单,但如果不参照标准,也不容易理解,所以标准中的句法表是很重要的,可以说是最关键的.

int     i_slice_type;

slice的类型,在x264中我的感觉好像一帧只有一个slice.如果确定了帧的类型,slice的类型也就确定了.

我们来看看编码器是如何区分读入的一帧是I帧,P帧,或者B帧,这个过程需要好好理解.

还以I  B   B   P   B   B   P为例.

if( h->i_frame % (h->param.i_iframe * h->param.i_idrframe) == 0 ){

确定这是立即刷新片.

}

这里很好理解.

但到了if( h->param.i_bframe > 0 )//可以B帧编码时.

就有问题了.

注意我们编完I帧后碰到了一个B帧,这时我们先不对它进编码.而是采用frame = x264_encoder_frame_put_from_picture( h, h->frame_next, pic )函数将这个B帧放进h->frame_next中.

好,这里出现了h->frame_next,在h中同时定义了下面几个帧数组用以实现帧的管理.

x264_frame_t  *bframe_current[X264_BFRAME_MAX]; /* store the sequence of b frame being encoded */
x264_frame_t    *frame_next[X264_BFRAME_MAX+1];   /* store the next sequence of frames to be encoded *///搞清意义,下一个帧,而不一定是B帧.
x264_frame_t    *frame_unused[X264_BFRAME_MAX+1]; /* store unused frames */

注意区分这3个数组.

同时还有下面4个函数(定义在/ENCODER/encoder.c中).

x264_encoder_frame_put_from_picture();
x264_encoder_frame_put();
x264_encoder_frame_get();
x264_frame_copy_picture();

这3个数组和4个函数可以说完成了整个帧的类型的判定问题.这个里面if ,else语句较多,容易使人迷惑.但我们只要把握下面一个观点就可以看清实质:在不对P帧进行编码之前,我们不对B帧进行编码,只是把B帧放进缓冲区(就是前面提到的数组).

比如视频序列:I     B B P  B B P

先确立第一个帧的类型,然后进行编码.然后是2个B帧,我们把它放进缓冲区数组.然后是P帧,我们可以判定它的类型并进行编码.同时,我们将缓冲区的B帧放进h->bframe_current[i],不过这时P帧前的两个B帧并没有编码.当读到P帧后面的第一个B帧时,我们实际上才将h->bframe_current数组中的第一个B帧编码,也就是将在I帧后面的第一个B帧(说成P帧前面的第一个B帧容易误解J)编码.

依此类推,把握好上面4个函数的调用流程和指针操作的用法,就可以将帧的类型判定这个问题搞明白了.

F.      然后是速率控制(先不说这个,因为它对编码的流程影响不大),看看建立参考帧列表的操作,也就是

x264_reference_build_list( h, h->fdec->i_poc ); (定义在/ENCODER/encoder.c中).

光看这个函数是不行的,它是和后面的这个函数(如下)一起配合工作的.

if( i_nal_ref_idc != NAL_PRIORITY_DISPOSABLE )//B帧时.
{
   x264_reference_update( h );
}

If条件是判断当前帧是否是B帧,如果是的话就不更新参考列表,因为B帧本来就不能作为参考帧嘛!如果是I帧或P帧的话,我们就更新参考帧列表.

我们看到了一个for循环,两个do—while循环.这是实现的关键,具体看代码,不好用语言说明白.

G.     进入另一个复杂的领域:写slice的操作,刚开使挺简单,如我下面的注释.

/* ---------------------- Write the bitstream -------------------------- */
    /* Init bitstream context */
    h->out.i_nal = 0;//out的声明在bs.h中.
    bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位.

/* Write SPS and PPS */
    if( i_nal_type == NAL_SLICE_IDR )//不是每次都要写SPS and PPS,只有碰见立即刷新片时才写.
    {
        /* generate sequence parameters */
        x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );
        x264_sps_write( &h->out.bs, h->sps );
        x264_nal_end( h );
        /* generate picture parameters */

x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );
        x264_pps_write( &h->out.bs, h->pps );
        x264_nal_end( h );
}

不过看下面那个函数(就进入了复杂的领域).

H.     x264_slice_write()(定义在/ENCODER/encoder.c中),这里面是编码的最主要部分,下面仔细分析.

前面不说,看下面这个循环,它是采用for循环对一帧图像的所有块依次进行编码.

for( mb_xy = 0, i_skip = 0; mb_xy < h->sps->i_mb_width * h->sps->i_mb_height; mb_xy++ )//h->sps->i_mb_width指的是从宽度上说有多少个宏快.对于宽度也就是288 / 16 = 18
    {
        const int i_mb_y = mb_xy / h->sps->i_mb_width;
        const int i_mb_x = mb_xy % h->sps->i_mb_width;//这两个变量是定义宏块的位置.而不是指宏块中元素的位置.
   
        /* load cache */
        x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是 把当前宏块的up宏块和left宏块的intra4x4_pred_mode,non_zero_count加载进来,放到一个数组里面,这个数组用来直 接得到当前宏块的左侧和上面宏块的相关值.要想得到当前块的预测值,要先知道上面,左面的预测值,它的目的是替代getneighbour函数.
/* analyse parameters
         * Slice I: choose I_4x4 or I_16x16 mode
         * Slice P: choose between using P mode or intra (4x4 or 16x16)
         * */
        TIMER_START( i_mtime_analyse );
        x264_macroblock_analyse( h );//定义在analyse.h中.
        TIMER_STOP( i_mtime_analyse );
   
        /* encode this macrobock -> be carefull it can change the mb type to P_SKIP if needed */
        TIMER_START( i_mtime_encode );
        x264_macroblock_encode( h );//定义在Enc/encoder.c中.
        TIMER_STOP( i_mtime_encode );

截止到这就已经完成编码的主要过程了,后面就是熵编码的过程了(我也没看到那,但认为前面才是编码的主要过程).下面对这个过程进行分析.

A.     x264_macroblock_cache_load( h, i_mb_x, i_mb_y );它是将要编码的宏块的周围的宏块的值读进来,要想得到当前块的预测值,要先知道上面,左面的预测值,它的作用相当于jm93中的getneighbour函数.

B.      进入x264_macroblock_analyse( h )函数(定义在/Enc/analyse.c中,这里涉及到了函数指针数组,需要好好复习,个人认为这也是x264代码最为复杂的一个地方了).既然已经将该宏块周围的宏块的值读了出来,我们就可以对该宏块进行分析了(其实主要就是通过计算sad值分析是否要将16*16的宏块进行分割和采用哪种分割方式合适).

看似很复杂,但我们只要把握一个东西就有利于理解了:

举个生活中的例子来说:

如果你有2元钱,你可以去买2袋1元钱的瓜子,也可以买一袋2元钱的瓜子,如果2袋1元钱的瓜子数量加起来比1袋2元钱的瓜子数量多,你肯定会买2袋1元的.反之你会去买那2元1袋的.

具体来说,对于一个16*16的块,

如果它是I帧的块,我们可以将它分割成16个4*4的块,如果这16个块的sad加起来小于按16*16的方式计算出来的sad值,我们就将这个16*16的块分成16个4*4的块进行编码(在计算每个4*4的块的最小sad值时已经知道它采用何种编码方式最佳了),否则采用16*16的方式编码(同样我们也已知道对它采用哪种编码方式最为合适了.

如果它是P帧或B帧的块,同样是循环套循环,但更为复杂了,可以看我在analyse.c中的注释.

这里还要注意的是提到了

x264_predict_t      predict_16x16[4+3];

typedef void (*x264_predict_t)( uint8_t *src, int i_stride );

这是函数指针数组,有很多对它的调用.

C.     退出x264_macroblock_analyse( h )函数,进入x264_macroblock_encode( )函数(定义在/ENCODER/macroblock.c中).

我拿宏块类型为I_16*16为例.

if( h->mb.i_type == I_16x16 )
    {
        const int i_mode = h->mb.i_intra16x16_pred_mode;
        /* do the right prediction */
        h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );//这两个参数的关系.
                                                                   //涉及到x264_predict_t(函数指针数组),声明在core/predict.h中,core/predict.c里有不同定义.
        /* encode the 16x16 macroblock */
        x264_mb_encode_i16x16( h, i_qscale );//
/* fix the pred mode value */
       …     }

我们看到h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );只调用了一次,这是因为在x264_macroblock_analyse(  )中我们已经确定了采用4种方式中的哪种最合适.而在x264_macroblock_analyse(  )中判定一个块是否为I_16*16,我们调用了四次.这是因为当时我们需要拿最小的sad值进行比较.

继续,是x264_mb_encode_i16x16( h, i_qscale )函数(定义在/ENCODER/macroblock.c中).在这个函数中我们就可以看到量化,zig-扫描等函数了,这些都是直来直去的,需要的只是我们的细心和对数学知识的掌握了

c)       到这里还没完,我们接着看

void x264_macroblock_encode( x264_t *h ){

…….前面省略.

执行到下面这条语句,看看下面是干啥的.

/* encode chroma */
   i_qscale = i_chroma_qp_table[x264_clip3( i_qscale + h->pps->i_chroma_qp_index_offset, 0, 51 )];
    if( IS_INTRA( h->mb.i_type ) )
    {
       const int i_mode = h->mb.i_chroma_pred_mode;
        /* do the right prediction */
        h->predict_8x8[i_mode]( h->mb.pic.p_fdec[1], h->mb.pic.i_fdec[1] );
        h->predict_8x8[i_mode]( h->mb.pic.p_fdec[2], h->mb.pic.i_fdec[2] );
 
        /* fix the pred mode value */
        h->mb.i_chroma_pred_mode = x264_mb_pred_mode8x8_fix[i_mode];
    }
   
    /* encode the 8x8 blocks */
x264_mb_encode_8x8( h, !IS_INTRA( h->mb.i_type ), i_qscale );//对色度块进行编码了.

到这我们可以看到原来我们在这前面是对宏块中的亮度系数进行了编码,我们到上面那个函数才开始对色度系数进行编码.进入x264_mb_encode_8x8()函数看到for循环里面有个2可以证明是对2个色度系数进行编码,想法没错.

那下面这些又是干啥的呢?它们是计算cbp系数看需要对残差(包括ac,dc)中的哪个系数进行传输的.

/* Calculate the Luma/Chroma patern and non_zero_count */
    if( h->mb.i_type == I_16x16 )
    {
        h->mb.i_cbp_luma = 0x00;
        for( i = 0; i < 16; i++ )
        {
            const int nz = array_non_zero_count( h->dct.block[i].residual_ac, 15 );
            h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
            if( nz > 0 )
            {
                h->mb.i_cbp_luma = 0x0f;
            }
        }
    }
    else
    {
        h->mb.i_cbp_luma = 0x00;
        for( i = 0; i < 16; i++ )
        {
            const int nz = array_non_zero_count( h->dct.block[i].luma4x4, 16 );//统计非0个数.
            h->mb.cache.non_zero_count[x264_scan8[i]] = nz;
            if( nz > 0 )
            {
                h->mb.i_cbp_luma |= 1 << (i/4);// %16的意义.
            }
        }
    }
   
    /* Calculate the chroma patern *///色度的cbp有3种方式.
    h->mb.i_cbp_chroma = 0x00;
    for( i = 0; i < 8; i++ )
    {
        const int nz = array_non_zero_count( h->dct.block[16+i].residual_ac, 15 );
        h->mb.cache.non_zero_count[x264_scan8[16+i]] = nz;
        if( nz > 0 )                      
        {
            h->mb.i_cbp_chroma = 0x02;    /* dc+ac (we can‘t do only ac) */
        }
    }
    if( h->mb.i_cbp_chroma == 0x00 &&
        ( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 || array_non_zero_count( h->dct.chroma_dc[1], 4 ) ) > 0 )
    {
        h->mb.i_cbp_chroma = 0x01;    /* dc only */
    }
   
    if( h->param.b_cabac )
    {
        if( h->mb.i_type == I_16x16 && array_non_zero_count( h->dct.luma16x16_dc, 16 ) > 0 )
            i_cbp_dc = 0x01;
        else
            i_cbp_dc = 0x00;
   
        if( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 )
            i_cbp_dc |= 0x02;
        if( array_non_zero_count( h->dct.chroma_dc[1], 4 ) > 0 )
            i_cbp_dc |= 0x04;
    }
   
    /* store cbp */
h->mb.cbp[h->mb.i_mb_xy] = (i_cbp_dc << 8) | (h->mb.i_cbp_chroma << 4) | h->mb.i_cbp_luma;

到这,基本上x264_macroblock_encode( h )(定义在Enc/encoder.c)基本上就分析完了.剩下的就是熵编码的部分了.以后的部分更需要的应该是耐心和数学知识吧,相对前面来说应该简单些.

总结:

1. 我对代码的理解应该还算比较深入,把代码的主线已经分析了出来,对代码中几个最难理解的地方(最难理解的地方就是帧的类型的判定,参考帧是如何管理的,一个16*16的块是采用到底需不需要分割,分割的话分成什么大小的,子块又采用何种预测方式,这些实际上就是整个编码的主线.)基本上已经明白,但有些过分复杂的函数的实现(或者涉及数学知识较多的地方)还有待深入研究,但我相信沿着这条主线应该能够继续深入下去,自己需要的是更多的时间和耐心. 自己需要的是更多的时间和耐心,争取以后能写出更详细更准确的流程分析,并尽量思考能改进的地方.

2.层次性,就像网络的7层结构一样,每一帧图像也可以分成很多层,只有对每层的语法结构(具体来说就是各个结构体中变量的意思)有了很好的理解,才有可能真正认清代码,这需要对标准认真研习.比如说量化参数,就在3个地方有定义,不读标准根本不会明白意思.

3. 很多过分复杂的东西不容易在本文中表达出来(比如说预测部分),只有通过自己的钻研才能真正悟到,直觉也很重要,还有就是信心了.看这种程序的收获就好像是真地肉眼看到了原子那样.

4.由于代码过分复杂,对某些函数的实现过程还没能彻底理解,比如说x264_macroblock_cache_load()函数的具体实现过程,我只是知道它的功能,实现过程还有待认真理解.dct变换是如何实现的,是如何计算残差的等等,这些都需要很多功夫,当然这里也需要大家的共同学习和交流.实现分工阅读不同代码部分并进行交流,才有可能对代码做到彻底的理解.

参考:http://blog.csdn.net/tjh666/article/details/1604130

分类: X264

时间: 2024-10-08 13:15:30

X264源代码分析——总纲入门最佳的相关文章

x264源代码简单分析:宏块分析(Analysis)部分-帧间宏块(Inter)

本文记录x264的 x264_slice_write()函数中调用的x264_macroblock_analyse()的源代码.x264_macroblock_analyse()对应着x264中的分析模块.分析模块主要完成了下面2个方面的功能: (1)对于帧内宏块,分析帧内预测模式(2)对于帧间宏块,进行运动估计,分析帧间预测模式 上一篇文章记录了帧内宏块预测模式的分析,本文继续记录帧间宏块预测模式的分析. 函数调用关系图 宏块分析(Analysis)部分的源代码在整个x264中的位置如下图所示

x264源代码简单分析:宏块分析(Analysis)部分-帧内宏块(Intra)

本文记录x264的 x264_slice_write()函数中调用的x264_macroblock_analyse()的源代码.x264_macroblock_analyse()对应着x264中的分析模块.分析模块主要完成了下面2个方面的功能: (1)对于帧内宏块,分析帧内预测模式(2)对于帧间宏块,进行运动估计,分析帧间预测模式 由于分析模块比较复杂,因此分成两篇文章记录其中的源代码:本文记录帧内宏块预测模式的分析,下一篇文章记录帧间宏块预测模式的分析. 函数调用关系图 宏块分析(Analys

x264源代码简单分析:熵编码(Entropy Encoding)部分

本文记录x264的 x264_slice_write()函数中调用的x264_macroblock_write_cavlc()的源代码.x264_macroblock_write_cavlc()对应着x264中的熵编码模块.熵编码模块主要完成了编码数据输出的功能. 函数调用关系图 熵编码(Entropy Encoding)部分的源代码在整个x264中的位置如下图所示. 单击查看更清晰的图片 熵编码(Entropy Encoding)部分的函数调用关系如下图所示.   单击查看更清晰的图片 从图中

x264源代码简单分析:滤波(Filter)

本文记录x264的x264_slice_write()函数中调用的x264_fdec_filter_row()的源代码.x264_fdec_filter_row()对应着x264中的滤波模块.滤波模块主要完成了下面3个方面的功能: (1)环路滤波(去块效应滤波)(2)半像素内插(3)视频质量指标PSNR和SSIM的计算 本文分别记录上述3个方面的源代码. 函数调用关系图 滤波(Filter)部分的源代码在整个x264中的位置如下图所示. 单击查看更清晰的图片 滤波(Filter)部分的函数调用关

x264源代码简单分析:x264命令行工具(x264.exe)

本文简单分析x264项目中的命令行工具(x264.exe)的源代码.该命令行工具可以调用libx264将YUV格式像素数据编码为H.264码流. 函数调用关系图 X264命令行工具的源代码在x264中的位置如下图所示. 单击查看更清晰的图片 X264命令行工具的源代码的调用关系如下图所示. 单击查看更清晰的图片 从图中可以看出,X264命令行工具调用了libx264的几个API完成了H.264编码工作.使用libx264的API进行编码可以参考<最简单的视频编码器:基于libx264(编码YUV

x264源代码简单分析:概述

最近正在研究H.264和HEVC的编码方式,因此分析了一下最常见的H.264编码器--x264的源代码.本文简单梳理一下它的结构.X264的源代码量比较大而且涉及到很多的算法,目前还有很多不懂的地方,因此也不能保证分析的完全正确.目前打算先把已经理解的部分整理出来以作备忘. 函数调用关系图 X264的函数调用关系图如下所示. 单击查看更清晰的大图 下面解释一下图中关键标记的含义. 函数背景色 函数在图中以方框的形式表现出来.不同的背景色标志了该函数不同的作用: 白色背景的函数:不加区分的普通内部

x264源代码简单分析:宏块编码(Encode)部分

本文记录x264的 x264_slice_write()函数中调用的x264_macroblock_encode()的源代码.x264_macroblock_encode()对应着x264中的宏块编码模块.宏块编码模块主要完成了DCT变换和量化两个步骤. 函数调用关系图 宏块编码(Encode)部分的源代码在整个x264中的位置如下图所示. 单击查看更清晰的图片 宏块编码(Encode)部分的函数调用关系如下图所示. 单击查看更清晰的图片 从源代码可以看出,宏块编码模块的x264_macrobl

Raid1源代码分析--读流程

我阅读的代码的linux内核版本是2.6.32.61.刚进实验室什么都不懂,处于摸索阶段,近期的任务就是阅读raid1的源码.第一次接触raid相关的东西,网上分析源码的资料又比较少,不详细.逐行阅读代码,做了笔记.如果要对raid1的读流程有个整体上的把握,需要将笔记中的主线提炼出来,这里不写了.理解不足或者有误之处,希望批评指正. 读流程主要涉及以下函数: 请求函数make_request 读均衡read_balance 回调函数raid1_end_read_request 读出错处理rai

“别踩白块儿&quot;游戏源代码分析和下载(一)

本帖最后由 lch123go 于 2014-7-9 17:51 编辑 "别踩白块儿"是目前非常火的一款游戏,游戏非常简单刺激.关于具体怎么火法怎么玩我就不多说了,相信看到本文的朋友们都非常地清楚. 什么游戏火,我们都想知道自己能不能也弄一个玩玩,我也花了点时间弄了一个,游戏代码将会开源,利人利己,大家一起提高,希望各位多多支持. 下面介绍如何用OGEngine游戏引擎完成"别踩白块儿"游戏的经典模式. 源码下载地址 一.最终实现的部分效果截图 1.刚开始时,最下面有