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
相关产品推荐
相关产品推荐

