Categories
Final Major Project

Understanding Latency Between Arduino and Unreal Engine: Delay vs. Millis, Data Pacing, and Smoothing

One of the first major challenges I encountered in this project was unexpected latency and jitter in the animation feedback inside Unreal Engine. At the beginning, this was frustrating because the visual behaviour looked like a hiccup—small recursive jumps in the animation, happening regularly. After investigating the issue end-to-end, I now understand much more clearly how timing, data pacing, and sensor noise interact between the physical world (Arduino) and the virtual world (Unreal Engine).

This section describes what caused the latency, why delay() and millis() behave differently, and how I ultimately fixed the issue.


1. Two Different Worlds With Two Different Clocks

The core issue comes from the fact that Arduino and Unreal Engine operate on completely separate update loops:

  • Arduino sends sensor data at whatever pace your sketch defines.
  • Unreal Engine reads the serial port at the pace defined in Blueprints (e.g., using a timer running every X milliseconds).

If these two paces do not match, Unreal will sometimes read:

  • half-written lines,
  • empty strings, or
  • strings that cut off mid-value,

which produces 0, truncated values, or invalid numeric conversions.
This is exactly what caused the visible jitter in animation.


2. Why delay() Causes Problems

In my early implementation, I used Arduino’s built-in delay() function to control the output frequency.

What delay() actually does

delay(100) literally freezes the entire microcontroller for 100 milliseconds.
During that freeze:

  • no sensor updates occur
  • no serial writes occur
  • capacitive updates pause
  • the device cannot respond to anything

This creates blocking behaviour and interrupts the natural flow of data.

When Unreal tries to read the serial port during one of these blocked periods, it often receives an incomplete line or nothing at all — which appears as a value of 0 after parsing.

This is why I was seeing irregular spikes and jitter in animation.


3. Why millis() Works Better

millis() allows us to implement non-blocking timing.
Instead of freezing the Arduino, we check whether the required time interval has passed:

unsigned long now = millis();
if (now - lastSendTime >= sendInterval) {
    lastSendTime = now;
    // send sensor data
}

The advantages:

  • Arduino keeps reading sensors continuously
  • capacitive updates continue uninterrupted
  • serial communication never freezes
  • Unreal receives consistent packets
  • no backlog or piling up of delayed operations

This is crucial when sending multiple sensor values in one line.


4. Why Unreal Recursion Made the Problem Worse

Originally, I used a recursive function in Unreal to constantly read the serial port.
Inside that recursive call, Unreal Blueprint also contained its own Delay node.

This created a double-latency situation:

  • Arduino was delayed
  • Unreal was delayed
  • Their delays drifted out of sync over time

This explains the “recursive hiccup” behaviour — Unreal repeatedly attempted to read data during moments when Arduino was not sending anything because it was frozen by delay().

The result:
empty strings → parsed as zero → visual jitter in animation.

Fix

I replaced the recursion with Unreal’s:

Set Timer by Function Name
✔ consistent “read every X ms” behaviour
✔ no recursion
✔ no stack buildup
✔ consistent pacing

This matched Arduino’s non-blocking millis() timing and removed the jitter.


5. Why Smoothing the Data Is Essential

Another problem was that analogue sensors (stretch sensors, LDRs, proximity deltas) are naturally noisy.
Unfiltered raw data is unstable:

  • small spikes
  • sudden drops
  • inconsistent readings

Using raw input for animation blending is especially problematic — blend spaces expect gradual transitions, not random jumps.

Solution: Smoothing

On Arduino, we can smooth each sensor individually before sending:

  • Moving average
  • Exponential smoothing
  • Median filtering
  • Sample averaging

This reduces noise drastically and produces stable, predictable curves for animation.


6. Guaranteeing Correct Data Parsing in Unreal

Because I send 5 sensor values in every line, Unreal must ensure it reads a complete line, not a partial one.

My message format:

touch, toggle, proximity, stretch, LDR

To prevent Unreal from parsing incomplete packets, I check:

✔ ArrayLength == 5

Only if the incoming line splits into exactly 5 values do I allow animation logic to run.
If not, that frame is skipped.

This eliminates crashes and unexpected behaviour caused by truncated serial messages.



Summary

Why the jitter appeared:

  • Arduino froze during delay() → incomplete serial messages
  • Unreal read during these frozen periods → empty strings
  • Unreal recursion amplified timing drift
  • raw analogue data introduced noise

What fixed it:

  • replacing delay() with millis() (non-blocking timing)
  • using Unreal’s timer instead of recursive calls
  • smoothing sensor data before sending
  • verifying ArrayLength == 5 before parsing

Result:

Smooth, stable, responsive interaction between physical sensors and Unreal animation.

Leave a Reply

Your email address will not be published. Required fields are marked *