思考的救赎(二):三消游戏功能完善

来源:互联网 发布:端口映射和端口转发 编辑:程序博客网 时间:2024/06/06 00:00

一、引言

昨天我已经在上一篇博客里实现了三消游戏的初始化和消除功能,然而并没有实现一般三消游戏都有的重力降落、自动消除、随机补充的功能。这篇博客就是研究如何实现这些功能的。

让我们总结下我们将要实现的三消游戏的功能:

1. 初始化

2. 消除

3. 重力降落

4. 自动消除(重力降落后可能引起的消除操作)

5. 随机补充(补充消除格子)

6. 额外功能:重新开始、计算分数

在上一篇博客我们已经实现了第 1 步和第 2 步,现在我们开始接下来功能的实现吧。

ps:
想要了解上一篇博客内容的同学可以点击这里:
思考的救赎(一):三消游戏实现探索

想要获取本系列博客代码的同学可以点击这里:
wangying2016/ThreeClearGame

哈哈哈,先来看下我们的最终效果展示吧:

demo.gif

二、开始前的一点架构

我们要实现如上 Gif 图的效果,我们就要知道整个游戏的流程是什么样子的:

三消游戏流程图

这里我特意花了一个流程图(T_T 虽然有点不雅观),但是已经可以非常详细的说明我们一次消除过程中,到底程序做了什么。

也就是说,我们在消除两个格子之后,重力降落、自动消除和随机补充这三个行为是同步进行的(也就是一个结束了下一个才能开始的)。

并且由于自动消除的行为发生,我们会不停的随机补充新的格子,然后又可能会发生自动消除行为,最后又会触发我们的随机补充行为…

直到我们的当前网格里,已经没有三消可能了,才会停止下来。

这个过程的模拟,是由 SOUI 的定时器实现的。

1. 当我们成功消除格子之后,我们设置消除格子为删除皮肤,然后刷新界面。

// 刷新网格void CMainDlg::RefreshNet(std::vector<std::vector<Grid>> vecNet){    // 删除计时器    KillTimer(TIMER_LAND);    // 刷新网格    SOUI::STileView* pTileView = FindChildByName2<SOUI::STileView>(L"tv_net");    assert(pTileView);    CNetAdapter* pAdapter = static_cast<CNetAdapter*>(pTileView->GetAdapter());    if (pAdapter != nullptr) {        pAdapter->UpdateNet(vecNet);        pAdapter->notifyDataSetChanged();    }       // 设置分数    SetScore();    // 定时计算降落    SetTimer(TIMER_LAND, 700);}

2. 刷新界面的函数自动调用 SetTimer 函数 1s 后计时响应检查是否需要重力降落,如果是,则重力降落(每次只冒泡一个删除按钮)执行完后继续调用刷新界面的函数,直到所有的重力降落逻辑结束;当没有需要重力降落的格子了,就需要进行自动消除的逻辑,检查是否产生了自动消除操作,如果产生了自动消除操作,则调用随机补充函数,如果没有产生自动消除操作,我们就可以安心退出此逻辑了(证明当前界面已经没有三消可能了)。

以下是我们的计时器响应逻辑:

