A Shmup engine built on top of Flame
❗ In order to start using Scrolite you must have the Flutter SDK installed on your machine.
Install via flutter pub add:
dart pub add scroliteScrolite uses two main file formats to define game content:
Stage files define the level layout, scrolling sections, and object placement. They use an INI-like syntax with sections marked by brackets.
[data]
speed = 6
[objects]
(2, 32) = minion_1
(2, 40) = minion_1
(54, 16) = minion_2
[section]
id = #1
type = sprite
spritePath = level1/s1.png
width = 64
height = 128
[section]
id = #2
type = sprite
spritePath = level1/s2.png
width = 64
height = 128[data] - Global stage configuration
speed(double): The scroll speed of the stage
[objects] - Object spawn definitions
- Format:
(x, y) = object_name x: Horizontal position (integer)y: Vertical position/scroll distance (integer) - objects spawn when the stage scrolls past this Y coordinateobject_name: Reference to a game object defined in the objects file
[section] - Background sections (multiple allowed)
id(string): Unique identifier for the sectiontype(string): Section type (currently only "sprite" is supported)spritePath(string): Path to the background image assetwidth(double): Section width in pixelsheight(double): Section height in pixels
Object files define reusable game objects that can be spawned in stages.
[minion_1]
tag = enemy
type = sprite
imageFile = sprites.png
srcSrc = 0, 16
srcSize = 8, 8
controller = minion_1
hitbox = 2, 1, 4, 4
[player_bullet]
tag = bullet
type = sprite
imageFile = sprites.png
srcSrc = 16, 0
srcSize = 4, 4
controller = bullet_controllerEach object is defined in its own [object_name] section:
type(string): Object type (currently only "sprite" is supported) - requiredimageFile(string): Path to the sprite sheet image - requiredsrcSrc(string):x, ycoordinates of the sprite in the sprite sheet - requiredsrcSize(string):width, heightof the sprite in pixels - requiredcontroller(string): Name of the controller class that will handle this object's behavior - requiredtag(string): Optional tag for collision filtering (e.g., "enemy", "bullet")hitbox(string):x, y, width, heightdefining the collision hitbox (optional)
import 'package:flame/flame.dart';
import 'package:scrolite/scrolite.dart';
// Load and parse a stage file
final stageData = await Flame.assets.readFile('level_data/level_1.scrolite');
final stage = Stage.fromData(stageData);
// Access stage properties
print(stage.scrollSpeed); // 6.0
print(stage.sections.length); // Number of sections
print(stage.objects.length); // Number of objects to spawnStage.fromData(String data) - Factory constructor that parses stage data
Properties:
sections:List<Section>- Background sections that scroll verticallyscrollSpeed:double- The base scroll speedobjects:List<(int x, int y, String name)>- Objects to spawn with their positions
Section (abstract base class)
id:String- Section identifierwidth:double- Section widthheight:double- Section height
SpriteSection (concrete implementation)
spritePath:String- Path to the background imagebuild(FlameGame game): Builds aSpriteComponentfrom the section definition
// Load and parse object definitions
final objectsData = await Flame.assets.readFile('objects.scrolite');
final objectsMapping = GameObject.readObjectMap(objectsData);
// Access parsed objects
final minion = objectsMapping['minion_1'];
print(minion.controller); // "minion_1"
print(minion.hitbox); // (2, 1, 4, 4)GameObject.readObjectMap(String raw) - Static method that parses object definitions
Returns: Map<String, GameObject> - Map of object names to their definitions
GameObject (abstract base class)
controller:String- Controller name for behaviortag:String?- Optional collision taghitbox:(int, int, int, int)?- Optional hitbox as(x, y, width, height)
SpriteGameObject (concrete implementation)
spritePath:String- Path to sprite sheetsrcPosition:Vector2- Sprite position in sheet (fromsrcSrc)srcSize:Vector2- Sprite size (fromsrcSize)
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:scrolite/scrolite.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Parse stage and objects
final stageData = await Flame.assets.readFile('level_data/level_1.scrolite');
final stage = Stage.fromData(stageData);
final objectsData = await Flame.assets.readFile('objects.scrolite');
final objectsMapping = GameObject.readObjectMap(objectsData);
runApp(
GameWidget(
game: MyGame(
stage: stage,
objectsMapping: objectsMapping,
controllersMapping: {
'minion_1': MinionController.new,
'bullet': BulletController.new,
},
resolution: Vector2(64, 128),
),
),
);
}
class MyGame extends ScroliteGame {
MyGame({
required super.stage,
required super.objectsMapping,
required super.controllersMapping,
required super.resolution,
}) : super(scrollSpeed: 6);
}Constructor:
stage:Stage- Parsed stage dataobjectsMapping:Map<String, GameObject>- Parsed object definitionscontrollersMapping:Map<String, Component Function()>- Factory functions for controllersresolution:Vector2- Game resolutionscrollSpeed:double- Initial scroll speed (optional, defaults to 60)
The engine automatically:
- Scrolls background sections vertically
- Spawns objects at their defined Y positions based on scroll distance
- Attaches controllers to spawned objects
- Handles collision detection with hitboxes
Controllers are Flame components that define object behavior:
class MinionController extends Component
with ParentIsA<PositionComponent>, HasGameReference<MyGame> {
@override
void update(double dt) {
super.update(dt);
// Move down with the scroll plus extra speed
parent.y += 10 * dt;
// Remove when off-screen
if (parent.y > game.resolution.y) {
removeFromParent();
}
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
// Handle collisions
}
}Controllers receive collision callbacks automatically via GameObjectComponent.
Scrolite comes with a built-in GitHub Actions workflow powered by Very Good Workflows but you can also add your preferred CI/CD solution.
Out of the box, on each pull request and push, the CI formats, lints, and tests the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses Very Good Analysis for a strict set of analysis options used by our team. Code coverage is enforced using the Very Good Workflows.
For first time users, install the very_good_cli:
dart pub global activate very_good_cliTo run all unit tests:
very_good test --coverageTo view the generated coverage report you can use lcov.
# Generate Coverage Report
genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
open coverage/index.html