Learn Flutter Game Development — For MonoGame/XNA Developers

Flame is the usual 2D path in Flutter. If you already know MonoGame or XNA, the game loop, sprites, input, and content pipeline are the same jobs with different names. This guide maps those APIs side by side so you can port a mental model rather than start from a Flutter-first tutorial.

Game Engine Overview

Flutter uses the Flame engine for 2D game development, which provides similar functionality to MonoGame/XNA. Both are component-based and handle rendering, input, and audio.

MonoGame/XNA

// MonoGame Game class
public class MyGame : Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    
    public MyGame()
    {
        graphics = new 
            GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }
    
    protected override void Initialize()
    {
        base.Initialize();
    }
}

Flutter/Flame

// Flame game class
import 'package:flame/game.dart';

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    // Initialize game here
    // Load assets, setup world
    await super.onLoad();
  }
  
  @override
  void update(double dt) {
    super.update(dt);
    // Update game logic
  }
}

// Run the game
void main() {
  runApp(GameWidget(game: MyGame()));
}

Key differences

Game Loop & Update Cycle

Both engines use a fixed update loop with delta time for frame-independent movement.

MonoGame/XNA

// Game loop methods
protected override void Update(
    GameTime gameTime
)
{
    float deltaTime = (float)gameTime
        .ElapsedGameTime.TotalSeconds;
    
    // Update player position
    playerPosition.X += 
        velocity.X * deltaTime;
    playerPosition.Y += 
        velocity.Y * deltaTime;
    
    base.Update(gameTime);
}

protected override void Draw(
    GameTime gameTime
)
{
    GraphicsDevice.Clear(Color.Black);
    
    spriteBatch.Begin();
    spriteBatch.Draw(
        playerTexture,
        playerPosition,
        Color.White
    );
    spriteBatch.End();
    
    base.Draw(gameTime);
}

Flutter/Flame

// Game loop in Flame
class MyGame extends FlameGame {
  late Player player;
  
  @override
  Future<void> onLoad() async {
    player = Player();
    add(player);
  }
  
  @override
  void update(double dt) {
    super.update(dt);
    // Additional game logic
    // Components update themselves
  }
}

// Component with update and render
class Player extends SpriteComponent {
  Vector2 velocity = Vector2(100, 0);
  
  @override
  void update(double dt) {
    super.update(dt);
    position.x += velocity.x * dt;
    position.y += velocity.y * dt;
  }
  
  // render() is handled by SpriteComponent
}

Key differences

Sprites & Textures

Both frameworks load and render 2D sprites. Flame uses components while MonoGame uses textures directly.

MonoGame/XNA

// Loading and drawing sprites
Texture2D playerTexture;
Vector2 position;
Rectangle sourceRect;

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(
        GraphicsDevice
    );
    playerTexture = Content.Load<Texture2D>(
        "player"
    );
    position = new Vector2(100, 100);
}

protected override void Draw(
    GameTime gameTime
)
{
    spriteBatch.Begin();
    
    // Simple draw
    spriteBatch.Draw(
        playerTexture,
        position,
        Color.White
    );
    
    // With source rectangle (sprite sheet)
    spriteBatch.Draw(
        playerTexture,
        position,
        sourceRect,
        Color.White,
        rotation,
        origin,
        scale,
        SpriteEffects.None,
        0f
    );
    
    spriteBatch.End();
}

Flutter/Flame

// Loading and rendering sprites
class Player extends SpriteComponent 
    with HasGameRef<MyGame> {
  
  @override
  Future<void> onLoad() async {
    sprite = await gameRef.loadSprite(
      'player.png'
    );
    size = Vector2(64, 64);
    position = Vector2(100, 100);
    anchor = Anchor.center;
  }
}

// Sprite sheet animation
class AnimatedPlayer extends 
    SpriteAnimationComponent {
  
  @override
  Future<void> onLoad() async {
    animation = await gameRef.loadSpriteAnimation(
      'player_sheet.png',
      SpriteAnimationData.sequenced(
        amount: 4,
        stepTime: 0.2,
        textureSize: Vector2(64, 64),
      ),
    );
    size = Vector2(64, 64);
    position = Vector2(100, 100);
  }
}

Key differences

Input Handling

Both frameworks provide keyboard, mouse, and touch input. Flame uses mixins for input handling.

MonoGame/XNA

// Input handling in MonoGame
KeyboardState keyboardState;
MouseState mouseState;

