张银峰的编程课堂

回退一步

面对一个复杂的关卡,一步操作失误使得箱子不可再移动,那就宣告了我们需要从头开始。既然是无心之举,不用担心,我们可以让游戏回退一步。回退的原理就是记录动作变化前的所有状态,之后在需要时恢复到最近的状态。

从我们的实现可以看到,角色移动后,地图瓦片会被改写,因此这是需要记录的状态之一。一个瓦片的状态可由其位置与此处的元素描述:

typedef struct tagTILE
{
    POS p;
    enum ELEMENT e;
} TILE;

行走一步会引起两个瓦片发生变化;而推动箱子则会引发三个。因此我们需要一个步阶信息,记录一个操作引发的所有变化,它由3个TILE构成,同时使用tex记录了角色此时的纹理,这是另一个变化状态。

typedef struct tagSTEP
{
    TILE t0;
    TILE t1;
    TILE t2;
    int  tex;
} STEP;

如果游戏只允许回退一次,那么这已经可行了。我们希望设计更灵活一些,允许指定玩家可回退的步数,这里选择最少回退0步(不可回退),最多3步,这由UNDO结构描述

// 悔步结构
typedef struct tagUNDO
{
    int  index;     // 步阶索引
    STEP steps[3];  // 可回退步阶数组
    int  max;       // 最大可回退步阶数(0-3)
    int  count;     // 已回退步阶数
} UNDO;

回退的实现

首先我们需要一个重置函数,方便我们在载入关卡时复位所有回退状态。这其中重要的是重置索引与计数,至于STEP数组的中内容,并不重要,因此这里全置为了0。

void Undo_Reset()
{
    memset(&g_undo, 0, sizeof(UNDO));
    g_undo.index = -1;
    g_undo.max = 3;
    g_undo.count = 0;
}

接下来我们需要实现状态记录函数,这个函数在任意可行走动作前调用,以便收集变化前的状态。当数组填满时,我们将移出最早记录的状态,将最新的状态记录在g_undo.steps[2]处。

void Undo_Push(int tex, POS p0, POS p1, POS p2)
{
    g_undo.index += 1;
    g_undo.count = 0;

    // 移除较早的步阶信息
    if (g_undo.index > 2)
    {
        g_undo.steps[0] = g_undo.steps[1];
        g_undo.steps[1] = g_undo.steps[2];
        g_undo.index = 2;
    }

    int i = g_undo.index;
    g_undo.steps[i].t0.p = p0;
    g_undo.steps[i].t1.p = p1;
    g_undo.steps[i].t2.p = p2;
    g_undo.steps[i].t0.e = g_map[p0.r][p0.c];
    g_undo.steps[i].t1.e = g_map[p1.r][p1.c];
    g_undo.steps[i].t2.e = g_map[p2.r][p2.c];    
    g_undo.steps[i].tex = tex;
}

最后,我们看看回退一步是如何实现的。我们从回退数组中拿出最近一次的状态s,然后更新地图即可,就这么简单!

void Undo_Pop()
{
    if (g_undo.index >= 0 && g_undo.count < g_undo.max)
    {
        const STEP* const s = &g_undo.steps[g_undo.index];

        g_player_pos = s->t0.p;
        g_player_tex = s->tex;

        g_map[s->t0.p.r][s->t0.p.c] = s->t0.e;
        g_map[s->t1.p.r][s->t1.p.c] = s->t1.e;
        g_map[s->t2.p.r][s->t2.p.c] = s->t2.e;

        g_undo.index -= 1;
        g_undo.count += 1;
    }
}

glimix.com