尝试实现《Puyo Puyo》(2)

上一文,我们设计好了数据格式,并且实现了基本操作,游戏初见雏形。本文主要解决两个问题:随机方块颜色和连锁消除。

先挑软柿子捏。随机颜色,实现上也就是随机数,暂时不考虑性能,直接使用rand()

struct BlockPair {    
    BlockPair(int x = 4, int y = 1) : pivot{x, y}, attach{x, y - 1}
    {
        srand(time(nullptr));
        render();
    }
    void randomColor()
    {
        cp = static_cast<ColorFlag>(rand() % 4 + 1);
        ca = static_cast<ColorFlag>(rand() % 4 + 1);
    }
    void render()
    {
        randomColor();
        gPivot() = cp;
        gAttach() = ca;
    }
    void spawn()
    {
        randomColor();
        pivot = {4, 1};
        attach = {4, 0};
    }
}

添加一个randomColor函数,修改相关代码即可。

关于连锁消除上文提到了无向图。我搜索了相关内容,结果没有别的头绪。下面的算法功能上没有问题,不雅观就是…:

void buildChain(int x,
                int y,
                const Pos& pos,
                std::unordered_set<Pos>& dset,
                std::unordered_multimap<Pos, Pos>& dmap)
{
    ColorFlag flag = grids[pos.x][pos.y];
    if(grids[x][y] == Empty || dset.find(Pos{x, y}) != end(dset)) {
        return;
    }
    dset.insert(Pos{x, y});
    dmap.insert({pos, Pos{x, y}});
    if(x + 1 < gridWidth && grids[x + 1][y] == flag) {
        buildChain(x + 1, y, pos, dset, dmap);
    }
    if(y + 1 < gridHeight && grids[x][y + 1] == flag) { 
        buildChain(x, y + 1, pos, dset, dmap); 
    } 
    if(x > 0 && grids[x - 1][y] == flag) {
        buildChain(x - 1, y, pos, dset, dmap);
    }
    if(y > 0 && grids[x][y - 1] == flag) {
        buildChain(x, y - 1, pos, dset, dmap);
    }
}

解释一下,我们将整个网格划根据颜色分成不同的链,每个链其实都是无向图。构造链条方法就是暴力搜索,每个格子分别检查上下左右是否出界,是否和自己是相同颜色,是否已经被记录过。

dmap当然就是链了,为了方便,直接使用multimap。这个数据结构比较冷门,它和map的区别在于一个key可以存多个值,这刚好符合我们的抽象数据结构。我们每检索一个新的格子,就将其当作key,先插入他自身,再插入其他相同颜色的格子。这样就需要我们对每个格子作build操作。正所谓buildChain。

这里有个显然的事实,一个格子只被记录一次。所以dset就是用来保存被检索过的格子,同样为了方便直接使用set

 

bool clearChains()
{
    std::unordered_set<Pos> dset;
    std::unordered_multimap<Pos, Pos> dmap;

    for(int w = 0; w < gridWidth; ++w) {
        for(int h = 0; h < gridHeight; ++h) { 
            buildChain(w, h, Pos{w, h}, dset, dmap); 
        } 
    } 
    for(auto m : dmap) { if(dmap.count(m.first) >= 4) {
            grids[m.second.x][m.second.y] = Empty;
        }
    }
    dset.clear();
    dmap.clear();
    fallDown();
    for(int w = 0; w < gridWidth; ++w) {
        for(int h = 0; h < gridHeight; ++h) { 
           buildChain(w, h, Pos{w, h}, dset, dmap); 
        } 
    } 
    auto res = std::find_if(begin(dmap), end(dmap), 
        [&](const auto& it) { return dmap.count(it.first) >= 4; });
    if(res == end(dmap)) {
        return false;
    }
    return true;
}

从上面代码可以看出流程:先遍历一次格子,构建好所有链。接着对每个长度大于等于4的链,将其值赋为Empty。

理论上这样就结束了,不过Puyo是连锁消除的,也就是每消除一次之后,下落的方块刚好构成第二次消除的条件。(这部分算法有冗余的地方,我还没想好怎么简化)我的策略是先让悬空方块下落,清理dset和dmap再buildChain第二次。这时如果找到长度大于等于4的链就返回true,否则返回false。在上层处理方块下落的位置搞一个循环反复清理就OK了:

// get bottom
if(v > 0) {
    // falling rule
    fallDown();
    
    while(clearChains()) {
    };
    spawn();
}

 

暂时来看,你已经有了一个可玩的Puyo游戏。虽然简陋,但五脏俱全。

稍稍总结一下。数据驱动的设计在前端(包括Web和Desktop)已经非常成熟,本次实验也感受到其便利。我不清楚当前游戏工业的软件架构是什么样,但绝对不是15年前所鼓励的面向对象。至少大部分考虑到性能的场景,都会采用面向数据的实体,这样做强行分离了数据和行为,非常不符合面向对象的哲学。与其形而上学地设计一套框架,不如从直接操纵数据,所谓自底向上的抽象。

后期可以加一些补间动画,再搞一些华而不实的特效。那么,有缘再续。

Leave a Reply

Your email address will not be published. Required fields are marked *