flutter_hitbox 0.0.1 copy "flutter_hitbox: ^0.0.1" to clipboard
flutter_hitbox: ^0.0.1 copied to clipboard

A powerful Flutter package for generating hitboxes from any shape (circles, rectangles, polygons, custom paths) and detecting collisions between them.

example/lib/main.dart

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Hitbox Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HitboxExamplePage(),
    );
  }
}

enum ShapeType { circle, square, polygon, complexArrow }

class HitboxExamplePage extends StatefulWidget {
  const HitboxExamplePage({super.key});

  @override
  State<HitboxExamplePage> createState() => _HitboxExamplePageState();
}

class _HitboxExamplePageState extends State<HitboxExamplePage> {
  final Set<ShapeType> _selectedShapes = {};
  final Map<ShapeType, Offset> _shapePositions = {
    ShapeType.circle: const Offset(150, 200),
    ShapeType.square: const Offset(300, 200),
    ShapeType.polygon: const Offset(200, 350),
    ShapeType.complexArrow: const Offset(400, 300),
  };
  ShapeType? _draggingShape;
  bool _showHitbox = true;

  @override
  Widget build(BuildContext context) {
    final hitboxes = _getHitboxes();
    final collisions = _detectCollisions(hitboxes);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Flutter Hitbox Example'),
      ),
      body: Column(
        children: [
          // Shape selector
          Container(
            padding: const EdgeInsets.all(16),
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Select Shapes (Multiple Selection):',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 12),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    _buildShapeCheckbox(
                      ShapeType.circle,
                      'Circle',
                      Icons.circle,
                    ),
                    _buildShapeCheckbox(
                      ShapeType.square,
                      'Square',
                      Icons.crop_square,
                    ),
                    _buildShapeCheckbox(
                      ShapeType.polygon,
                      'Polygon',
                      Icons.change_history,
                    ),
                    _buildShapeCheckbox(
                      ShapeType.complexArrow,
                      'Arrow',
                      Icons.arrow_forward,
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Text(
                      'Show Hitbox:',
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    const SizedBox(width: 8),
                    Switch(
                      value: _showHitbox,
                      onChanged: (value) {
                        setState(() {
                          _showHitbox = value;
                        });
                      },
                    ),
                  ],
                ),
              ],
            ),
          ),
          // Canvas area
          Expanded(
            child: GestureDetector(
              onPanStart: (details) {
                _draggingShape = _getShapeAtPosition(details.localPosition);
              },
              onPanUpdate: (details) {
                if (_draggingShape != null) {
                  setState(() {
                    _shapePositions[_draggingShape!] = details.localPosition;
                  });
                }
              },
              onPanEnd: (_) {
                _draggingShape = null;
              },
              child: CustomPaint(
                painter: MultiShapeHitboxPainter(
                  selectedShapes: _selectedShapes,
                  shapePositions: _shapePositions,
                  showHitbox: _showHitbox,
                  collisions: collisions,
                ),
                size: Size.infinite,
              ),
            ),
          ),
          // Info panel
          Container(
            padding: const EdgeInsets.all(16),
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  'Collision Detection:',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                if (collisions.isEmpty)
                  Text(
                    'No collisions detected',
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Colors.green,
                      fontWeight: FontWeight.bold,
                    ),
                  )
                else
                  ...collisions.map(
                    (collision) => Padding(
                      padding: const EdgeInsets.only(bottom: 4),
                      child: Text(
                        '⚠️ ${collision.shape1} ↔ ${collision.shape2}',
                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                          color: Colors.red,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                const SizedBox(height: 8),
                Text(
                  'Selected: ${_selectedShapes.length} shape(s)',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
                const SizedBox(height: 4),
                Text(
                  'Drag shapes to test collision detection',
                  style: Theme.of(
                    context,
                  ).textTheme.bodySmall?.copyWith(fontStyle: FontStyle.italic),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildShapeCheckbox(ShapeType shape, String label, IconData icon) {
    final isSelected = _selectedShapes.contains(shape);
    return FilterChip(
      label: Row(
        mainAxisSize: MainAxisSize.min,
        children: [Icon(icon, size: 18), const SizedBox(width: 4), Text(label)],
      ),
      selected: isSelected,
      onSelected: (selected) {
        setState(() {
          if (selected) {
            _selectedShapes.add(shape);
          } else {
            _selectedShapes.remove(shape);
          }
        });
      },
    );
  }

  ShapeType? _getShapeAtPosition(Offset position) {
    for (final shape in _selectedShapes) {
      final hitbox = _createHitbox(shape, _shapePositions[shape]!);
      if (hitbox.containsPoint(position)) {
        return shape;
      }
    }
    return null;
  }

  Map<ShapeType, Hitbox> _getHitboxes() {
    final hitboxes = <ShapeType, Hitbox>{};
    for (final shape in _selectedShapes) {
      hitboxes[shape] = _createHitbox(shape, _shapePositions[shape]!);
    }
    return hitboxes;
  }

  List<CollisionInfo> _detectCollisions(Map<ShapeType, Hitbox> hitboxes) {
    final collisions = <CollisionInfo>[];
    final shapes = hitboxes.keys.toList();

    for (int i = 0; i < shapes.length; i++) {
      for (int j = i + 1; j < shapes.length; j++) {
        final shape1 = shapes[i];
        final shape2 = shapes[j];
        final hitbox1 = hitboxes[shape1]!;
        final hitbox2 = hitboxes[shape2]!;

        if (hitbox1.intersects(hitbox2)) {
          collisions.add(
            CollisionInfo(
              shape1: _getShapeName(shape1),
              shape2: _getShapeName(shape2),
            ),
          );
        }
      }
    }

    return collisions;
  }

  String _getShapeName(ShapeType shape) {
    switch (shape) {
      case ShapeType.circle:
        return 'Circle';
      case ShapeType.square:
        return 'Square';
      case ShapeType.polygon:
        return 'Polygon';
      case ShapeType.complexArrow:
        return 'Arrow';
    }
  }

  Hitbox _createHitbox(ShapeType shape, Offset position) {
    switch (shape) {
      case ShapeType.circle:
        return CircleHitbox(position, 40);
      case ShapeType.square:
        return RectangleHitbox(
          Offset(position.dx - 30, position.dy - 30),
          const Size(60, 60),
        );
      case ShapeType.polygon:
        return PolygonHitbox(position, [
          const Offset(0, -30),
          const Offset(20, 10),
          const Offset(-20, 10),
        ]);
      case ShapeType.complexArrow:
        return _createComplexArrowHitbox(position);
    }
  }

  PolygonHitbox _createComplexArrowHitbox(Offset position) {
    final vertices = [
      const Offset(50, 0),
      const Offset(40, -10),
      const Offset(35, -8),
      const Offset(35, -4),
      const Offset(15, -4),
      const Offset(15, -10),
      const Offset(5, -15),
      const Offset(-3, -12),
      const Offset(-5, -8),
      const Offset(-5, 8),
      const Offset(-3, 12),
      const Offset(5, 15),
      const Offset(15, 10),
      const Offset(15, 4),
      const Offset(35, 4),
      const Offset(35, 8),
      const Offset(40, 10),
    ];
    return PolygonHitbox(position, vertices);
  }
}

class CollisionInfo {
  final String shape1;
  final String shape2;

  CollisionInfo({required this.shape1, required this.shape2});
}

class MultiShapeHitboxPainter extends CustomPainter {
  final Set<ShapeType> selectedShapes;
  final Map<ShapeType, Offset> shapePositions;
  final bool showHitbox;
  final List<CollisionInfo> collisions;

  MultiShapeHitboxPainter({
    required this.selectedShapes,
    required this.shapePositions,
    required this.showHitbox,
    required this.collisions,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final hitboxes = <ShapeType, Hitbox>{};
    final shapeColors = <ShapeType, Color>{
      ShapeType.circle: Colors.blue,
      ShapeType.square: Colors.green,
      ShapeType.polygon: Colors.purple,
      ShapeType.complexArrow: Colors.orange,
    };

    // Create hitboxes and draw shapes
    for (final shape in selectedShapes) {
      final position = shapePositions[shape]!;
      final hitbox = _createHitbox(shape, position);
      hitboxes[shape] = hitbox;

      final color = shapeColors[shape] ?? Colors.grey;
      final isColliding = _isShapeColliding(shape);

      _drawShape(canvas, shape, position, color, isColliding);
    }

    // Draw hitboxes
    if (showHitbox) {
      for (final shape in selectedShapes) {
        final hitbox = hitboxes[shape]!;
        _drawHitbox(canvas, hitbox, _isShapeColliding(shape));
      }
    }
  }

  bool _isShapeColliding(ShapeType shape) {
    return collisions.any(
      (c) =>
          c.shape1 == _getShapeName(shape) || c.shape2 == _getShapeName(shape),
    );
  }

  String _getShapeName(ShapeType shape) {
    switch (shape) {
      case ShapeType.circle:
        return 'Circle';
      case ShapeType.square:
        return 'Square';
      case ShapeType.polygon:
        return 'Polygon';
      case ShapeType.complexArrow:
        return 'Arrow';
    }
  }

  void _drawShape(
    Canvas canvas,
    ShapeType shape,
    Offset position,
    Color color,
    bool isColliding,
  ) {
    final shapePaint = Paint()
      ..style = PaintingStyle.fill
      ..color = (isColliding ? Colors.red : color).withValues(alpha: 0.7);

    final borderPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2
      ..color = isColliding ? Colors.red : color;

    switch (shape) {
      case ShapeType.circle:
        canvas.drawCircle(position, 40, shapePaint);
        canvas.drawCircle(position, 40, borderPaint);
        break;

      case ShapeType.square:
        final rect = Rect.fromCenter(center: position, width: 60, height: 60);
        canvas.drawRect(rect, shapePaint);
        canvas.drawRect(rect, borderPaint);
        break;

      case ShapeType.polygon:
        final path = Path();
        final vertices = [
          Offset(position.dx, position.dy - 30),
          Offset(position.dx + 20, position.dy + 10),
          Offset(position.dx - 20, position.dy + 10),
        ];
        path.moveTo(vertices[0].dx, vertices[0].dy);
        for (int i = 1; i < vertices.length; i++) {
          path.lineTo(vertices[i].dx, vertices[i].dy);
        }
        path.close();
        canvas.drawPath(path, shapePaint);
        canvas.drawPath(path, borderPaint);
        break;

      case ShapeType.complexArrow:
        _drawComplexArrow(canvas, position, shapePaint, borderPaint);
        break;
    }
  }

  void _drawComplexArrow(
    Canvas canvas,
    Offset pos,
    Paint fillPaint,
    Paint strokePaint,
  ) {
    final path = Path();
    path.moveTo(pos.dx + 50, pos.dy);
    path.lineTo(pos.dx + 40, pos.dy - 10);
    path.lineTo(pos.dx + 35, pos.dy - 8);
    path.lineTo(pos.dx + 35, pos.dy - 4);
    path.lineTo(pos.dx + 15, pos.dy - 4);
    path.lineTo(pos.dx + 15, pos.dy - 10);
    path.lineTo(pos.dx + 5, pos.dy - 15);
    path.lineTo(pos.dx - 3, pos.dy - 12);
    path.lineTo(pos.dx - 5, pos.dy - 8);
    path.lineTo(pos.dx - 5, pos.dy + 8);
    path.lineTo(pos.dx - 3, pos.dy + 12);
    path.lineTo(pos.dx + 5, pos.dy + 15);
    path.lineTo(pos.dx + 15, pos.dy + 10);
    path.lineTo(pos.dx + 15, pos.dy + 4);
    path.lineTo(pos.dx + 35, pos.dy + 4);
    path.lineTo(pos.dx + 35, pos.dy + 8);
    path.lineTo(pos.dx + 40, pos.dy + 10);
    path.close();
    canvas.drawPath(path, fillPaint);
    canvas.drawPath(path, strokePaint);
  }

  void _drawHitbox(Canvas canvas, Hitbox hitbox, bool isColliding) {
    final hitboxPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2
      ..color = isColliding ? Colors.red : Colors.red.withValues(alpha: 0.5);

    // Draw bounding box
    final bounds = hitbox.boundingBox;
    _drawDashedRect(canvas, bounds, hitboxPaint);

    // Draw hitbox shape outline
    final outlinePaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.5
      ..color = isColliding ? Colors.red : Colors.orange;

    if (hitbox is CircleHitbox) {
      canvas.drawCircle(hitbox.position, hitbox.radius, outlinePaint);
    } else if (hitbox is RectangleHitbox) {
      canvas.drawRect(hitbox.rect, outlinePaint);
    } else if (hitbox is PolygonHitbox) {
      final path = Path();
      final vertices = hitbox.worldVertices;
      if (vertices.isNotEmpty) {
        path.moveTo(vertices[0].dx, vertices[0].dy);
        for (int i = 1; i < vertices.length; i++) {
          path.lineTo(vertices[i].dx, vertices[i].dy);
        }
        path.close();
        canvas.drawPath(path, outlinePaint);
      }
    }
  }

  Hitbox _createHitbox(ShapeType shape, Offset position) {
    switch (shape) {
      case ShapeType.circle:
        return CircleHitbox(position, 40);
      case ShapeType.square:
        return RectangleHitbox(
          Offset(position.dx - 30, position.dy - 30),
          const Size(60, 60),
        );
      case ShapeType.polygon:
        return PolygonHitbox(position, [
          const Offset(0, -30),
          const Offset(20, 10),
          const Offset(-20, 10),
        ]);
      case ShapeType.complexArrow:
        return _createComplexArrowHitbox(position);
    }
  }

  PolygonHitbox _createComplexArrowHitbox(Offset position) {
    final vertices = [
      const Offset(50, 0),
      const Offset(40, -10),
      const Offset(35, -8),
      const Offset(35, -4),
      const Offset(15, -4),
      const Offset(15, -10),
      const Offset(5, -15),
      const Offset(-3, -12),
      const Offset(-5, -8),
      const Offset(-5, 8),
      const Offset(-3, 12),
      const Offset(5, 15),
      const Offset(15, 10),
      const Offset(15, 4),
      const Offset(35, 4),
      const Offset(35, 8),
      const Offset(40, 10),
    ];
    return PolygonHitbox(position, vertices);
  }

  void _drawDashedRect(Canvas canvas, Rect rect, Paint paint) {
    const dashWidth = 5.0;
    const dashSpace = 5.0;

    // Top edge
    double startX = rect.left;
    while (startX < rect.right) {
      canvas.drawLine(
        Offset(startX, rect.top),
        Offset((startX + dashWidth).clamp(rect.left, rect.right), rect.top),
        paint,
      );
      startX += dashWidth + dashSpace;
    }

    // Right edge
    double startY = rect.top;
    while (startY < rect.bottom) {
      canvas.drawLine(
        Offset(rect.right, startY),
        Offset(rect.right, (startY + dashWidth).clamp(rect.top, rect.bottom)),
        paint,
      );
      startY += dashWidth + dashSpace;
    }

    // Bottom edge
    startX = rect.left;
    while (startX < rect.right) {
      canvas.drawLine(
        Offset(startX, rect.bottom),
        Offset((startX + dashWidth).clamp(rect.left, rect.right), rect.bottom),
        paint,
      );
      startX += dashWidth + dashSpace;
    }

    // Left edge
    startY = rect.top;
    while (startY < rect.bottom) {
      canvas.drawLine(
        Offset(rect.left, startY),
        Offset(rect.left, (startY + dashWidth).clamp(rect.top, rect.bottom)),
        paint,
      );
      startY += dashWidth + dashSpace;
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate is! MultiShapeHitboxPainter ||
        oldDelegate.selectedShapes != selectedShapes ||
        oldDelegate.shapePositions != shapePositions ||
        oldDelegate.showHitbox != showHitbox ||
        oldDelegate.collisions.length != collisions.length;
  }
}
1
likes
160
points
110
downloads

Publisher

verified publisherbechattaoui.dev

Weekly Downloads

A powerful Flutter package for generating hitboxes from any shape (circles, rectangles, polygons, custom paths) and detecting collisions between them.

Repository (GitHub)
View/report issues

Topics

#collision-detection #hitbox #game-development #physics #shapes

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, vector_math

More

Packages that depend on flutter_hitbox