diff --git a/src/components/street-generated-clones.js b/src/components/street-generated-clones.js index 79d93e94..b29d5e0e 100644 --- a/src/components/street-generated-clones.js +++ b/src/components/street-generated-clones.js @@ -1,4 +1,5 @@ /* global AFRAME */ +import { createRNG } from '../lib/rng'; AFRAME.registerComponent('street-generated-clones', { multiple: true, @@ -10,6 +11,7 @@ AFRAME.registerComponent('street-generated-clones', { positionX: { default: 0, type: 'number' }, positionY: { default: 0, type: 'number' }, facing: { default: 0, type: 'number' }, // Y Rotation in degrees + seed: { default: 0, type: 'int' }, // random seed for random and randomFacing mode randomFacing: { default: false, type: 'boolean' }, direction: { type: 'string', oneOf: ['none', 'inbound', 'outbound'] }, // not used if facing defined? @@ -40,6 +42,18 @@ AFRAME.registerComponent('street-generated-clones', { }, update: function (oldData) { + // If mode is random or randomFacing and seed is 0, generate a random seed and return, + // the update will be called again because of the setAttribute. + if (this.data.mode === 'random' || this.data.randomFacing) { + if (this.data.seed === 0) { + const newSeed = Math.floor(Math.random() * 1000000) + 1; // Add 1 to avoid seed 0 + this.el.setAttribute(this.attrName, 'seed', newSeed); + return; + } + // Always recreate RNG when update is called to be sure we end of with the same clones positions for a given seed + this.rng = createRNG(this.data.seed); + } + // Clear existing entities this.remove(); @@ -115,7 +129,7 @@ AFRAME.registerComponent('street-generated-clones', { rotationY = 180 - data.facing; } if (data.randomFacing) { - rotationY = Math.random() * 360; + rotationY = this.rng() * 360; } clone.setAttribute('rotation', `0 ${rotationY} 0`); @@ -131,9 +145,7 @@ AFRAME.registerComponent('street-generated-clones', { getModelMixin: function () { const data = this.data; if (data.modelsArray && data.modelsArray.length > 0) { - return data.modelsArray[ - Math.floor(Math.random() * data.modelsArray.length) - ]; + return data.modelsArray[Math.floor(this.rng() * data.modelsArray.length)]; } return data.model; }, @@ -151,8 +163,12 @@ AFRAME.registerComponent('street-generated-clones', { // Apply the offset similar to fixed mode return start + idx * correctedSpacing; }); + // Use seeded random for shuffling + for (let i = positions.length - 1; i > 0; i--) { + const j = Math.floor(this.rng() * (i + 1)); + [positions[i], positions[j]] = [positions[j], positions[i]]; + } - // Randomly select positions - return positions.sort(() => 0.5 - Math.random()).slice(0, count); + return positions.slice(0, count); } }); diff --git a/src/components/street-generated-pedestrians.js b/src/components/street-generated-pedestrians.js index 7fc75a1e..5fef3cb6 100644 --- a/src/components/street-generated-pedestrians.js +++ b/src/components/street-generated-pedestrians.js @@ -1,11 +1,10 @@ /* global AFRAME */ +import { createRNG } from '../lib/rng'; -// a-frame component to generate cloned pedestrian models along a street AFRAME.registerComponent('street-generated-pedestrians', { multiple: true, schema: { segmentWidth: { - // width of the segment in meters type: 'number', default: 3 }, @@ -15,7 +14,6 @@ AFRAME.registerComponent('street-generated-pedestrians', { oneOf: ['empty', 'sparse', 'normal', 'dense'] }, length: { - // length in meters of linear path to fill with clones type: 'number' }, direction: { @@ -23,15 +21,13 @@ AFRAME.registerComponent('street-generated-pedestrians', { default: 'none', oneOf: ['none', 'inbound', 'outbound'] }, - // animated: { - // // load 8 animated characters instead of 16 static characters - // type: 'boolean', - // default: false - // }, positionY: { - // y position of pedestrians type: 'number', default: 0 + }, + seed: { + type: 'int', + default: 0 } }, @@ -47,12 +43,22 @@ AFRAME.registerComponent('street-generated-pedestrians', { remove: function () { this.createdEntities.forEach((entity) => entity.remove()); - this.createdEntities.length = 0; // Clear the array + this.createdEntities.length = 0; }, update: function (oldData) { const data = this.data; + // Handle seed initialization + if (this.data.seed === 0) { + const newSeed = Math.floor(Math.random() * 1000000) + 1; + this.el.setAttribute(this.attrName, 'seed', newSeed); + return; + } + + // Create seeded RNG + this.rng = createRNG(this.data.seed); + // Clean up old entities this.remove(); @@ -67,7 +73,7 @@ AFRAME.registerComponent('street-generated-pedestrians', { this.densityFactors[data.density] * data.length ); - // Get available z positions + // Get Z positions using seeded randomization const zPositions = this.getZPositions( -data.length / 2, data.length / 2, @@ -79,25 +85,23 @@ AFRAME.registerComponent('street-generated-pedestrians', { const pedestrian = document.createElement('a-entity'); this.el.appendChild(pedestrian); - // Set random position within bounds + // Set seeded random position within bounds const position = { x: this.getRandomArbitrary(xRange.min, xRange.max), y: data.positionY, - z: zPositions.pop() + z: zPositions[i] }; pedestrian.setAttribute('position', position); - // Set model variant - const variantNumber = this.getRandomIntInclusive( - 1, - data.animated ? 8 : 16 - ); - const variantPrefix = data.animated ? 'a_char' : 'char'; - pedestrian.setAttribute('mixin', `${variantPrefix}${variantNumber}`); + // Set model variant using seeded random + const variantNumber = this.getRandomIntInclusive(1, 16); + pedestrian.setAttribute('mixin', `char${variantNumber}`); - // Set rotation based on direction - if (data.direction === 'none' && Math.random() < 0.5) { - pedestrian.setAttribute('rotation', '0 180 0'); + // Set rotation based on direction and seeded random + if (data.direction === 'none') { + if (this.rng() < 0.5) { + pedestrian.setAttribute('rotation', '0 180 0'); + } } else if (data.direction === 'outbound') { pedestrian.setAttribute('rotation', '0 180 0'); } @@ -111,22 +115,29 @@ AFRAME.registerComponent('street-generated-pedestrians', { } }, - // Helper methods from legacy function + // Helper methods now using seeded RNG getRandomIntInclusive: function (min, max) { min = Math.ceil(min); max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1) + min); + return Math.floor(this.rng() * (max - min + 1) + min); }, getRandomArbitrary: function (min, max) { - return Math.random() * (max - min) + min; + return this.rng() * (max - min) + min; }, getZPositions: function (start, end, step) { const len = Math.floor((end - start) / step) + 1; - const arr = Array(len) + const positions = Array(len) .fill() .map((_, idx) => start + idx * step); - return arr.sort(() => 0.5 - Math.random()); + + // Use seeded shuffle (Fisher-Yates algorithm with seeded RNG) + for (let i = positions.length - 1; i > 0; i--) { + const j = Math.floor(this.rng() * (i + 1)); + [positions[i], positions[j]] = [positions[j], positions[i]]; + } + + return positions; } }); diff --git a/src/lib/rng.js b/src/lib/rng.js new file mode 100644 index 00000000..8d36dda9 --- /dev/null +++ b/src/lib/rng.js @@ -0,0 +1,12 @@ +export function createRNG(seed) { + // Create a RNG with the specified seed + // Mulberry32 PRNG implementation + return (function (a) { + return function () { + var t = (a += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + })(seed) +} \ No newline at end of file