You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Roblox中毫秒级精确脚本暂停方案求助(ECG绘图场景)

问题:Roblox中ECG波形绘制脚本性能与精确等待问题

我在Roblox平台编写了一段通过SurfaceGui上的Frame(像素)绘制ECG波形的脚本,实际运行速度远低于预期——比如需要80ms绘制的波形,实际耗时160ms甚至更久。

排查发现:

  • wait()和task.wait()在短等待时长(如1/60秒)时精度严重不足
  • 尝试用os.clock()实现的AccurateWait函数理论上可以精确计时,但实际运行时会导致电脑卡顿,频繁触发Script exhausted execution time错误,函数代码如下:
local function AccurateWait(time)
    local start = os.clock()
    while os.clock() - start < time do 
        -- Nothing
    end
end

目前需要实现精确的1帧(60FPS下约0.017秒)暂停功能,完整脚本如下:

local leadFrame = script.Parent.blackBg.I_leadFrame
local leadFrameWidth = leadFrame.Size.X.Offset
local leadFrameUpdateTick = 1

local pixelSize = 2 -- def: 
local pixels = {}

local step = 3 -- DO NOT CHANGE THIS VALUE, def: 4

local colGreen = Color3.fromRGB(25, 255, 25)

local heartBPM = 60 / 60 * 1000 -- Will need it later, for now let it remain as 60

local linePosX = 0
local linePosY = 0
local linePosY_Scale = 0.5
local lineAccuracy = 16 -- def: 16
local lineReachedFrameWidth = false
local lineLoops = true
local lineClearanceOffset = 20
local lineClearing = false

local section = 0
local sectionInMS = 0
local sectionMaxWidth = 0

local wholeBeatLength = 0

local function DrawLine()
    if linePosX >= leadFrameWidth - pixelSize then
        if lineLoops then
            linePosX = 0
        end
        lineReachedFrameWidth = true
        return
    end
    
    if linePosX >= leadFrameWidth - pixelSize - lineClearanceOffset or lineClearing then
        lineClearing = true
        pixels[1]:Destroy()
        table.remove(pixels, 1)
    end
    
    if linePosY ~= linePosY then
        linePosY = 0
    end
    
    local pixel = Instance.new("Frame", leadFrame)
    pixel.Size = UDim2.new(0, pixelSize, 0, pixelSize)
    pixel.Position = UDim2.new(0, linePosX, linePosY_Scale, linePosY)

    pixel.BackgroundColor3 = colGreen
    pixel.BackgroundTransparency = 0
    pixel.BorderSizePixel = 0
    pixel.Name = "pixel"
    
    table.insert(pixels, pixel)
end

local function DrawP_Wave()
    local durationP_Wave = 80 -- In ms, assume it is normal duration at 60 bpm
    local durationPR_Segment = durationP_Wave + 40
    
    local startTime = os.clock()
    while sectionInMS < durationP_Wave do
        -- At these parameters length of P wave is 90 ms
        local A = 15 * step / 4 -- Scale of P wave, def: 15
        local B = 2.4 -- Width of P wave, def: 2.2 (the higher - the shorter)
        local C = 1.5 -- Can't describe, better not touch it, def: 1.5
        local D = 0.4 -- Height of P wave, def:
        local E = 1 -- Polarity of P wave, def: 1
        
        for i = 1, lineAccuracy do
            linePosY = -E * (A * D * math.sin(B * section / A))^C
            
            DrawLine()
            
            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
            
            if sectionInMS > durationP_Wave then
                break
            end
        end
        wait(1/60)
    end
    print("P wave: "..((os.clock()-startTime)*1000).." ms")
    
    while sectionInMS < durationPR_Segment do
        for i = 1, lineAccuracy do
            DrawLine()

            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
            
            if sectionInMS > durationPR_Segment then
                break
            end
        end
        wait(1/60)
        wholeBeatLength += 1/60*1000
    end
    print("PQ segment: "..((os.clock()-startTime)*1000).." ms")
    
    section = 0
    sectionInMS = 0
end

local function DrawQRS_Complex()
    local durationQRS_Complex = 90
    local durationST_Segment = 100 + durationQRS_Complex
    
    local startTime = os.clock()
    while sectionInMS < durationQRS_Complex do
        local A = 1.7 -- Width of QRS, def: 1.5 (the higher A - the shorter QRS)
        local B = 1.7 -- Height of QRS, def: 1.7
        local C = 3.6
        local D = 3.5
        local E = 5 -- def: 5
        local F = 1.1 -- Proportions of Q to S (bigger num -> deeper peak Q), def: 1.1
        local G = 15 * step / 4 -- Scale, def: 15
        
        for i = 1, lineAccuracy do
            linePosY = -G*((B*(math.sin(A/G * section))^E)^D-
                       (math.sin(A/G*F * section))^C)
            
            DrawLine()
            
            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
            
            if sectionInMS > durationQRS_Complex then
                break
            end
        end
        wait(1/60)
        wholeBeatLength += 1/60*1000
    end
    print("QRS complex: "..((os.clock()-startTime)*1000).." ms")
    
    while sectionInMS < durationST_Segment do
        for i = 1, lineAccuracy do
            DrawLine()

            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
            
            if sectionInMS > durationST_Segment then
                break
            end
        end
        wait(1/60)
        wholeBeatLength += 1/60*1000
    end
    print("ST segment: "..((os.clock()-startTime)*1000).." ms")
    
    section = 0
    sectionInMS = 0
end