protected override void Update(
    GameTime gameTime
)
{
    keyboardState = Keyboard.GetState();
    mouseState = Mouse.GetState();
    
    // Keyboard input
    if (keyboardState.IsKeyDown(Keys.Right))
    {
        playerPosition.X += speed * 
            deltaTime;
    }
    
    if (keyboardState.IsKeyDown(Keys.Space))
    {
        Jump();
    }
    
    // Mouse input
    if (mouseState.LeftButton == 
        ButtonState.Pressed)
    {
        Shoot(new Vector2(
            mouseState.X,
            mouseState.Y
        ));
    }
    
    base.Update(gameTime);
}

Flutter/Flame

// Input handling in Flame
class MyGame extends FlameGame 
    with KeyboardEvents, TapDetector {
  
  // Keyboard input
  @override
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    if (keysPressed.contains(
      LogicalKeyboardKey.arrowRight
    )) {
      player.position.x += speed * dt;
    }
    
    if (event is RawKeyDownEvent &&
        event.logicalKey == 
        LogicalKeyboardKey.space) {
      player.jump();
    }
    
    return KeyEventResult.handled;
  }
  
  // Touch/Mouse input
  @override
  void onTapDown(TapDownInfo info) {
    shoot(info.eventPosition.game);
  }
}

// Per-component input
class Button extends SpriteComponent 
    with TapCallbacks {
  
  @override
  void onTapDown(TapDownEvent event) {
    // Handle button press
  }
}

Key differences

Collision Detection

Both frameworks provide collision detection systems. Flame has built-in collision detection with mixins.

MonoGame/XNA

// Collision detection in MonoGame
Rectangle playerBounds;
Rectangle enemyBounds;

protected override void Update(
    GameTime gameTime
)
{
    playerBounds = new Rectangle(
        (int)playerPosition.X,
        (int)playerPosition.Y,
        playerTexture.Width,
        playerTexture.Height
    );
    
    enemyBounds = new Rectangle(
        (int)enemyPosition.X,
        (int)enemyPosition.Y,
        enemyTexture.Width,
        enemyTexture.Height
    );
    
    // Check collision
    if (playerBounds.Intersects(
        enemyBounds))
    {
        OnCollision();
    }
    
    // Circle collision
    float distance = Vector2.Distance(
        playerPosition,
        enemyPosition
    );
    if (distance < playerRadius + 
        enemyRadius)
    {
        OnCollision();
    }
}

Flutter/Flame

// Collision detection in Flame
class MyGame extends FlameGame 
    with HasCollisionDetection {
  
  @override
  Future<void> onLoad() async {
    add(Player());
    add(Enemy());
  }
}

class Player extends SpriteComponent 
    with CollisionCallbacks {
  
  @override
  Future<void> onLoad() async {
    await super.onLoad();
    
    // Rectangle hitbox
    add(RectangleHitbox());
    
    // Or circle hitbox
    // add(CircleHitbox());
  }
  
  @override
  void onCollision(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollision(intersectionPoints, other);
    
    if (other is Enemy) {
      // Handle collision with enemy
      onHitEnemy();
    }
  }
}

class Enemy extends SpriteComponent 
    with CollisionCallbacks {
  
  @override
  Future<void> onLoad() async {
    await super.onLoad();
    add(RectangleHitbox());
  }
}

Key differences

Camera & Viewport

Both frameworks provide camera systems for scrolling and zooming. Flame has a built-in camera with follow behavior.

MonoGame/XNA

// Camera in MonoGame
Matrix cameraTransform;
Vector2 cameraPosition;
float zoom = 1.0f;

protected override void Update(
    GameTime gameTime
)
{
    // Follow player
    cameraPosition = playerPosition;
    
    // Create transform matrix
    cameraTransform = Matrix.CreateTranslation(
        -cameraPosition.X,
        -cameraPosition.Y,
        0
    ) * Matrix.CreateScale(zoom);
}

protected override void Draw(
    GameTime gameTime
)
{
    spriteBatch.Begin(
        transformMatrix: cameraTransform
    );
    
    // Draw game objects
    spriteBatch.Draw(
        playerTexture,
        playerPosition,
        Color.White
    );
    
    spriteBatch.End();
}

Flutter/Flame

// Camera in Flame
class MyGame extends FlameGame {
  late Player player;
  
