Skip to main content

Cookbook: Sprite Sheets & Grids

For animations and tilemaps, sprite sheets are more efficient than individual images. This tutorial explains how to slice a texture into a uniform grid and render specific frames using the engine's declarative widget pattern.

Live Demo

Click "Play" below to see the result. The demo slices a single texture and cycles through frames using a timer in the game loop.

Assets Used

This tutorial uses assets from the ansimuz Explosion Pack.

PreviewAssetAction
explosion.pngDownload

Tutorial

0. Asset Setup

Before writing any code, you must register your assets with Flutter.

  1. Create a directory named assets/sprites/ in your project root.
  2. Place the explosion.png file into that directory.
  3. Add the directory to your pubspec.yaml file:
flutter:
assets:
- assets/sprites/

1. Asset & Scaffolding

Set up the textures and root widget. We pre-load the explosion texture using a FutureBuilder to ensure it's available for slicing during initialization.

import 'package:flutter/material.dart';
import 'package:goo2d/goo2d.dart';

void main() => runApp(const SpriteExample());

enum GameTextures with AssetEnum, TextureAssetEnum {
explosion;

AssetSource get source => AssetSource.local("assets/sprites/$name.png");
}

class SpriteExample extends StatelessWidget {
const SpriteExample({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: GameAsset.loadAll(GameTextures.values).drain(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return const Game(child: MyGameWidget());
},
),
);
}
}

2. Declaring the Sheet Reference

In your GameState, declare a late final variable for the SpriteSheet. This variable will hold our sliced sub-textures once they are initialized.

class MyGameWidget extends StatefulGameWidget {
const MyGameWidget({super.key});

GameState<MyGameWidget> createState() => MyGameState();
}

class MyGameState extends GameState<MyGameWidget> {
// We initialize this sheet once the game state is created
late final SpriteSheet explosionSheet;
}

3. Slicing the Texture Grid

Override initState to define the grid dimensions. We slice the explosion texture into 13 horizontal frames. The ppu (Pixels Per Unit) determines the size of the sprite relative to the world coordinate system.

class MyGameState extends GameState<MyGameWidget> {
late final SpriteSheet explosionSheet;


void initState() {
super.initState();

// Create a 13x1 grid from the explosion texture
explosionSheet = SpriteSheet.grid(
texture: GameTextures.explosion,
columns: 13,
rows: 1,
ppu: 64.0, // Each frame is 64x64 pixels
);
}
}

4. Creating the Animation Logic

To make the example verifiable, add an onUpdate loop that increments the current frame index over time. This creates a running animation that we can see in the demo.

class MyGameState extends GameState<MyGameWidget> {
late final SpriteSheet explosionSheet;
int currentFrame = 0;
double timer = 0;


void onUpdate(double dt) {
timer += dt;
if (timer > 0.1) { // Change frame every 100ms
timer = 0;
setState(() {
currentFrame = (currentFrame + 1) % 13;
});
}
}

// ... rest of the class ...
}

5. Rendering the Sprite

Implement the build method to render the entity. We access a specific frame from our sheet using the [(column, row)] operator, passing in our dynamic currentFrame index.

class MyGameState extends GameState<MyGameWidget> {
// ... variables and initState ...


Iterable<Widget> build(BuildContext context) sync* {
yield GameWidget(
components: () => [
ObjectTransform(),
SpriteRenderer()..sprite = explosionSheet[(currentFrame, 0)],
],
);

// Add a camera to see the world
yield GameWidget(
components: () => [
ObjectTransform(),
Camera()..orthographicSize = 5.0,
],
);
}
}

Full Implementation

import 'package:flutter/material.dart';
import 'package:goo2d/goo2d.dart';

void main() => runApp(const SpriteExample());

enum GameTextures with AssetEnum, TextureAssetEnum {
explosion;

AssetSource get source => AssetSource.local("assets/sprites/$name.png");
}

class SpriteExample extends StatelessWidget {
const SpriteExample({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: GameAsset.loadAll(GameTextures.values).drain(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return const Game(child: MyGameWidget());
},
),
);
}
}

class MyGameWidget extends StatefulGameWidget {
const MyGameWidget({super.key});

GameState<MyGameWidget> createState() => MyGameState();
}

class MyGameState extends GameState<MyGameWidget> {
late final SpriteSheet explosionSheet;
int currentFrame = 0;
double timer = 0;


void initState() {
super.initState();
explosionSheet = SpriteSheet.grid(
texture: GameTextures.explosion,
columns: 13,
rows: 1,
ppu: 64.0,
);
}


void onUpdate(double dt) {
timer += dt;
if (timer > 0.1) {
timer = 0;
setState(() {
currentFrame = (currentFrame + 1) % 13;
});
}
}


Iterable<Widget> build(BuildContext context) sync* {
yield GameWidget(
components: () => [
ObjectTransform(),
SpriteRenderer()..sprite = explosionSheet[(currentFrame, 0)],
],
);

yield GameWidget(
components: () => [
ObjectTransform(),
Camera()..orthographicSize = 5.0,
],
);
}
}