local function DrawT_Wave()
    local durationT_Wave = 160
    
    local startTime = os.clock()
    while sectionInMS < durationT_Wave do
        local A = 1.1
        local B = 1.6
        local C = 3.6
        local D = 0.9
        local E = 5
        local F = 1.1
        local G = 15 * step / 4
        local H = 2.1
        
        for i = 1, lineAccuracy do
            linePosY = -G*((B*(math.sin(A/G * section))^E)^D-
                ((math.sin(A/G*F * section))^H)^C)
            
            DrawLine()
            
            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
            
            if sectionInMS > durationT_Wave then
                break
            end
        end
        wait(1/60)
        wholeBeatLength += 1/60*1000
    end
    print("T wave: "..((os.clock()-startTime)*1000).." ms")
    
    section = 0
    sectionInMS = 0
end

local function BreakBetweenBeats()
    local startTime = os.clock()
    while wholeBeatLength < heartBPM do
        for i = 1, lineAccuracy do
            DrawLine()

            linePosX += step/lineAccuracy
            section += step/lineAccuracy
            sectionInMS = ((section/(60*step))*1000)
        end
        wait(1/60)
        wholeBeatLength += 1/60*1000
    end
    print("Pause: "..((os.clock()-startTime)*1000).." ms")
    
    section = 0
    sectionInMS = 0
    wholeBeatLength = 0
end

while true do
    DrawP_Wave()
    DrawQRS_Complex()
    DrawT_Wave()
    BreakBetweenBeats()
end
print("ECG session has ended.")

解决方案

1. 修复精确等待逻辑,避免自旋卡顿

自旋等待(空while循环)会占用线程全部资源,触发执行超时。改用task.wait()结合时间补偿实现兼顾精度和性能的等待:

local function AccurateWait(targetTime)
    local startTime = os.clock()
    local elapsed = 0
    while elapsed < targetTime do
        local remaining = targetTime - elapsed
        -- 让出线程避免空转,等待剩余时间的一半
        if remaining > 0 then
            task.wait(remaining * 0.5)
        end
        elapsed = os.clock() - startTime
    end
end

2. 用对象池优化UI元素创建开销

原脚本每次绘制都创建新Frame,这是核心性能瓶颈。复用已创建的Frame,减少Instance操作:

-- 初始化像素对象池
local pixelPool = {}
local function getPixel()
    local pixel = table.remove(pixelPool)
    if not pixel then
        pixel = Instance.new("Frame")
        pixel.Size = UDim2.new(0, pixelSize, 0, pixelSize)
        pixel.BackgroundColor3 = colGreen
        pixel.BackgroundTransparency = 0
        pixel.BorderSizePixel = 0
        pixel.Name = "pixel"
    end
    pixel.Parent = leadFrame
    return pixel
end

local function returnPixel(pixel)
    pixel.Parent = nil
    table.insert(pixelPool, pixel)
end

-- 修改DrawLine函数
local function DrawLine()
    if linePosX >= leadFrameWidth - pixelSize then
        if lineLoops then
            linePosX = 0
        end
        lineReachedFrameWidth = true
        return
    end
    
    if linePosX >= leadFrameWidth - pixelSize - lineClearanceOffset or lineClearing then
        lineClearing = true
        local oldPixel = table.remove(pixels, 1)
        returnPixel(oldPixel)
    end
    
    if linePosY ~= linePosY then
        linePosY = 0
    end
    
    local pixel = getPixel()
    pixel.Position = UDim2.new(0, linePosX, linePosY_Scale, linePosY)
    
    table.insert(pixels, pixel)
end

3. 按帧批量绘制,减少等待次数

原脚本每帧仅绘制少量像素,导致等待次数过多。计算每帧应推进的进度,批量处理绘制逻辑:

-- 全局预计算时间转换系数,避免重复计算
local sectionToMS = 1000 / (60 * step)

local function DrawP_Wave()
    local durationP_Wave = 80 -- ms
    local durationPR_Segment = durationP_Wave + 40
    local startTime = os.clock()
    local totalDuration = durationPR_Segment
    
    -- 计算每帧应推进的section量
    local sectionPerFrame = (60 * step) / 60

    while sectionInMS < totalDuration do
        local frameStartTime = os.clock()
        for i = 1, lineAccuracy do
            if sectionInMS < durationP_Wave then
                local A = 15 * step / 4
                local B = 2.4
                local C = 1.5
                local D = 0.4
                local E = 1
                linePosY = -E * (A * D * math.sin(B * section / A))^C
            else
                linePosY = 0 -- PR段保持水平
            end
            
            DrawLine()
            
            local stepIncrement = sectionPerFrame / lineAccuracy
            linePosX += stepIncrement
            section += stepIncrement
            sectionInMS = section * sectionToMS
            
            if sectionInMS >= totalDuration then
                break
            end
        end
        
        -- 精确等待到下一帧
        local frameElapsed = os.clock() - frameStartTime
        local waitTime = math.max(0, 1/60 - frameElapsed)
        if waitTime > 0 then
            AccurateWait(waitTime)
        end
        wholeBeatLength += (frameElapsed + waitTime) * 1000
    end
    print("P wave + PR segment: "..((os.clock()-startTime)*1000).." ms")
    
    section = 0
    sectionInMS = 0
end

4. 其他优化点

  • 简化时间计算逻辑,预计算sectionToMS系数减少循环内重复运算
  • 统一各波形绘制函数的帧处理逻辑,减少冗余代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 19:54:55