flame_worldgen
This package provides a lightweight, modular system for managing
procedurally generated tilemaps in Flutter using the Flame
engine.
It is built around the concept of chunks, noise-based terrain
generation, and sprite selectors for flexible rendering.
β¨ Features
- π¦ Chunk-based world streaming
- Dynamically load/unload chunks around the camera or player.
- Adjustable view distance and chunk cache for performance tuning.
- π Noise-driven terrain generation
- Powered by fast_noise.
- Heightmaps per chunk for flexible tile selection.
- π¨ Sprite selection system
StaticSpriteSelector--- pick a tile based only on noise value.AnimatedSpriteSelector--- cycle through frames per tile.WeightedSpriteSelector--- probabilistic sprite selection with noise-based weights.
- πΌ Batch rendering with Flame's
SpriteBatch- Efficient tile rendering using batched draw calls.
- π¬ Tile animations
TileAnimationControllerupdates frame indices at a fixed duration.- Integrates seamlessly with selectors.
- π Utility functions
- Convert between chunk, tile, and world coordinates.
π Usage
1. Create a ChunkManager
final chunkManager = ChunkManager(
noise: PerlinFractalNoise( // Choose your noise generator
seed: seed,
frequency: 0.0005,
octaves: 5,
lacunarity: 2.0,
),
chunkSize: Vector2i(16, 16),
tileSize: Vector2i(16, 16),
viewDistance: 4,
);
2. Configure your TileLayers
final waterLayer = TileLayer(
chunkManager: chunkManager,
spriteBatch: SpriteBatch(images.fromCache('water.png')),
config: TileLayerConfig(
animationController: TileAnimationController(frameDuration: 0.3),
spriteSelector: AnimatedSpriteSelector((noise) {
// render animated water all over the map
return [
Rect.fromLTWH(0, 0, 16, 16), // 1. frame
Rect.fromLTWH(16, 0, 16, 16), // 2. frame
Rect.fromLTWH(32, 0, 16, 16), // 3. frame
Rect.fromLTWH(48, 0, 16, 16), // 4. frame
];
}),
),
priority: -0x80000000,
);
final groundLayer = TileLayer(
chunkManager: chunkManager,
spriteBatch: SpriteBatch(images.fromCache('grass.png')),
config: TileLayerConfig(
animationController: TileAnimationController(frameDuration: 0.3), // animation controller is needed to animate `WeightedSprite.multi` sprite
spriteSelector: WeightedSpriteSelector(
options: [
WeightedSprite.single(
Rect.fromLTWH(16, 16, 16, 16),
weight: (noise) => 0.7, // 70% chance to get this tile
),
WeightedSprite.single(
Rect.fromLTWH(0, 80, 16, 16),
weight: (noise) => 0.15, // 15% chance to get this tile
),
WeightedSprite.multi([ // Animated tile
Rect.fromLTWH(32, 96, 16, 16),
Rect.fromLTWH(48, 96, 16, 16),
Rect.fromLTWH(64, 96, 16, 16),
Rect.fromLTWH(80, 96, 16, 16),
], weight: (noise) => 0.15), // 15% chance to get this tile
],
predicate: (noise) => noise > -0.08, // only render grass when if the noise value is bigger than -0.08
),
),
priority: -0x7FFFFFFF, // render the ground layer with the priority 1 higher than the water layer
);
// add the layers to the world
world.add(groundLayer);
world.add(waterLayer);
π§© Sprite Selection Strategies
StaticSpriteSelector
Selects a sprite based on noise only.
StaticSpriteSelector((noise) => noise > 0.5 ? grassTile : waterTile);
AnimatedSpriteSelector
Cycles through frames per tile.
AnimatedSpriteSelector((noise) =>
noise > 0.5 ? grassFrames : waterFrames
);
WeightedSpriteSelector
Weighted random sprite choice.
WeightedSpriteSelector(
predicate: (noise) => noise > 0.5,
options: [
WeightedSprite.single(grassTile, weight: (_) => 0.7),
WeightedSprite.single(flowerTile, weight: (_) => 0.3),
],
);
π Coordinate Conversion Helpers
chunkToWorldPosition(chunkCoords, chunkWorldSize)\worldToChunkPosition(worldPos, chunkWorldSize)\tileToWorldPosition(tileCoords, tileSize)\worldToTilePosition(worldPos, tileSize)
These make it easy to move between world, chunk, and tile coordinates.
π‘ Chunk Streaming
- The
ChunkManagerloads/unloads chunks around the camera or player. - Emits
ChunkUpdateInfoevents on changes (loaded/unloaded chunks). - Integrates with
TileLayerto rebuild batches efficiently.
β‘ Performance Notes
- SpriteBatch drastically reduces draw calls.
- Chunk caching avoids regenerating terrain unnecessarily.
Contributing
Contributions and suggestions are welcome! Feel free to open issues or submit pull requests.