Hello,
I have a problem where my macOS (Sequoia) Spritekit game spikes delta during the first press of an arrow key. I've test this with a small program, code below:
//
// GameScene.swift
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var lastTime: TimeInterval = 0
override func keyDown(with event: NSEvent) {
print("---------------> delta keyDown: \(event.characters!) keyCode: \(event.keyCode)")
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
print("update begins")
let dt: CGFloat
if lastTime > 0 {
dt = (CGFloat(currentTime - lastTime))
} else {
dt = 1.0 / 60.0
}
if dt > (1/30) {
print("************************************ delta spike ", dt)
}
lastTime = currentTime
print("dt: ", dt)
print("update ends")
}
}
Example output:
update begins
************************************ delta spike 0.03381687504588626
dt: 0.03381687504588626
update ends
update begins
dt: 0.016670208307914436
update ends
As you can see, when I press left arrow key in this case I get a big delta spike. There's no spike with further presses of the arrow key.
Other keys, such as the common W A S D controls for games, do not cause this delta spike.
The delta spike on the first arrow key press is a macOS behavior. Arrow keys take a different path through the responder chain than regular character keys — they route through interpretKeyEvents(_:), which likely triggers lazy initialization of the text input system on first use. That one-time setup cost is what causes the spike. Regular keys like WASD don't take this path, which is why they're unaffected.
Your addLocalMonitorForEvents workaround bypasses the responder chain, which avoids the initialization cost. However, returning nil from the monitor consumes all key-down events, which will break keyboard shortcuts (Cmd+Q, Cmd+W, etc.) and prevent events from reaching other responders.
The standard solution in game development is to clamp your delta time to a maximum value:
let dt: CGFloat
if lastTime > 0 {
dt = min(CGFloat(currentTime - lastTime), 1.0 / 30.0)
} else {
dt = 1.0 / 60.0
}
lastTime = currentTime
This protects against all sources of frame time spikes — not just arrow keys, but also system interruptions, thermal throttling, window resizing, and background transitions. Any game that uses delta time to scale movement or physics should have a clamp like this.