更新精灵位置时出现奇怪的行为

6
我正在使用SDL库用C++编写一个简单的地牢游戏,但我在屏幕上移动我的角色时遇到了一些问题。每当需要呈现一帧时,我都会使用update()函数更新精灵的位置,如果玩家静止不动,则什么也不做。为了发出移动命令并开始动画,我使用step()函数,每次玩家从一个图块移动到另一个图块时只调用一次。收到“向上”命令后,游戏表现良好,角色在一秒钟内平稳移动到新位置。然而,当给出“向下”命令时,他的速度减半,并且显然在一秒钟过去后,他会瞬间“传送”到最终位置,出现突然的闪烁。移动代码基本相同,但是一个情况下增量移动被加到y位置中,在另一个情况下则被减去。也许位置为整数而增量为双精度浮点数导致了问题?加法和减法行为是否不同(可能存在不同的舍入)?以下是相关代码(对于长度我很抱歉):
void Player::step(Player::Direction dir)
{
    if(m_status != STANDING) // no animation while standing
        return;

    switch(dir)
    {
    case UP:
        if(m_currMap->tileAt(m_xPos, m_yPos - m_currMap->tileHeight())->m_type == Tile::FLOOR)
        {
            // if  next tile is not a wall, set up animation
            m_status = WALKING_UP;
            m_yDelta = m_currMap->tileHeight(); // sprite have to move by a tile
            m_yVel = m_currMap->tileHeight() / 1000.0f; // in one second
            m_yNext = m_yPos - m_currMap->tileHeight(); // store final destination
        }
        break;
    case DOWN:
        if(m_currMap->tileAt(m_xPos, m_yPos + m_currMap->tileHeight())->m_type == Tile::FLOOR)
        {
            m_status = WALKING_DOWN;
            m_yDelta = m_currMap->tileHeight();
            m_yVel = m_currMap->tileHeight() / 1000.0f;
            m_yNext = m_yPos + m_currMap->tileHeight();
        }
        break;

    //...

    default:
        break;
    }

    m_animTimer = SDL_GetTicks();
}

void Player::update()
{
    m_animTimer = SDL_GetTicks() - m_animTimer; // get the ms passed since last update

    switch(m_status)
    {
    case WALKING_UP:
        m_yPos -= m_yVel * m_animTimer; // update position
        m_yDelta -= m_yVel * m_animTimer; // update the remaining space
        break;
    case WALKING_DOWN:
        m_yPos += m_yVel * m_animTimer;
        m_yDelta -= m_yVel * m_animTimer;
        break;

    //...

    default:
        break;
    }

    if(m_xDelta <= 0 && m_yDelta <= 0) // if i'm done moving
    {
        m_xPos = m_xNext; // adjust position
        m_yPos = m_yNext;
        m_status = STANDING; // and stop
    }
    else
        m_animTimer = SDL_GetTicks(); // else update timer
}

编辑:我删除了一些变量,只留下了经过的时间、速度和最终位置。现在它可以平滑地移动,但向下和向右的移动速度明显比向上和向左的慢。仍然不知道为什么...

编辑2:好的,我弄清楚了这是为什么。正如我首先所猜测的那样,在将双精度转换为整数时,加法和减法有不同的舍入方式。如果我进行如下强制转换:

m_xPos += (int)(m_xVel * m_animTimer);

动画速度相同,问题得到解决。

@KenWayneVanderLinde,m_yDelta变量存储了到达该位置所需行走的剩余像素,因此在两种情况下都应该递减。 - Pietro Lorefice
m_yPosm_yVelm_animTimer的类型是什么?我认为m_animTimer应该始终更新,否则在下一次调用update时会得到虚假值。 - Piotr Praszmo
更好的方法是,如果m_yDelta实际上是在一秒钟内完成的步骤,并且速度包含在该计算中。然后,您通过将delta乘以animTimer来更新m_yPos,而delta永远不会改变。这将是每秒钟的恒定步长。(免责声明:我可能在这里说了一些无意义的话,我现在有点困。) - Xeo
@Xeo 谢谢,我会尝试这个解决方案并告诉你。 - Pietro Lorefice
你是否在调试器中运行代码,以确定DOWN状态下的m_yVel是什么,以及它是否与UP状态下的m_yVel不同? - brendanw
显示剩余3条评论
2个回答

3

请考虑以下内容:

#include <iostream>

void main()
{
    int a = 1, b = 1;
    a += 0.1f;
    b -= 0.1f;

    std::cout << a << std::endl;
    std::cout << b << std::endl;
}

当将float转换为int进行隐式转换时,a和b被赋值时小数点后的所有内容将被截断而不是四舍五入。该程序的结果为:

1
0

您说m_yPos是整数,而m_yVel是双精度浮点数。请考虑以下情况:如果m_yVel * m_animTimer的结果小于1,则在Player::update中会发生什么。在UP情况下,您的精灵将向下移动一个像素,但在DOWN情况下,您的精灵将不会移动,因为如果将小于1的数字添加到整数中,什么也不会发生。尝试将位置存储为双精度浮点数,并且只在需要将它们传递给绘图函数时将它们转换为整数。
一种确保在转换过程中进行四舍五入而不是截断的技巧是在将浮点值分配给整数时始终添加0.5。
例如:
double d1 = 1.2;
double d2 = 1.6;
int x = d1 + 0.5;
int y = d2 + 0.5;

在这种情况下,x将变为1,而y将变为2。

这正是我在你回答之前几秒钟得出的相同结论。我会将你的答案标记为采纳。 - Pietro Lorefice

1

我宁愿不进行增量计算。这样更简单,即使你回到过去也可以得出正确的结果,不会因精度丢失而受到影响,并且在现代硬件上速度会一样快,甚至更快:

void Player::step(Player::Direction dir)
{
    // ...
        case UP:
        if(m_currMap->tileAt(m_xPos, m_yPos - m_currMap->tileHeight())->m_type == Tile::FLOOR)
        {
            // if  next tile is not a wall, set up animation
            m_status = WALKING_UP;
            m_yStart = m_yPos;
            m_yDelta = -m_currMap->tileHeight(); // sprite have to move by a tile
            m_tStart = SDL_GetTicks(); // Started now
            m_tDelta = 1000.0f; // in one second
        }
        break;
    case DOWN:
        if(m_currMap->tileAt(m_xPos, m_yPos + m_currMap->tileHeight())->m_type == Tile::FLOOR)
        {
            m_status = WALKING_DOWN;
            m_yStart = m_yPos;
            m_yDelta = m_currMap->tileHeight();
            m_tStart = SDL_GetTicks(); // Started now
            m_tDelta = 1000.0f; // in one second
        }
        break;
    // ...
}

void Player::update()
{
    auto tDelta = SDL_GetTicks() - m_tStart;

    switch(m_status)
    {
    case WALKING_UP:
    case WALKING_DOWN:
        m_yPos = m_yStart + m_yDelta*tDelta/m_tDelta; // update position
        break;

    default:
        break;
    }

    if(tDelta >= m_tDelta) // if i'm done moving
    {
        m_xPos = m_xStart + m_xDelta; // adjust position
        m_yPos = m_yStart + m_yDelta;
        m_status = STANDING; // and stop
    }
}

非常感谢,它看起来比我的解决方案更好。我会试一下。 - Pietro Lorefice

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接