  @override
  Future<void> onLoad() async {
    // Setup camera
    camera.followComponent(
      player,
      worldBounds: Rect.fromLTRB(
        0, 0, 2000, 2000
      ),
    );
    
    // Set zoom
    camera.zoom = 2.0;
    
    // Camera shake effect
    camera.shake(
      intensity: 10,
      duration: 0.5,
    );
  }
}

// Using CameraComponent (advanced)
class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    final world = World();
    final camera = CameraComponent(world: world);
    
    camera.viewfinder.anchor = Anchor.center;
    camera.viewport = FixedResolutionViewport(
      Vector2(800, 600)
    );
    
    addAll([camera, world]);
  }
}

Key differences

Particle Systems

Both frameworks support particle effects for explosions, fire, smoke, etc.

MonoGame/XNA

// Simple particle system
class Particle
{
    public Vector2 Position;
    public Vector2 Velocity;
    public float Life;
    public Color Color;
}

List<Particle> particles = 
    new List<Particle>();

void CreateExplosion(Vector2 position)
{
    for (int i = 0; i < 50; i++)
    {
        particles.Add(new Particle
        {
            Position = position,
            Velocity = new Vector2(
                Random(-100, 100),
                Random(-100, 100)
            ),
            Life = 1.0f,
            Color = Color.Orange
        });
    }
}

void UpdateParticles(float deltaTime)
{
    foreach (var p in particles)
    {
        p.Position += p.Velocity * deltaTime;
        p.Life -= deltaTime;
    }
    particles.RemoveAll(p => p.Life <= 0);
}

Flutter/Flame

// Particle system in Flame
import 'package:flame/particles.dart';

void createExplosion(Vector2 position) {
  final particle = ParticleSystemComponent(
    particle: Particle.generate(
      count: 50,
      lifespan: 1.0,
      generator: (i) => AcceleratedParticle(
        acceleration: Vector2(0, 100),
        speed: Vector2.random() * 200,
        position: position,
        child: CircleParticle(
          radius: 2.0,
          paint: Paint()
            ..color = Colors.orange,
        ),
      ),
    ),
  );
  
  add(particle);
}

// Advanced particle with sprites
void createSmokeEffect(Vector2 position) {
  add(
    ParticleSystemComponent(
      particle: Particle.generate(
        count: 20,
        generator: (i) => MovingParticle(
          from: position,
          to: position + Vector2(0, -100),
          curve: Curves.easeOut,
          child: ScalingParticle(
            to: 2.0,
            child: SpriteParticle(
              sprite: smokeSprite,
            ),
          ),
        ),
      ),
    ),
  );
}

Key differences

Audio & Sound Effects

Both frameworks provide audio playback for music and sound effects.

MonoGame/XNA

// Audio in MonoGame
SoundEffect jumpSound;
SoundEffectInstance musicInstance;
Song backgroundMusic;

protected override void LoadContent()
{
    jumpSound = Content.Load<SoundEffect>(
        "jump"
    );
    backgroundMusic = Content.Load<Song>(
        "background"
    );
    
    // Play music
    MediaPlayer.Play(backgroundMusic);
    MediaPlayer.IsRepeating = true;
    MediaPlayer.Volume = 0.5f;
}

void PlayJumpSound()
{
    // Simple playback
    jumpSound.Play();
    
    // With volume and pitch
    jumpSound.Play(
        volume: 0.8f,
        pitch: 0.0f,
        pan: 0.0f
    );
}

void StopMusic()
{
    MediaPlayer.Stop();
}

Flutter/Flame

// Audio in Flame
import 'package:flame_audio/flame_audio.dart';

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    // Preload audio
    await FlameAudio.audioCache.loadAll([
      'jump.mp3',
      'background.mp3',
    ]);
    
    // Play background music
    FlameAudio.bgm.initialize();
    await FlameAudio.bgm.play(
      'background.mp3',
      volume: 0.5,
    );
  }
  
  void playJumpSound() {
    // Simple playback
    FlameAudio.play('jump.mp3');
    
    // With volume
    FlameAudio.play(
      'jump.mp3',
      volume: 0.8,
    );
  }
  
  void stopMusic() {
    FlameAudio.bgm.stop();
  }
  
  @override
  void onRemove() {
    FlameAudio.bgm.dispose();
    super.onRemove();
  }
}

Key differences

Tilemaps & Level Design

Both frameworks support tilemap rendering for 2D level design.

MonoGame/XNA

// Tilemap in MonoGame
Texture2D tilesheet;
int[,] mapData;
int tileWidth = 32;
int tileHeight = 32;

