Hello,
I'm making a multiplayer game that needs to send updates from server to client.
I'm using the suggested Observer/SoA serialization (with diff) pattern from the docs.
I have a PlayerSystem with a TrailPoints component like so :
const TrailPoints = {
xs: array(f32),
ys: array(f32),
};
the idea being that a single player entity has a list of [x,y] trail points
I've noticed TrailPoints were never actually considered as diffs when using epsilon.
I'd expect the lib to look into the arrays content and check for diffs there based on the epsilon value? maybe my expectations are wrong, idk.
Below is a reported generated with AI. I can confirm the base issue and workaround do work. I'm leaving the root cause and suggested fix in case it's useful somehow.
maybe a trailpoint should be an entity itself with a position component and it would be a child of the parent player entity? seems heavy but maybe the intended way?
using 0.4
AI Report
Summary
SoA diff serializer's hasChanged uses epsilon-based float comparison (Math.abs(NaN) > ε → false) on nested array(f32) components because isFloatType only looks at the outer array's element type marker, ignoring that the actual stored values are Arrays (not floats). Result: changes to nested array components are silently dropped from all diffs after the first serialization.
How to reproduce
import { array, f32, createSoASerializer } from 'bitecs/serialization';
// Nested array component: each entity stores its own f32 trail array
const TrailPoints = {
xs: array(f32), // outer SoA array, element type marker = $f32
ys: array(f32),
};
// Add entity 0 with a 2-point trail
addEntity(world);
TrailPoints.xs[0] = [100, 200];
TrailPoints.ys[0] = [300, 400];
// Create diff serializer
const serialize = createSoASerializer([TrailPoints], { diff: true, epsilon: 0.0001 });
// First diff — works because shadow starts at 0, NaN !== Array
serialize([0]); // includes trail data ✓
// Add a new point (new reference via spread)
TrailPoints.xs[0] = [...TrailPoints.xs[0], 500];
TrailPoints.ys[0] = [...TrailPoints.ys[0], 600];
// Second diff — never includes trail data ✗
serialize([0]); // empty / 0 bytes
Expected behavior
The second call to the serializer should include the updated xs/ys arrays. Adding new elements changes the array reference, so the shadow comparison should detect the change.
Actual behavior
The second call returns 0 bytes (no data). TrailPoints changes are never detected after the first serialization.
Root cause
In hasChanged (and getEpsilonForType / isFloatType):
var isFloatType = (array2) => {
const arrayType = getTypeForArray(array2); // checks outer array's $arr marker
return arrayType === $f32 || arrayType === $f64;
};
var hasChanged = (shadowMap, array2, index, epsilon) => {
const shadow = getShadow(shadowMap, array2);
const currentValue = array2[index]; // ← this is an Array, not a float!
const actualEpsilon = getEpsilonForType(array2, epsilon);
const changed = actualEpsilon > 0
? Math.abs(shadow[index] - currentValue) > actualEpsilon
: shadow[index] !== currentValue;
// ...
};
For nested array(f32) components:
TrailPoints.xs is a branded array with $arr = f32
getTypeForArray(TrailPoints.xs) returns $f32 — the outer array's element type
isFloatType returns true
getEpsilonForType returns ε = 0.0001
actualEpsilon > 0 is true, so the float comparison path is taken:
Math.abs(shadow[index] - currentValue) > epsilon
- Both
shadow[index] and currentValue are Array objects (the per-entity trail array)
Array - Array → NaN
Math.abs(NaN) > epsilon → false — always false regardless of reference change
Workaround
Set epsilon: 0 when creating the SoA diff serializer. This forces the shadow[index] !== currentValue (reference equality) branch, which works correctly for nested arrays. The trade-off is that scalar float components lose epsilon tolerance for diff detection, but this is typically harmless for game ECS usage where entities are explicitly marked dirty.
Suggested fix
isFloatType (or getEpsilonForType) should not rely solely on the outer array's element type marker. For SoA array-of-array components, the elements at each index are themselves arrays, not scalar floats. One approach:
- Check if
array2[index] is array-like before using epsilon comparison, or
- Add a depth concept to
isFloatType that distinguishes "array of floats" from "array of float-arrays"
Hello,
I'm making a multiplayer game that needs to send updates from server to client.
I'm using the suggested Observer/SoA serialization (with diff) pattern from the docs.
I have a PlayerSystem with a TrailPoints component like so :
the idea being that a single player entity has a list of [x,y] trail points
I've noticed TrailPoints were never actually considered as diffs when using epsilon.
I'd expect the lib to look into the arrays content and check for diffs there based on the epsilon value? maybe my expectations are wrong, idk.
Below is a reported generated with AI. I can confirm the base issue and workaround do work. I'm leaving the root cause and suggested fix in case it's useful somehow.
maybe a trailpoint should be an entity itself with a position component and it would be a child of the parent player entity? seems heavy but maybe the intended way?
using 0.4
AI Report
Summary
SoA diff serializer's hasChanged uses epsilon-based float comparison (Math.abs(NaN) > ε → false) on nested array(f32) components because isFloatType only looks at the outer array's element type marker, ignoring that the actual stored values are Arrays (not floats). Result: changes to nested array components are silently dropped from all diffs after the first serialization.
How to reproduce
Expected behavior
The second call to the serializer should include the updated
xs/ysarrays. Adding new elements changes the array reference, so the shadow comparison should detect the change.Actual behavior
The second call returns 0 bytes (no data). TrailPoints changes are never detected after the first serialization.
Root cause
In
hasChanged(andgetEpsilonForType/isFloatType):For nested
array(f32)components:TrailPoints.xsis a branded array with$arr = f32getTypeForArray(TrailPoints.xs)returns$f32— the outer array's element typeisFloatTypereturnstruegetEpsilonForTypereturns ε = 0.0001actualEpsilon > 0istrue, so the float comparison path is taken:shadow[index]andcurrentValueare Array objects (the per-entity trail array)Array - Array→NaNMath.abs(NaN) > epsilon→false— always false regardless of reference changeWorkaround
Set
epsilon: 0when creating the SoA diff serializer. This forces theshadow[index] !== currentValue(reference equality) branch, which works correctly for nested arrays. The trade-off is that scalar float components lose epsilon tolerance for diff detection, but this is typically harmless for game ECS usage where entities are explicitly marked dirty.Suggested fix
isFloatType(orgetEpsilonForType) should not rely solely on the outer array's element type marker. For SoA array-of-array components, the elements at each index are themselves arrays, not scalar floats. One approach:array2[index]is array-like before using epsilon comparison, orisFloatTypethat distinguishes "array of floats" from "array of float-arrays"