flutter_hitbox 0.0.1
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.
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;
}
}