Fifty World Engine
Flame-based interactive grid map rendering for Flutter games. Part of Fifty Flutter Kit.
| Tactical Overview | Unit Selection & Pathfinding |
|---|---|
![]() |
![]() |
Features
- Tile Rendering - Grid-based map with sprites for dungeon crawlers and strategy games
- Camera Controls - Smooth pan and pinch-to-zoom for map exploration
- Entity Management - Spawn, update, remove lifecycle for characters, rooms, monsters
- Movement Animation - Animated transitions for movable entities
- Event Markers - Overlay icons with customizable alignment
- Asset Loading - Registration-based asset loading and caching
- JSON Serialization - Map serialization/deserialization for level design
- Custom Entity Types - Register custom spawners for game-specific entities
- Multi-Platform - Full support for Android, iOS, macOS, Linux, Windows, and Web
Installation
Add to your pubspec.yaml:
dependencies:
fifty_world_engine:
git:
url: https://github.com/fiftynotai/fifty_flutter_kit.git
path: packages/fifty_world_engine
Dependencies:
flame: ^1.30.1logging: ^1.3.0
Quick Start
import 'package:fifty_world_engine/fifty_world_engine.dart';
// 1. Register your assets before the game starts
FiftyAssetLoader.registerAssets([
'rooms/room1.png',
'rooms/room2.png',
'characters/hero.png',
'monsters/goblin.png',
'events/npc.png',
'events/basic.png',
'events/master_of_shadow.png',
]);
// 2. Create your map entities
final entities = [
FiftyWorldEntity(
id: 'room1',
type: 'room',
asset: 'rooms/room1.png',
gridPosition: Vector2(0, 0),
blockSize: FiftyBlockSize(4, 3),
components: [
FiftyWorldEntity(
id: 'hero',
parentId: 'room1',
type: 'character',
asset: 'characters/hero.png',
gridPosition: Vector2(1, 1),
blockSize: FiftyBlockSize(1, 1),
),
],
),
];
// 3. Create controller and widget
final controller = FiftyWorldController();
FiftyWorldWidget(
controller: controller,
initialEntities: entities,
onEntityTap: (entity) {
print('Tapped: ${entity.id}');
},
);
// 4. Use the controller to manipulate the map
controller.addEntities(newEntities);
controller.move(entity, 5, 3);
controller.centerOnEntity(entity);
controller.zoomIn();
Architecture
FiftyWorldWidget (Flutter Widget)
|
+-- FiftyWorldController (UI Facade)
| Entity orchestration, camera control, lookups
|
+-- FiftyWorldBuilder (FlameGame)
|
+-- World (Entity Container)
| Holds all spawned components
|
+-- CameraComponent (View Control)
| Pan, zoom, center operations
|
+-- Entity Registry (Map<String, Component>)
Quick ID-based lookups
Core Components
| Component | Description |
|---|---|
FiftyWorldController |
UI-friendly facade for map manipulation |
FiftyWorldBuilder |
FlameGame implementation with pan/zoom gestures |
FiftyWorldWidget |
Flutter widget embedding the map game |
FiftyWorldEntity |
Data model for map entities |
FiftyEntitySpawner |
Factory for creating entity components |
API Reference
FiftyWorldController
UI-friendly facade for map manipulation.
final controller = FiftyWorldController();
/// Binding lifecycle
controller.bind(game); // Bind to a FiftyWorldBuilder
controller.unbind(); // Unbind and cleanup
controller.isBound; // Check if bound
/// Entity Management
controller.addEntities(entities); // Add or update entities
controller.removeEntity(entity); // Remove an entity
controller.clear(); // Remove all entities
/// Movement (grid coordinates)
controller.move(entity, x, y); // Move to position
controller.moveUp(entity, steps); // Move up by steps
controller.moveDown(entity, steps); // Move down by steps
controller.moveLeft(entity, steps); // Move left by steps
controller.moveRight(entity, steps); // Move right by steps
/// Lookups
controller.currentEntities; // Initial entities list
controller.getComponentById(id); // Get component by ID
controller.getEntityById(id); // Get entity model by ID
/// Camera Control
controller.centerMap(); // Center on all entities
controller.centerOnEntity(entity); // Center on specific entity
controller.zoomIn(); // Zoom in (1.2x factor)
controller.zoomOut(); // Zoom out (1.2x factor)
Design Notes:
- Safe-by-default: if no game is bound (
isBound == false), public methods are no-ops - Movement helpers only work on
FiftyMovableComponentinstances - All operations are synchronous proxies; spawning/animations occur on Flame tick
FiftyWorldBuilder
FlameGame implementation with pan/zoom gestures.
final game = FiftyWorldBuilder(
initialEntities: entities,
onEntityTap: (entity) => print('Tapped: ${entity.id}'),
);
/// Lifecycle
game.initializeGame(); // Setup world, camera, spawn initial entities
game.destroy(); // Pause engine and clear entities
/// Entity Management
game.addEntities(entities); // Add or update batch of entities
game.removeEntity(entity); // Remove single entity
game.clear(); // Clear all entities
game.spawnEntity(entity); // Spawn single entity
/// Lookups
game.getComponentById(id); // Get component by ID
game.initialEntities; // Original entity list
/// Camera Control
game.centerMap(duration: Duration(seconds: 1));
game.centerOnEntity(entity, duration: Duration(seconds: 1));
game.zoomIn(factor: 1.2);
game.zoomOut(factor: 1.2);
game.resetZoom();
Gesture Model:
- One finger drag: Pans the camera
- Two finger pinch: Zooms anchored at pinch midpoint
- Zoom range: 0.3x to 3.0x
FiftyWorldWidget
Flutter widget embedding the map game.
FiftyWorldWidget(
initialEntities: entities, // Optional preloaded entities
onEntityTap: (entity) => {...}, // Required tap callback
controller: controller, // Required controller
);
The widget:
- Automatically binds the controller to a new FiftyWorldBuilder
- Initializes with provided entities
- Forwards tap events via callback
FiftyWorldEntity
Data model for map entities.
final entity = FiftyWorldEntity(
id: 'room1', // Unique identifier (required)
parentId: null, // Parent entity ID (optional)
type: 'room', // Entity type string (required)
asset: 'rooms/room1.png', // Sprite asset path (required)
gridPosition: Vector2(0, 0), // Tile coordinates (required)
blockSize: FiftyBlockSize(4, 3), // Size in tiles (required)
zIndex: 0, // Render priority (default: 0)
quarterTurns: 0, // Rotation 0-3 (default: 0)
text: 'Room A', // Optional text overlay
event: FiftyWorldEvent(...), // Optional event marker
components: [...], // Child entities (default: [])
metadata: {...}, // Custom data (optional)
);
/// Computed properties
entity.position; // Pixel position (Vector2)
entity.size; // Pixel size (Vector2)
/// Serialization
final json = entity.toJson();
final restored = FiftyWorldEntity.fromJson(json);
FiftyWorldEvent
Event marker attached to entities.
final event = FiftyWorldEvent(
text: 'Quest', // Display text
type: FiftyEventType.npc, // Event type
alignment: FiftyEventAlignment.topLeft, // Position relative to parent
clicked: false, // Acknowledged state
);
/// Serialization
final json = event.toJson();
final restored = FiftyWorldEvent.fromJson(json);
FiftyBlockSize
Tile-based size wrapper for map entities.
final size = FiftyBlockSize(4, 3); // 4 tiles wide, 3 tiles tall
/// Properties
size.width; // Horizontal tiles
size.height; // Vertical tiles
/// Serialization
final json = size.toJson();
final restored = FiftyBlockSize.fromJson(json);
Entity Types
| Type | Class | Description |
|---|---|---|
room |
FiftyRoomComponent | Container with children |
character |
FiftyMovableComponent | Movable player/NPC |
monster |
FiftyMovableComponent | Movable enemy |
furniture |
FiftyStaticComponent | Static prop |
door |
FiftyStaticComponent | Static door |
event |
FiftyEventComponent | Event marker |
Event Types
| Type | Description |
|---|---|
FiftyEventType.basic |
Generic event |
FiftyEventType.npc |
NPC interaction |
FiftyEventType.masterOfShadow |
Boss/story event |
Event Alignments
FiftyEventAlignment.topLeft FiftyEventAlignment.topCenter FiftyEventAlignment.topRight
FiftyEventAlignment.centerLeft FiftyEventAlignment.center FiftyEventAlignment.centerRight
FiftyEventAlignment.bottomLeft FiftyEventAlignment.bottomCenter FiftyEventAlignment.bottomRight
Component Classes
FiftyBaseComponent
Abstract base for all entity components.
abstract class FiftyBaseComponent extends SpriteComponent
with HasGameReference<FiftyWorldBuilder>, TapCallbacks {
FiftyWorldEntity model; // Entity data model
FiftyEventComponent? eventComponent; // Optional event overlay
FiftyTextComponent? textComponent; // Optional text overlay
void spawnChild(FiftyWorldEntity child); // Hook for nested entities
}
Features:
- Loads sprite from model.asset
- Calculates pixel position with Y-axis flip
- Applies rotation via quarterTurns
- Attaches RectangleHitbox for collisions
- Spawns event/text overlays if present
- Forwards taps to game handler
FiftyStaticComponent
Component for static, non-moving entities (furniture, doors).
final component = FiftyStaticComponent(model: entity);
Inherits all base behaviors with no additional logic.
FiftyMovableComponent
Component for movable entities (characters, monsters).
final component = FiftyMovableComponent(model: entity);
/// Movement
component.moveTo(newPosition, newModel, speed: 200);
component.moveUp(steps, speed: 200);
component.moveDown(steps, speed: 200);
component.moveLeft(steps, speed: 200);
component.moveRight(steps, speed: 200);
/// Effects
component.attack(onComplete: () => {...}); // Bounce animation
component.die(onComplete: () => {...}); // Fade out and remove
/// Sprite Swap
await component.swapSprite('characters/hero_battle.png');
Movement Notes:
- All movements animate smoothly using MoveToEffect
- Speed is in pixels per second (default: 200)
- Event overlays automatically follow parent movement
FiftyRoomComponent
Container component that spawns child entities.
final room = FiftyRoomComponent(model: roomEntity);
// Child entities in model.components are auto-spawned
FiftyEventComponent
Event marker overlay component.
final event = FiftyEventComponent(model: entity);
event.moveWithParent(newModel); // Follow parent movement
FiftyTextComponent
Text overlay component for entity labels.
// Automatically spawned when entity.text is non-null
Services
FiftyAssetLoader
Asset registration and loading.
/// Register assets (call before game starts)
FiftyAssetLoader.registerAssets([
'rooms/room1.png',
'characters/hero.png',
'events/npc.png',
]);
/// Check registered assets
FiftyAssetLoader.registeredAssets;
/// Reset registry (for testing/hot reload)
FiftyAssetLoader.reset();
Notes:
- Safe to call registerAssets multiple times
- Duplicates are automatically ignored
- Throws exception if loadAll() called with empty registry
FiftyWorldLoader
Map JSON loading and serialization.
/// Load from asset bundle
final entities = await FiftyWorldLoader.loadFromAssets('assets/maps/level1.json');
/// Load from JSON string
final entities = FiftyWorldLoader.loadFromJsonString(jsonString);
/// Serialize to JSON
final json = FiftyWorldLoader.toJsonString(entities);
FiftyEntitySpawner
Factory for spawning map entity components.
/// Spawn a component
final component = FiftyEntitySpawner.spawn(entityModel);
game.world.add(component);
/// Register custom entity type
FiftyEntitySpawner.register(
'trap',
(model) => TrapComponent(model: model),
);
Extensions
FiftyWorldEntityExtension
/// Clone with overrides
final copy = entity.copyWith(id: 'new-id', zIndex: 5);
/// Change position
final moved = entity.changePosition(gridPosition: Vector2(5, 3));
/// Offset by parent position
final absolute = entity.copyWithParent(parentPosition);
/// Type checks
entity.entityType; // FiftyEntityType enum
entity.isRoom; // true if room
entity.isMonster; // true if monster
entity.isCharacter; // true if character
entity.isFurniture; // true if furniture
entity.isEvent; // true if event
entity.isMovable; // true if character or monster
Map JSON Format
[
{
"id": "room1",
"type": "room",
"asset": "rooms/room1.png",
"grid_position": {"x": 0, "y": 0},
"size": {"width": 4, "height": 3},
"z_index": 0,
"quarter_turns": 0,
"text": "Room A",
"components": [
{
"id": "hero",
"parent_id": "room1",
"type": "character",
"asset": "characters/hero.png",
"grid_position": {"x": 1, "y": 1},
"size": {"width": 1, "height": 1},
"event": {
"event_text": "Quest",
"event_type": "npc",
"alignment": "topLeft",
"clicked": false
}
}
],
"metadata": {
"custom_key": "custom_value"
}
}
]
Configuration
Grid Configuration
The default block size is 64 pixels. Access via:
FiftyWorldConfig.blockSize // 64.0
Render Priorities
Default render priorities (higher = on top):
FiftyRenderPriority.background // 0
FiftyRenderPriority.furniture // 10
FiftyRenderPriority.door // 20
FiftyRenderPriority.monster // 30
FiftyRenderPriority.character // 40
FiftyRenderPriority.event // 50
FiftyRenderPriority.uiOverlay // 100
Override with zIndex in FiftyWorldEntity.
Coordinate System
- Grid coordinates: Tile-based
(x, y)where(0, 0)is bottom-left - Pixel coordinates:
gridPosition * FiftyWorldConfig.blockSize - Flame rendering: Y-axis is flipped internally for correct display
Usage Patterns
Loading Maps Dynamically
// Load from JSON file
final entities = await FiftyWorldLoader.loadFromAssets('assets/maps/dungeon.json');
// Clear existing and load new
controller.clear();
controller.addEntities(entities);
controller.centerMap();
Character Movement with Collision Checks
void moveCharacter(FiftyWorldEntity character, double x, double y) {
// Get destination cell
final targetPos = Vector2(x, y);
// Check for obstacles (custom logic)
final blocked = checkCollision(targetPos);
if (!blocked) {
controller.move(character, x, y);
}
}
Event Marker Interaction
FiftyWorldWidget(
controller: controller,
initialEntities: entities,
onEntityTap: (entity) {
if (entity.event != null && !entity.event!.clicked) {
// Mark event as acknowledged
entity.event!.clicked = true;
// Show quest dialog
showQuestDialog(entity.event!.text);
}
},
);
Camera Animation Sequences
// Pan through locations
await controller.centerOnEntity(entrance, duration: Duration(seconds: 1));
await Future.delayed(Duration(milliseconds: 500));
await controller.centerOnEntity(treasure, duration: Duration(seconds: 2));
await Future.delayed(Duration(milliseconds: 500));
await controller.centerOnEntity(exit, duration: Duration(seconds: 1));
Custom Entity Types
Register custom entity types with the spawner:
class TrapComponent extends FiftyBaseComponent {
TrapComponent({required super.model});
@override
Future<void> onLoad() async {
await super.onLoad();
// Custom trap logic
}
void trigger() {
// Trap activation logic
}
}
// Register before game starts
FiftyEntitySpawner.register(
'trap',
(model) => TrapComponent(model: model),
);
// Use in entity definitions
final trap = FiftyWorldEntity(
id: 'trap1',
type: 'trap', // Uses registered spawner
asset: 'traps/spike.png',
gridPosition: Vector2(3, 2),
blockSize: FiftyBlockSize(1, 1),
);
Best Practices
- Register assets first - Call
FiftyAssetLoader.registerAssets()before game starts - Use controller methods - Prefer controller over direct game access
- Check isBound - Verify controller is bound before operations
- Clean up properly - Call
controller.unbind()when disposing - Use grid coordinates - Movement methods use tile units, not pixels
- Leverage custom types - Register custom spawners for game-specific entities
See the example directory for a complete tactical skirmish sandbox showcasing tile grid rendering, camera controls, entity spawning with team decorators, A* pathfinding, animation queues, and tap interaction.
Platform Support
| Platform | Support | Notes |
|---|---|---|
| Android | Yes | Full feature support |
| iOS | Yes | Full feature support |
| macOS | Yes | Full feature support |
| Linux | Yes | Full feature support |
| Windows | Yes | Full feature support |
| Web | Yes | Full feature support |
Fifty Design Language Integration
This package is part of Fifty Flutter Kit:
- Consistent naming - All classes use
Fiftyprefix - Compatible packages - Works with
fifty_ui,fifty_theme,fifty_tokens - Kit patterns - Follows Fifty Flutter Kit coding standards
Version
Current: 0.1.0
License
MIT License - see LICENSE for details.
Part of Fifty Flutter Kit.
Libraries
- fifty_world_engine
- Fifty World Engine - Grid game toolkit for Flutter/Flame
- fifty_world_engine_method_channel
- fifty_world_engine_platform_interface
- fifty_world_engine_web

