【问题标题】:Strange behaviour updating sprite position奇怪的行为更新精灵位置
【发布时间】:2012-01-16 06:05:08
【问题描述】:

我正在使用 SDL 库在 C++ 中编写一个简单的 roguelike 游戏,但在屏幕上移动我的角色时遇到了一些问题。每次需要渲染一帧时,我都会使用 update() 函数更新精灵的位置,如果玩家静止不动,该函数不会执行任何操作。为了发出移动命令,从而启动动画,我使用 step() 函数,每次玩家从一个瓷砖移动到另一个瓷砖时只调用一次。收到“向上”命令后,游戏表现良好,角色在一秒钟内平稳移动到新位置。然而,在下达“下”命令的时候,他以大约一半的速度移动,显然一秒过去后,他瞬间被“传送”到了最终位置,突然闪烁了一下。运动的代码基本相同,但在一种情况下,增量运动与 y 位置相加,在另一种情况下被减去。也许位置是整数而增量是双精度的事实会引起问题? sum 和 subract 的行为是否不同(可能是不同的舍入)?以下是相关代码(见长见谅):

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);

动画速度一样,问题就解决了。

【问题讨论】:

  • 可能是错误的,但是在Player::update 中,在WALKING_DOWN 的情况下,两行不应该使用+= 而不是-= 吗?我只是猜测WALKING_DOWN 应该与WALKING_UP 完全相反。不确定这是否与您的问题有关。
  • @KenWayneVanderLinde m_yDelta 变量存储要步行到达该位置的剩余像素,因此在这两种情况下都应该递减。
  • m_yPosm_yVelm_animTimer的类型是什么?而且,我认为m_animTimer 应该始终更新,否则您将在下次调用update 时获得虚假价值。
  • @Banthar m_yPos 是一个整数,如m_animTimer,而m_yVel 是一个双精度数。 m_animTimer 会在玩家处于移动状态的每一帧中更新。仅当WALKING_DOWN 状态为当前状态时才会显示问题,因此我认为计时器不是问题。
  • 如果m_yDelta 实际上是在一秒钟内完成的步骤,并且速度包含在该计算中,则更好的方法是。然后,您通过将 delta 乘以 animTimer 来更新 m_yPos,并且 delta 永远不会改变。那将是每秒一个恒定的步骤。 (免责声明:我可能在这里说了一些废话,我现在有点困。)

标签: c++ sdl


【解决方案1】:

考虑以下几点:

#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;
}

在分配 a 和 b 时将 float 隐式转换为 int 期间,小数点后的所有内容都将被截断,而不是四舍五入。这个程序的结果是:

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。

【讨论】:

  • 这与我在您回答前几秒钟得出的结论完全相同。我会将您的标记为已接受。
【解决方案2】:

我宁愿不进行增量计算。这更简单,即使您及时返回也会给出正确的结果,不会丢失精度,并且在现代硬件上会同样快,甚至更快:

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
    }
}

【讨论】:

  • 非常感谢,它实际上看起来比我的解决方案更好。我试试看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-28
  • 1970-01-01
  • 2018-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多