397 lines
9.1 KiB
Markdown
397 lines
9.1 KiB
Markdown
|
|
# Procedural Generation (Procgen)
|
||
|
|
|
||
|
|
`@esengine/procgen` provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm install @esengine/procgen
|
||
|
|
```
|
||
|
|
|
||
|
|
## Quick Start
|
||
|
|
|
||
|
|
### Noise Generation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||
|
|
|
||
|
|
// Create Perlin noise
|
||
|
|
const perlin = createPerlinNoise(12345); // seed
|
||
|
|
|
||
|
|
// Sample 2D noise
|
||
|
|
const value = perlin.noise2D(x * 0.1, y * 0.1);
|
||
|
|
console.log(value); // [-1, 1]
|
||
|
|
|
||
|
|
// Use FBM for more natural results
|
||
|
|
const fbm = createFBM(perlin, {
|
||
|
|
octaves: 6,
|
||
|
|
persistence: 0.5
|
||
|
|
});
|
||
|
|
|
||
|
|
const height = fbm.noise2D(x * 0.01, y * 0.01);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Seeded Random
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createSeededRandom } from '@esengine/procgen';
|
||
|
|
|
||
|
|
// Create deterministic random generator
|
||
|
|
const rng = createSeededRandom(42);
|
||
|
|
|
||
|
|
// Same seed always produces same sequence
|
||
|
|
console.log(rng.next()); // 0.xxx
|
||
|
|
console.log(rng.nextInt(1, 100)); // 1-100
|
||
|
|
console.log(rng.nextBool(0.3)); // 30% true
|
||
|
|
```
|
||
|
|
|
||
|
|
### Weighted Random
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const rng = createSeededRandom(42);
|
||
|
|
|
||
|
|
const loot = createWeightedRandom([
|
||
|
|
{ value: 'common', weight: 60 },
|
||
|
|
{ value: 'uncommon', weight: 25 },
|
||
|
|
{ value: 'rare', weight: 10 },
|
||
|
|
{ value: 'legendary', weight: 5 }
|
||
|
|
]);
|
||
|
|
|
||
|
|
const drop = loot.pick(rng);
|
||
|
|
console.log(drop); // Likely 'common'
|
||
|
|
```
|
||
|
|
|
||
|
|
## Noise Functions
|
||
|
|
|
||
|
|
### Perlin Noise
|
||
|
|
|
||
|
|
Classic gradient noise, output range [-1, 1]:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createPerlinNoise } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const perlin = createPerlinNoise(seed);
|
||
|
|
const value2D = perlin.noise2D(x, y);
|
||
|
|
const value3D = perlin.noise3D(x, y, z);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Simplex Noise
|
||
|
|
|
||
|
|
Faster than Perlin, less directional bias:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createSimplexNoise } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const simplex = createSimplexNoise(seed);
|
||
|
|
const value = simplex.noise2D(x, y);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Worley Noise
|
||
|
|
|
||
|
|
Cell-based noise for stone, cell textures:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createWorleyNoise } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const worley = createWorleyNoise(seed);
|
||
|
|
const distance = worley.noise2D(x, y);
|
||
|
|
```
|
||
|
|
|
||
|
|
### FBM (Fractal Brownian Motion)
|
||
|
|
|
||
|
|
Layer multiple noise octaves for richer detail:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const baseNoise = createPerlinNoise(seed);
|
||
|
|
|
||
|
|
const fbm = createFBM(baseNoise, {
|
||
|
|
octaves: 6, // Layer count (more = richer detail)
|
||
|
|
lacunarity: 2.0, // Frequency multiplier
|
||
|
|
persistence: 0.5, // Amplitude decay
|
||
|
|
frequency: 1.0, // Initial frequency
|
||
|
|
amplitude: 1.0 // Initial amplitude
|
||
|
|
});
|
||
|
|
|
||
|
|
// Standard FBM
|
||
|
|
const value = fbm.noise2D(x, y);
|
||
|
|
|
||
|
|
// Ridged FBM (for mountains)
|
||
|
|
const ridged = fbm.ridged2D(x, y);
|
||
|
|
|
||
|
|
// Turbulence
|
||
|
|
const turb = fbm.turbulence2D(x, y);
|
||
|
|
|
||
|
|
// Billowed (for clouds)
|
||
|
|
const cloud = fbm.billowed2D(x, y);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Seeded Random API
|
||
|
|
|
||
|
|
### SeededRandom
|
||
|
|
|
||
|
|
Deterministic PRNG based on xorshift128+:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createSeededRandom } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const rng = createSeededRandom(42);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Basic Methods
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
rng.next(); // [0, 1) float
|
||
|
|
rng.nextInt(1, 10); // [min, max] integer
|
||
|
|
rng.nextFloat(0, 100); // [min, max) float
|
||
|
|
rng.nextBool(); // 50%
|
||
|
|
rng.nextBool(0.3); // 30%
|
||
|
|
rng.reset(); // Reset to initial state
|
||
|
|
```
|
||
|
|
|
||
|
|
### Distribution Methods
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Normal distribution (Gaussian)
|
||
|
|
rng.nextGaussian(); // mean 0, stdDev 1
|
||
|
|
rng.nextGaussian(100, 15); // mean 100, stdDev 15
|
||
|
|
|
||
|
|
// Exponential distribution
|
||
|
|
rng.nextExponential(); // λ = 1
|
||
|
|
rng.nextExponential(0.5); // λ = 0.5
|
||
|
|
```
|
||
|
|
|
||
|
|
### Geometry Methods
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Uniform point in circle
|
||
|
|
const point = rng.nextPointInCircle(50); // { x, y }
|
||
|
|
|
||
|
|
// Point on circle edge
|
||
|
|
const edge = rng.nextPointOnCircle(50); // { x, y }
|
||
|
|
|
||
|
|
// Uniform point in sphere
|
||
|
|
const point3D = rng.nextPointInSphere(50); // { x, y, z }
|
||
|
|
|
||
|
|
// Random direction vector
|
||
|
|
const dir = rng.nextDirection2D(); // { x, y }, length 1
|
||
|
|
```
|
||
|
|
|
||
|
|
## Weighted Random API
|
||
|
|
|
||
|
|
### WeightedRandom
|
||
|
|
|
||
|
|
Precomputed cumulative weights for efficient selection:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createWeightedRandom } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const selector = createWeightedRandom([
|
||
|
|
{ value: 'apple', weight: 5 },
|
||
|
|
{ value: 'banana', weight: 3 },
|
||
|
|
{ value: 'cherry', weight: 2 }
|
||
|
|
]);
|
||
|
|
|
||
|
|
const result = selector.pick(rng);
|
||
|
|
const result2 = selector.pickRandom(); // Uses Math.random
|
||
|
|
|
||
|
|
console.log(selector.getProbability(0)); // 0.5 (5/10)
|
||
|
|
console.log(selector.size); // 3
|
||
|
|
console.log(selector.totalWeight); // 10
|
||
|
|
```
|
||
|
|
|
||
|
|
### Convenience Functions
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const item = weightedPick([
|
||
|
|
{ value: 'a', weight: 1 },
|
||
|
|
{ value: 'b', weight: 2 }
|
||
|
|
], rng);
|
||
|
|
|
||
|
|
const item2 = weightedPickFromMap({
|
||
|
|
'common': 60,
|
||
|
|
'rare': 30,
|
||
|
|
'epic': 10
|
||
|
|
}, rng);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Shuffle and Sampling
|
||
|
|
|
||
|
|
### shuffle / shuffleCopy
|
||
|
|
|
||
|
|
Fisher-Yates shuffle:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { shuffle, shuffleCopy } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const arr = [1, 2, 3, 4, 5];
|
||
|
|
shuffle(arr, rng); // In-place
|
||
|
|
const shuffled = shuffleCopy(arr, rng); // Copy
|
||
|
|
```
|
||
|
|
|
||
|
|
### pickOne
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { pickOne } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const item = pickOne(['a', 'b', 'c', 'd'], rng);
|
||
|
|
```
|
||
|
|
|
||
|
|
### sample / sampleWithReplacement
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { sample, sampleWithReplacement } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||
|
|
const unique = sample(arr, 3, rng); // 3 unique
|
||
|
|
const withRep = sampleWithReplacement(arr, 5, rng); // 5 with replacement
|
||
|
|
```
|
||
|
|
|
||
|
|
### randomIntegers
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { randomIntegers } from '@esengine/procgen';
|
||
|
|
|
||
|
|
// 5 unique random integers from 1-100
|
||
|
|
const nums = randomIntegers(1, 100, 5, rng);
|
||
|
|
```
|
||
|
|
|
||
|
|
### weightedSample
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { weightedSample } from '@esengine/procgen';
|
||
|
|
|
||
|
|
const items = ['A', 'B', 'C', 'D', 'E'];
|
||
|
|
const weights = [10, 8, 6, 4, 2];
|
||
|
|
const selected = weightedSample(items, weights, 3, rng);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Practical Examples
|
||
|
|
|
||
|
|
### Procedural Terrain
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||
|
|
|
||
|
|
class TerrainGenerator {
|
||
|
|
private fbm: FBM;
|
||
|
|
private moistureFbm: FBM;
|
||
|
|
|
||
|
|
constructor(seed: number) {
|
||
|
|
const heightNoise = createPerlinNoise(seed);
|
||
|
|
const moistureNoise = createPerlinNoise(seed + 1000);
|
||
|
|
|
||
|
|
this.fbm = createFBM(heightNoise, {
|
||
|
|
octaves: 8,
|
||
|
|
persistence: 0.5,
|
||
|
|
frequency: 0.01
|
||
|
|
});
|
||
|
|
|
||
|
|
this.moistureFbm = createFBM(moistureNoise, {
|
||
|
|
octaves: 4,
|
||
|
|
persistence: 0.6,
|
||
|
|
frequency: 0.02
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
getHeight(x: number, y: number): number {
|
||
|
|
let height = this.fbm.noise2D(x, y);
|
||
|
|
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
|
||
|
|
return (height + 1) * 0.5; // Normalize to [0, 1]
|
||
|
|
}
|
||
|
|
|
||
|
|
getBiome(x: number, y: number): string {
|
||
|
|
const height = this.getHeight(x, y);
|
||
|
|
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
|
||
|
|
|
||
|
|
if (height < 0.3) return 'water';
|
||
|
|
if (height < 0.4) return 'beach';
|
||
|
|
if (height > 0.8) return 'mountain';
|
||
|
|
|
||
|
|
if (moisture < 0.3) return 'desert';
|
||
|
|
if (moisture > 0.7) return 'forest';
|
||
|
|
return 'grassland';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Loot System
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createSeededRandom, createWeightedRandom } from '@esengine/procgen';
|
||
|
|
|
||
|
|
class LootSystem {
|
||
|
|
private rng: SeededRandom;
|
||
|
|
private raritySelector: WeightedRandom<string>;
|
||
|
|
|
||
|
|
constructor(seed: number) {
|
||
|
|
this.rng = createSeededRandom(seed);
|
||
|
|
this.raritySelector = createWeightedRandom([
|
||
|
|
{ value: 'common', weight: 60 },
|
||
|
|
{ value: 'uncommon', weight: 25 },
|
||
|
|
{ value: 'rare', weight: 10 },
|
||
|
|
{ value: 'legendary', weight: 5 }
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
generateLoot(count: number): LootItem[] {
|
||
|
|
const loot: LootItem[] = [];
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const rarity = this.raritySelector.pick(this.rng);
|
||
|
|
// Get item from rarity table...
|
||
|
|
loot.push(item);
|
||
|
|
}
|
||
|
|
return loot;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Blueprint Nodes
|
||
|
|
|
||
|
|
### Noise Nodes
|
||
|
|
- `SampleNoise2D` - Sample 2D noise
|
||
|
|
- `SampleFBM` - Sample FBM noise
|
||
|
|
|
||
|
|
### Random Nodes
|
||
|
|
- `SeededRandom` - Generate random float
|
||
|
|
- `SeededRandomInt` - Generate random integer
|
||
|
|
- `WeightedPick` - Weighted random selection
|
||
|
|
- `ShuffleArray` - Shuffle array
|
||
|
|
- `PickRandom` - Pick random element
|
||
|
|
- `SampleArray` - Sample from array
|
||
|
|
- `RandomPointInCircle` - Random point in circle
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
1. **Use seeds for reproducibility**
|
||
|
|
```typescript
|
||
|
|
const seed = Date.now();
|
||
|
|
const rng = createSeededRandom(seed);
|
||
|
|
saveSeed(seed);
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Precompute weighted selectors**
|
||
|
|
```typescript
|
||
|
|
// Good: Create once, use many times
|
||
|
|
const selector = createWeightedRandom(items);
|
||
|
|
for (let i = 0; i < 1000; i++) {
|
||
|
|
selector.pick(rng);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Choose appropriate noise**
|
||
|
|
- Perlin: Smooth terrain, clouds
|
||
|
|
- Simplex: Performance-critical
|
||
|
|
- Worley: Cell textures, stone
|
||
|
|
- FBM: Natural multi-detail effects
|
||
|
|
|
||
|
|
4. **Tune FBM parameters**
|
||
|
|
- `octaves`: More = richer detail, higher cost
|
||
|
|
- `persistence`: 0.5 is common, higher = more high-frequency detail
|
||
|
|
- `lacunarity`: Usually 2, controls frequency growth
|