首页 > 代码库 > 桌面山寨版2048—游戏逻辑篇之移动方块的框架

桌面山寨版2048—游戏逻辑篇之移动方块的框架

         昨天看到博客(www.richinmemory.com)的流量统计,居然还有一位朋友评论了,感动的满眼都是泪啊!谢谢支持啊!为了使互动的朋友更方便的互动,今天我加了个能用微博等帐号登录评论的插件。需要源码的朋友可以直接发信到我的邮箱。猛戳之后(www.richinmemory.com)若觉得还过得去,可以尝试收藏啊,亲。有朝一日有幸流量稳定了,我就开始放弃这边更新了,不过这个肯定还要很久很久才能达到。

二、桌面山寨版2048—游戏逻辑篇之移动方块

         这个小游戏的基本逻辑就是:合并同类项。玩家通过上下左右键操纵游戏方块,然后操纵方向上所有相邻相同的数字的方块会被合并,合并之后方块上的数字会被更新并且改变颜色。

         对于逻辑推理这件事,我一直深深相信的就是“从1到2再到无穷”的方式,这还是我高中老师教会我的方法,对于任何一个有规则但是复杂的事务,先从简单的开始总是不会错的。于是,首先考虑只有两个方块在一个方向进行操作(我选择的是“下”这个方向)的时候。根据在界面篇中的原则,所有的游戏方块都是被保存在一个ItemBox的数组里并且在程序的一开始就进行了初始化,所以,现在要处理的逻辑就是如何控制每个方格里方块的显示与否。

         如果只有两个游戏方块时,用户按下了“下”方向键,要考虑的情况有两种:一是两个游戏方块不在一列,二是两个游戏方块位于一列。首先思考一下情况一的行为模式应该是:两个游戏方块都应该向下移动直到它们碰到了游戏区域的边框。这个逻辑并不难实现,最简单的办法,采用一个循环,遍历所有的方格,如果当前方格的bshow属性是false,那么不进行操作,否则,获取当前方格的位置信息(坐标),文本,颜色。将当前列的最后一行的方格赋予相同的文本和颜色,同时将当前游戏方块的信息清空(方块颜色设置为背景色,文本清空),刷新界面,这样就可以造成当前游戏方格“移动”到最后一行的假象。当一个方向做成功之后,另外三个方向只要以此类推就可以了。虽然说就这个逻辑和最终的逻辑还相差十万八千里,甚至对于整个游戏逻辑来说甚至是不正确的。但是如果没有学会加法,那么你怎么也不会学会三重积分吧。

         忘记上面一段扯的废话,现在来考虑情况二,如果两个方块位于一列,就要考虑到合并的情况(先假设两个方块的文本是一样的)。这时按下方向键,正确的期望结果是两个方块合并并且位于当前列的最后一行,这里的做法思路有太多了。我最最最开始的第一脑想法就是从(0,0)位置的方块开始遍历,如果bshow是false,就不用做任何操作,如果不是,首先检查当前列最后一行的方块bshow的属性,如果是false,那么和上面情况一样,设置当前列的最后一行的方块和当前方块信息一致并清空当前方块信息。否则,则说明需要合并,这时只要将当前列最后一行方块升一级(比如”2->4”)并清空当前方块信息就可以了。如此,两个方块的移动和合并已经做完了。可以按照这个思路先先出个代码试试。

         现在已经考虑完“1”的情况了,下面要开始进一步,考虑“2”的情况了。当两个初始方块合并完毕以后,开始思考又多生成了一个新的方块该怎么办。这里也有好几种情况需要被考虑。

         第一种,第一次出现的两个方块已经合并,新的方块出现并且和被合并的方块不在一列上(还是先只考虑一个方向上的情况)。这种最简单,也就是两个不在一列的方块而已,上面已经说过了。

         第二种,同样是最初出现的两个方块已经合并,新的方块与旧的方块在同一列中。这时新出现的方块和已经合并的方块文字不一样,不可能发生合并(暂时先从最简单的情况开始)。所以这时的思路是上面的一个扩展,也是从左上角的位置进行遍历,依次判断每个方块的bshow是否是true,如果是,那么这个移动方式就有点不同了,由于目前已知最后一行是已经合并的方块,所以你知道当前这个方块应该在倒数第二行上出现。但是如果我们不知道当前列最后一个bshow为false的行是多少该怎么办?怎么使用程序来找到呢?由于我们知道当前位置的纵坐标横坐标,所以从最后一行开始,依次向上遍历,如果遇到bshow为false就立马退出循环并且记录下当前的行坐标。这样就能完成上面所说的任务,有了这个坐标就可以知道当前的游戏块应该放在哪个位置,其余的操作就和前面所说的一致就可以了。

         第三种,最初的两个方块没有合并,那么这种情况肯定和前面只有两个方块之中的某一种是一样的。

         现在我们已经处理了”2”的情况,那么这种”2”的情况能不能推及到无穷呢?如果这时又出现了一个新的方块,那么情况能不能直接套用上面的思路呢?稍微想一想,总感觉可能可以又可能不可以,因为“无穷”的情况实在是太多了。我总结了下,用一个图表示,虽然在实际情况中不可能同时出现这四列,但是四列单独出来,都是可能出现的:

        

         第一列和第四列的情况最简单,直接移动合并就可以,具体步骤前面已经描述过了。

         第二列,需要判断出同一列的下一行的文字和当前的文字不相同,只能移动不能发生合并。

         第三列,有两个能合并的地方,列1和列2的“2”可以合并,列3和列4的“4”可以合并,而且合并后,两个“2”合并出的“4”要显示在第三行,“4”合并出来的“8”要出现在第四行。

         人脑思维分析完了,现在就要转换成为电脑的思维考虑如何用程序实现。按照前面的思维模式下来,从左上角第一个游戏方块开始进行遍历,如果遇到当前行的bshow是true,取得当前游戏方块的列标,从当前列最后一行开始,依次判断当前的bshow是否是false,如果是,记录下当前的行序号并且返回。获得这个返回的行序号之后,判断这个行序号的下一行(当然是同一列)和当前行的游戏方块文字是否一致(有点绕,意思你懂的就行),如果一致,那么要考虑合并,并且重新设置信息。

         考虑到这里,貌似觉得逻辑都屡顺了,我当时反正就是这么想的。于是第一次我写的代码是这样的,GetCoordsFromIndex(i,it)是我写的一个函数,目的是根据当前游戏方块的序号得到游戏方块的纵坐标和横坐标,这个使用除法和取余操作很容易做到:

    if(nChar == VK_DOWN)
    {
        for(int i=0;i<nTotalGrids-1;i++)
        {
            if(m_itemBoxArray[i].bShow)
            {
                GetCoordsFromIndex(i,it); 
                for(int j=nTotalGrids - (m_nRowAndCol-it.nColIndex);j>i;j-=m_nRowAndCol)
                {
                    // 找到当前列bshow为false的最大行序号
                    if(!m_itemBoxArray[j].bShow)
                    {
                        m_itemBoxArray[i].bShow = false;
                        m_itemBoxArray[j].bShow = true;    
                        m_itemBoxArray[j].strItemText = m_itemBoxArray[i].strItemText;
                        m_itemBoxArray[i].strItemText = _T("");
                        GetCoordsFromIndex(j,it);
                        break;
                    }
                }
                //合并
                if ( it.nRowIndex < m_nRowAndCol -1 )
                {
                    ItemBox itCurrent = m_itemBoxArray[GetIndexFromCoords(it)]; 
                    ++it.nRowIndex;
                    ItemBox itNextRow = m_itemBoxArray[GetIndexFromCoords(it)];
                    
                    if ( itNextRow.bShow &&
                         itNextRow.strItemText == itCurrent.strItemText 
                        )
                    {
                        m_itemBoxArray[GetIndexFromCoords(it)].strItemText = m_arrStrItemTexts[GetIndex(itNextRow.strItemText)+1];
                        --it.nRowIndex;
                        m_itemBoxArray[GetIndexFromCoords(it)].bShow = false;
                        m_itemBoxArray[GetIndexFromCoords(it)].strItemText = _T("");
                    }
                }
            }
        }
    }

         但是一测试,发现貌似情况没有向我想像的方向走啊。我观察到了一个现象,那就是按照这个思路,上图列3的行为完全不正确。然后我又回到了这个代码,毕竟走到这一步,从代码的角度出发更容易找出问题所在,这也不违背数学归纳法的原则。如果按照这个代码,列3这种情况就会出现这样一种情况,由于我们是从左上角开始遍历的,那么第一行的2和第二行的2合并之后成为第二行的4,遍历继续,当遍历到第三行的4时,决定与第四行的4进行合并,这样就形成了第四行的8,但是此时,第二行的4应该移动到第三行。然而,按照我们的代码,我们已经遍历过了第二行,不可能再回去了,所以就造成了在错误,就会造成合并的4和合并的8分别在第二行和第四行,第三行空出来了,这明显是不正确的。如何解决这个问题呢?通过分析可以发现,这个问题就是因为我们的遍历顺序有问题,那么就简单了,在向下这方向上,只要从最后一个游戏方格向第一个方格进行遍历这个问题就迎刃而解,具体逻辑同志们可以再思考思考。

        这样之后,带着洋洋得意的心情再次编译,尝试逻辑是否正确,结果发现还是太naive,问题又出现了,如果同一列出现了4个”2“,这时按一下”下“方向键,这4个”2“会直接合并成一个”8“,根据原游戏的规则,这也是不对的,毕竟我们是山寨,山寨就得有点诚意,不能乱窜改原先的游戏规则。这个问题又如何解决呢?首先得找出这个问题出现的原因,仔细思考下不难发现,原来游戏的规则是一次操作中,合并过的游戏方块不可以再次合并,那么,这个问题很容易解决,使用在封装中的另一个成员变量bJoin,标识已经合并的方块。所以最终”下“这个方向的代码如下所示:

    if(nChar == VK_DOWN)
    {
        for(int i=nTotalGrids-1;i>=0;i--) //从后向前遍历
        {
            if(m_itemBoxArray[i].bShow)
            {
                GetCoordsFromIndex(i,it); 
                // 查询同一列的bshow为false的最大行序号
                for(int j=nTotalGrids - (m_nRowAndCol-it.nColIndex);j>i;j-=m_nRowAndCol)
                {
                    if(!m_itemBoxArray[j].bShow)
                    {
                        m_itemBoxArray[i].bShow = false;
                        m_itemBoxArray[j].bShow = true;    
                        m_itemBoxArray[j].strItemText = m_itemBoxArray[i].strItemText;
                        m_itemBoxArray[i].strItemText = _T("");
                        GetCoordsFromIndex(j,it);
                        break;
                    }
                }
                // 合并
                if ( it.nRowIndex < m_nRowAndCol -1 )
                {
                    ItemBox itCurrent = m_itemBoxArray[GetIndexFromCoords(it)]; 
                    ++it.nRowIndex;
                    ItemBox itNextRow = m_itemBoxArray[GetIndexFromCoords(it)];
                    
                    if ( itNextRow.bShow &&
                         itNextRow.strItemText == itCurrent.strItemText &&
                         !itNextRow.bJoin // 判断当前块有无被合并过
                        )
                    {
                        m_itemBoxArray[GetIndexFromCoords(it)].strItemText = m_arrStrItemTexts[GetIndex(itNextRow.strItemText)+1];
                        m_nScore += 2*pow(2.0, GetIndex(itNextRow.strItemText)+1 );
                        m_itemBoxArray[GetIndexFromCoords(it)].bJoin = true; // 设置当前块已经被合并
                        --it.nRowIndex;
                        m_itemBoxArray[GetIndexFromCoords(it)].bShow = false;
                        m_itemBoxArray[GetIndexFromCoords(it)].strItemText = _T("");
                    }
                }
            }
        }
    }

          其余三个方向的代码依”下“方向类推,但要注意遍历顺序,具体代码可进入博客,那里有我的邮箱,若需要源代码,可以留言或者发信给我。到这里,对于一个完整的游戏,逻辑只能说完成了一个框架,还不能成为一个合格的山寨品,所以请看下一篇”逻辑面之缓缓出现的细节像风叶“。