protected override void LoadContent()
{
    tilesheet = Content.Load<Texture2D>(
        "tilesheet"
    );
    
    // Load map data
    mapData = new int[,] {
        { 1, 1, 1, 1, 1 },
        { 1, 0, 0, 0, 1 },
        { 1, 0, 2, 0, 1 },
        { 1, 1, 1, 1, 1 },
    };
}

protected override void Draw(
    GameTime gameTime
)
{
    spriteBatch.Begin();
    
    for (int y = 0; y < mapData.GetLength(0); y++)
    {
        for (int x = 0; x < mapData.GetLength(1); x++)
        {
            int tileId = mapData[y, x];
            Rectangle source = new Rectangle(
                (tileId % 8) * tileWidth,
                (tileId / 8) * tileHeight,
                tileWidth,
                tileHeight
            );
            
            spriteBatch.Draw(
                tilesheet,
                new Vector2(x * tileWidth, 
                    y * tileHeight),
                source,
                Color.White
            );
        }
    }
    
    spriteBatch.End();
}

Flutter/Flame

// Tilemap with Tiled in Flame
import 'package:flame_tiled/flame_tiled.dart';

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    // Load Tiled map
    final tiledMap = await TiledComponent.load(
      'map.tmx',
      Vector2.all(32),
    );
    add(tiledMap);
  }
}

// Manual tilemap rendering
class TileMap extends Component {
  late Sprite tileSprite;
  final List<List<int>> mapData = [
    [1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 2, 0, 1],
    [1, 1, 1, 1, 1],
  ];
  
  @override
  Future<void> onLoad() async {
    tileSprite = await Sprite.load(
      'tilesheet.png'
    );
  }
  
  @override
  void render(Canvas canvas) {
    for (int y = 0; y < mapData.length; y++) {
      for (int x = 0; x < mapData[y].length; x++) {
        final tileId = mapData[y][x];
        final srcX = (tileId % 8) * 32.0;
        final srcY = (tileId ~/ 8) * 32.0;
        
        tileSprite.renderRect(
          canvas,
          Rect.fromLTWH(
            x * 32.0, y * 32.0, 32, 32
          ),
          srcPosition: Vector2(srcX, srcY),
          srcSize: Vector2(32, 32),
        );
      }
    }
  }
}

Key differences

Physics Simulation

Both frameworks support physics engines. Flame integrates with Forge2D (Box2D port) for advanced physics.

MonoGame/XNA

// Using Farseer Physics (Box2D port)
World physicsWorld;
Body playerBody;

void Initialize()
{
    physicsWorld = new World(
        new Vector2(0, 9.8f)
    );
    
    // Create player body
    playerBody = BodyFactory.CreateRectangle(
        physicsWorld,
        1.0f, // width
        2.0f, // height
        1.0f, // density
        new Vector2(100, 100)
    );
    playerBody.BodyType = BodyType.Dynamic;
    playerBody.Restitution = 0.3f;
    playerBody.Friction = 0.5f;
}

void Update(GameTime gameTime)
{
    float deltaTime = (float)gameTime
        .ElapsedGameTime.TotalSeconds;
    
    // Step physics simulation
    physicsWorld.Step(deltaTime);
    
    // Apply forces
    playerBody.ApplyForce(
        new Vector2(100, 0)
    );
    
    // Sync graphics with physics
    playerPosition = playerBody.Position;
}

Flutter/Flame

// Using Flame Forge2D
import 'package:flame_forge2d/flame_forge2d.dart';

class MyGame extends Forge2DGame {
  @override
  Future<void> onLoad() async {
    world.gravity = Vector2(0, 9.8);
    
    add(Player());
  }
}

class Player extends BodyComponent {
  @override
  Body createBody() {
    final shape = PolygonShape()
      ..setAsBoxXY(0.5, 1.0);
    
    final fixtureDef = FixtureDef(shape)
      ..restitution = 0.3
      ..friction = 0.5
      ..density = 1.0;
    
    final bodyDef = BodyDef()
      ..position = Vector2(5, 5)
      ..type = BodyType.dynamic;
    
    return world.createBody(bodyDef)
      ..createFixture(fixtureDef);
  }
  
  void jump() {
    body.applyLinearImpulse(
      Vector2(0, -300)
    );
  }
  
  void moveRight() {
    body.applyForce(Vector2(100, 0));
  }
}

Key differences


Related reading

Official docs: Flame Documentation and Flame Examples.