You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Stopwatch显示<1ms但每秒仅50次执行,移动线程异常原因排查

问题分析与解决方案

嘿,我来帮你拆解下这个问题的核心原因,以及对应的解决办法~

为什么Stopwatch会显示0ms?

你看到的ElapsedMilliseconds显示0ms,主要是两个原因:

  1. 整数精度限制Stopwatch.ElapsedMilliseconds返回的是整数毫秒值,如果你的代码(移动逻辑+碰撞检测+Sleep)实际总耗时小于1ms,就会被取整为0。比如移动和碰撞代码执行极快,而Sleep(1)因为系统调度的原因,实际休眠时间不足1ms(这种情况在系统空闲时可能发生,但很少见)。
  2. 系统定时器精度的误解:Windows系统默认的时钟分辨率是15.625ms(对应64Hz的系统时钟),所以Thread.Sleep(1)并不会真的只休眠1ms——它会让线程休眠到下一个系统时钟周期,实际耗时通常是10-16ms。但你看到0ms,大概率是因为Stopwatch的计时逻辑被线程调度干扰,或者Debug输出的延迟让你看到了旧的计时值。

玩家移动极慢的核心原因

这和Thread.Sleep(1)的实际行为直接相关:

  • 因为系统时钟精度的问题,你的循环每秒只能执行约60次(1000ms/16ms≈60)。如果player.xMovementplayer.yMovement的数值很小(比如每次只+1),那玩家每秒只能移动60像素,视觉上自然会显得很慢。
  • 另外,你的循环逻辑里两次设置player.lastPos,可能会导致碰撞检测的回退逻辑出现问题(比如Y方向的碰撞回退覆盖X方向的位置变化),不过这不是移动慢的直接原因,但值得优化。

优化方案

1. 提高系统定时器精度(临时解决)

通过调用Windows API来临时提高系统时钟分辨率到1ms,让Sleep(1)的实际休眠时间更接近预期:

[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern uint timeBeginPeriod(uint uPeriod);

[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern uint timeEndPeriod(uint uPeriod);

// 在启动线程前调用
timeBeginPeriod(1);
// 程序退出时记得调用,恢复系统默认精度
timeEndPeriod(1);

2. 使用固定时间步长的游戏循环(推荐)

不要依赖Sleep来控制帧率,而是基于实际耗时计算移动距离,这样不管循环执行频率如何,玩家移动速度都能保持稳定:

private void MoveThreadFunc() {
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    long lastTick = stopWatch.ElapsedTicks;
    // 移动速度(像素/秒),可以根据需求调整
    float moveSpeedX = 200f;
    float moveSpeedY = 200f;

    while (true) {
        long currentTick = stopWatch.ElapsedTicks;
        // 计算两次循环的时间差(秒)
        float deltaTime = (currentTick - lastTick) / (float)Stopwatch.Frequency;
        lastTick = currentTick;

        // 根据时间差计算本次循环的移动距离
        int moveX = (int)(moveSpeedX * deltaTime);
        int moveY = (int)(moveSpeedY * deltaTime);

        player.lastPos = player.rec.Location;
        player.rec.X += moveX;
        Collision(player);
        player.lastPos = player.rec.Location;
        player.rec.Y += moveY;
        Collision(player);

        // 限制帧率,避免占用过多CPU资源
        if (stopWatch.ElapsedTicks - currentTick < Stopwatch.Frequency / 60) {
            Thread.Sleep(1);
        }
    }
}

3. 调整移动增量值

如果坚持用原来的固定增量逻辑,可以适当增大player.xMovementplayer.yMovement的数值,比如从1改成5,这样即使每秒60次循环,每秒也能移动300像素,视觉上会流畅很多。

内容的提问来源于stack exchange,提问作者Fleming

火山引擎 最新活动