void CMainDlg::OnTimer(UINT_PTR idEvent){    // 降落计算    if (idEvent == TIMER_LAND) {        // 清除计数器        KillTimer(TIMER_LAND);        // 是否有需要降落的点        if (m_NetMatrix.LandOneGrid()) {            RefreshNet(m_NetMatrix.GetNet());        }         // 没有需要降落的点        else {            // 是否产生了自动消除            // 如果没有产生自动消除,则意味着操作结束,可以开始随机补充了            if (!m_NetMatrix.AutoDelete()) {                // 随机补充一行                m_NetMatrix.AddRandomGrid();            }        }    }}

三、重力降落

重力降落,即当我们消除了指定区域的格子之后,在空白区域上方的格子能够自动掉落下来。

这个功能归根结底,是类似于冒泡的一种移动操作:

我们要做的,就是将每一列上的空白按钮往上移动,直到在它上面没有其他的非空白按钮为止

而幸而有我们上述讨论的架构设计,我们只需要一次冒泡一个删除按钮即可(也就是说我们的重力降落函数会在定时器内多次调用,直到没有需要降落的格子了为止)

这里就是我们的重力降落的函数:

// 重力降落bool NetMatrix::LandOneGrid(){    // 标记是否发生了重力降落    bool bNeedLand = false;    // 遍历每列    for (int col = 0; col < NET_COL_NUMBER; ++col) {        // 从下开始遍历每行        for (int row = NET_ROW_NUMBER - 1; row >= 1; --row) {            // 让删除点冒泡上去            if (m_vecNet[row][col].status == Grid_Delete   &&                m_vecNet[row - 1][col].status != Grid_Delete) {                // 标记发生了重力沉降                bNeedLand = true;                // 依次挪动删除点上去                int count = row;                do {                    std::swap(m_vecNet[count][col], m_vecNet[count - 1][col]);                } while (--count >= 1 && m_vecNet[count - 1][col].status != Grid_Delete);                break;            }        }    }    return bNeedLand;}

简单讲解下代码:

1. 首先,遍历每一列,这是最外层循环

2. 然后,在每一列中遍历每一行,从底部往上遍历

3. 最后,中间的 do while 循环依次将一个空白按钮往上移动

这里的 bNeedLand 变量是用来记录是否发生了重力降落事件的,如果没有发生重力降落事件,我们就认为重力降落部份结束。

四、自动消除

当我们重力降落完了之后,就有可能发生新的三消可能,此时我们就需要被动的消除这些格子。

在上一篇博客里,我们在实现用户主动消除格子的时候,定义了一个 GetCancelPoints 函数,这个函数可以根据一个操作点,将其四周与其图像相同的格子返回,而这里,我们也可以利用这个函数来返回我们想要的消除点。

以下是我们自动消除的代码:

// 被动产生消除(重力降落)bool NetMatrix::AutoDelete(){    // 记录有连续情况的点    std::vector<PosPoint> points;    // 行检查是否有连续 3 个    for (int i = 0; i < NET_ROW_NUMBER; ++i) {        for (int j = 0; j < NET_COL_NUMBER - 2; ++j) {            if (m_vecNet[i][j].status == m_vecNet[i][j + 1].status     &&                m_vecNet[i][j + 1].status == m_vecNet[i][j + 2].status &&                m_vecNet[i][j].status != Grid_Delete) {                points.push_back(PosPoint(i, j));                j += 3;            }        }    }    // 列检查是否有连续 3 个    for (int j = 0; j < NET_COL_NUMBER; ++j) {        for (int i = 0; i < NET_ROW_NUMBER - 2; ++i) {            if (m_vecNet[i][j].status == m_vecNet[i + 1][j].status     &&                m_vecNet[i + 1][j].status == m_vecNet[i + 2][j].status &&                m_vecNet[i][j].status != Grid_Delete) {                points.push_back(PosPoint(i, j));                i += 3;            }        }    }    // 如果没有找到这样的点,则证明没有被动产生消除    if (points.size() == 0) return false;    // 根据点获取消除点    std::set<PosPoint> cancelPoints;    for (int i = 0; i < points.size(); ++i) {        std::vector<PosPoint> tempPoints = GetCancelPoints(points[i], m_vecNet);        cancelPoints.insert(tempPoints.begin(), tempPoints.end());    }    // 计算分数    m_nScore += cancelPoints.size();    // 输出自动消除点的信息    SOUI::SStringW strAutoCancelPoints, strAutoCancelMsg;    for (auto it = cancelPoints.begin(); it != cancelPoints.end(); ++it) {        SOUI::SStringW strPoint;        strPoint.Format(L"(%d, %d)", it->row, it->col);        strAutoCancelPoints += strPoint;    }    strAutoCancelMsg.Format(L"执行自动消除:%s", strAutoCancelPoints);    MyHelper::Instance()->WriteLog(strAutoCancelMsg);    // 设置以上点为删除状态    for (auto it = cancelPoints.begin(); it != cancelPoints.end(); ++it) {        m_vecNet[it->row][it->col].status = Grid_Delete;    }    m_event->RefreshNet(m_vecNet);    return true;}

简单介绍下代码逻辑:

1. 首先获取产生了三消可能的点

2. 根据上述查找到的点,调用 GetCancelPoints 函数将其周围相同图像的点全部找出来

3. 输出消除消息

4. 计算分数(默认消除一个格子得 1 分 ^_^)

5. 设置消除点为删除皮肤

6. 刷新界面(这是非常重要的一步,除了刷新了界面,还隐含调用了定时器检查是否触发了重力降落,从而引起一系列循环逻辑)

五、随机补充

当我们完成了自动消除之后,并且已经保证没有再有能够重力降落和自动消除的点了之后,我们就需要随机补充格子了。

随机补充其实非常简单,我们只需要每次掉用的时候在第一行补充格子即可,然后调用刷新界面的函数,其就会自动调用重力降落的逻辑,从而又触发了:

刷新界面 -> 重力降落 -> 自动消除 -> 随机补充

的循环,直到,我们补充的格子无法被自动消除之后结束。

这是我们的随机补充的代码:

// 随机补充格子void NetMatrix::AddRandomGrid(){    // m_vecNet 已经成为了一个删除按钮全部在上面,    // 图像按钮全部在下面的矩阵了,此时需要做的是:    // 1. 在首行随机插入点    // 2. 执行一次沉降    // 遍历首行    for (int col = 0; col < NET_COL_NUMBER; ++col) {        if (m_vecNet[0][col].status == Grid_Delete) {            int random = MyHelper::Instance()->Random(4);            switch (random)            {            case 0: m_vecNet[0][col].status = Grid_Star; break;            case 1: m_vecNet[0][col].status = Grid_Heart; break;            case 2: m_vecNet[0][col].status = Grid_Sword; break;            case 3: m_vecNet[0][col].status = Grid_Shield; break;            }        }    }    m_event->RefreshNet(m_vecNet);}

代码逻辑非常简单,这里不作过多叙述了。

六、总结

至此,我们的三消游戏已经全部完成了:)

基本上的功能都有了,剩下的也就是优化下逻辑和代码,检查下用户的边界性操作之类的云云。

花了三天的时候,从一开始的构思、架构,到后面的初始化算法的实现、消除算法的时候,再到最后的重力降落、自动消除和随机补充的循环,都是我一个石头一个石头摸着走过来的。当最终看到了效果之后,开心的简直要哭出来!

这就是编程的乐趣!
这就是创造的乐趣!
这就是思考的魅力!

我爱编程,我爱创造,我爱思考!

明天还得继续!

To be Stronger!

阅读全文
0 0