I'm developing a Flutter game using the Flame Engine that runs on Flutter Web. I declare my assets in pubspec.yaml as follows:
assets:
- assets/start_screen_ui/
- assets/start_screen_ui/live_mode.png
My asset folder structure is:
project_root/
└── assets/
└── start_screen_ui/
├── live_mode.png
├── career_mode.png
├── manual.png
└── shop.gif
In my code (lib/screens/start_screen.dart), I load a sprite with:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
class StartScreen extends StatelessWidget {
const StartScreen({super.key});
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
return Scaffold(
body: GameWidget(
game: StartScreenGame(),
overlayBuilderMap: {
'settings': (context, game) =>
SettingsOverlay(game: game as StartScreenGame),
'support': (context, game) =>
SupportOverlay(game: game as StartScreenGame),
},
),
);
}
}
class StartScreenGame extends FlameGame with TapCallbacks {
late TextComponent gameTitle;
late Component
liveModeButton; // Changed to Component to handle both SpriteComponent and RectangleComponent
late Component careerModeButton;
late Component gameManualButton;
late RectangleComponent settingsButton;
late RectangleComponent logoutButton;
late Component shopButton;
late RectangleComponent supportButton;
late CircleComponent profileCircle;
@override
Future<void> onLoad() async {
await super.onLoad();
final backgroundGradient = GradientComponent(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0D47A1), Color(0xFF000000)],
),
size: size,
);
add(backgroundGradient);
gameTitle = TextComponent(
text: 'The Flash',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(2, 2),
blurRadius: 3.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
anchor: Anchor.center,
position: Vector2(size.x / 2, size.y * 0.2),
);
add(gameTitle);
final centerX = size.x / 2;
final buttonBaseY = size.y * 0.45;
final buttonSize = Vector2(150, 80);
final buttonSpacing = 160.0;
// Live Mode button
try {
print(
'Attempting to load live mode sprite from: start_screen_ui/live_mode.png');
final liveModeSprite = await loadSprite('start_screen_ui/live_mode.png');
print('Successfully loaded live mode sprite');
liveModeButton = SpriteComponent(
sprite: liveModeSprite,
size: buttonSize,
position: Vector2(centerX - buttonSpacing * 1.5, buttonBaseY),
anchor: Anchor.center,
);
} catch (e) {
print('Error loading live_mode.png: $e');
print('Stack trace: ${StackTrace.current}');
liveModeButton = RectangleComponent(
size: buttonSize,
position: Vector2(centerX - buttonSpacing * 1.5, buttonBaseY),
paint: Paint()..color = Colors.red.withOpacity(0.7),
anchor: Anchor.center,
);
}
add(liveModeButton);
add(TextComponent(
text: 'Live Mode',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(
centerX - buttonSpacing * 1.5, buttonBaseY + buttonSize.y * 0.6),
anchor: Anchor.center,
));
// Career Mode button
try {
print(
'Attempting to load career mode sprite from: start_screen_ui/career_mode.png');
final careerModeSprite =
await loadSprite('start_screen_ui/career_mode.png');
print('Successfully loaded career mode sprite');
careerModeButton = SpriteComponent(
sprite: careerModeSprite,
size: buttonSize,
position: Vector2(centerX - buttonSpacing * 0.5, buttonBaseY),
anchor: Anchor.center,
);
} catch (e) {
print('Error loading career_mode.png: $e');
print('Stack trace: ${StackTrace.current}');
careerModeButton = RectangleComponent(
size: buttonSize,
position: Vector2(centerX - buttonSpacing * 0.5, buttonBaseY),
paint: Paint()..color = Colors.blue.withOpacity(0.7),
anchor: Anchor.center,
);
}
add(careerModeButton);
add(TextComponent(
text: 'Career Mode',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(
centerX - buttonSpacing * 0.5, buttonBaseY + buttonSize.y * 0.6),
anchor: Anchor.center,
));
// Game Manual button
try {
print(
'Attempting to load manual sprite from: start_screen_ui/manual.png');
final manualSprite = await loadSprite('start_screen_ui/manual.png');
print('Successfully loaded manual sprite');
gameManualButton = SpriteComponent(
sprite: manualSprite,
size: buttonSize,
position: Vector2(centerX + buttonSpacing * 0.5, buttonBaseY),
anchor: Anchor.center,
);
} catch (e) {
print('Error loading manual.png: $e');
print('Stack trace: ${StackTrace.current}');
gameManualButton = RectangleComponent(
size: buttonSize,
position: Vector2(centerX + buttonSpacing * 0.5, buttonBaseY),
paint: Paint()..color = Colors.green.withOpacity(0.7),
anchor: Anchor.center,
);
}
add(gameManualButton);
add(TextComponent(
text: 'Game Manual',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(
centerX + buttonSpacing * 0.5, buttonBaseY + buttonSize.y * 0.6),
anchor: Anchor.center,
));
// Settings button
settingsButton = RectangleComponent(
size: buttonSize,
position: Vector2(centerX + buttonSpacing * 1.5, buttonBaseY),
paint: Paint()..color = Colors.grey.withOpacity(0.7),
anchor: Anchor.center,
);
add(settingsButton);
add(TextComponent(
text: '⚙️',
textRenderer: TextPaint(style: const TextStyle(fontSize: 24)),
position: Vector2(centerX + buttonSpacing * 1.5, buttonBaseY - 10),
anchor: Anchor.center,
));
add(TextComponent(
text: 'Settings',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(
centerX + buttonSpacing * 1.5, buttonBaseY + buttonSize.y * 0.6),
anchor: Anchor.center,
));
// Logout button
logoutButton = RectangleComponent(
size: Vector2(100, 40),
position: Vector2(
centerX + buttonSpacing * 1.5, buttonBaseY + buttonSize.y + 20),
paint: Paint()..color = Colors.redAccent.withOpacity(0.7),
anchor: Anchor.center,
);
add(logoutButton);
add(TextComponent(
text: 'Logout',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 14,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(
centerX + buttonSpacing * 1.5, buttonBaseY + buttonSize.y + 20),
anchor: Anchor.center,
));
// Profile circle
profileCircle = CircleComponent(
radius: 25,
position: Vector2(size.x - 40, 40),
paint: Paint()..color = Colors.white.withOpacity(0.5),
);
add(profileCircle);
add(TextComponent(
text: '👤',
textRenderer: TextPaint(style: const TextStyle(fontSize: 24)),
position: Vector2(size.x - 40, 40),
anchor: Anchor.center,
));
// Shop button
try {
print('Attempting to load shop sprite from: start_screen_ui/shop.gif');
final shopSprite = await loadSprite('start_screen_ui/shop.gif');
print('Successfully loaded shop sprite');
shopButton = SpriteComponent(
sprite: shopSprite,
size: Vector2(80, 80),
position: Vector2(centerX, size.y - 50),
anchor: Anchor.center,
);
} catch (e) {
print('Error loading shop.gif: $e');
print('Stack trace: ${StackTrace.current}');
shopButton = RectangleComponent(
size: Vector2(80, 80),
position: Vector2(centerX, size.y - 50),
paint: Paint()..color = Colors.amber.withOpacity(0.7),
anchor: Anchor.center,
);
}
add(shopButton);
add(TextComponent(
text: '🛒',
textRenderer: TextPaint(style: const TextStyle(fontSize: 24)),
position: Vector2(centerX, size.y - 50),
anchor: Anchor.center,
));
add(TextComponent(
text: 'Shop',
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 16,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
),
),
position: Vector2(centerX, size.y - 15),
anchor: Anchor.center,
));
// Support button
supportButton = RectangleComponent(
size: Vector2(60, 60),
position: Vector2(size.x - 40, size.y - 40),
paint: Paint()..color = Colors.teal.withOpacity(0.7),
anchor: Anchor.center,
);
add(supportButton);
add(TextComponent(
text: '❓',
textRenderer: TextPaint(style: const TextStyle(fontSize: 24)),
position: Vector2(size.x - 40, size.y - 40),
anchor: Anchor.center,
));
}
@override
void onTapDown(TapDownEvent event) {
super.onTapDown(event);
final touchPosition = event.canvasPosition;
if (liveModeButton.containsPoint(touchPosition)) {
print('Live Mode pressed');
} else if (careerModeButton.containsPoint(touchPosition)) {
print('Career Mode pressed');
} else if (gameManualButton.containsPoint(touchPosition)) {
print('Game Manual pressed');
} else if (settingsButton.containsPoint(touchPosition)) {
print('Settings pressed');
overlays.add('settings');
} else if (logoutButton.containsPoint(touchPosition)) {
print('Logout pressed');
} else if (shopButton.containsPoint(touchPosition)) {
print('Shop pressed');
} else if (supportButton.containsPoint(touchPosition)) {
print('Support pressed');
overlays.add('support');
} else if (profileCircle.containsPoint(touchPosition)) {
print('Profile pressed');
}
}
}
class GradientComponent extends Component {
final LinearGradient gradient;
final Vector2 size;
GradientComponent({required this.gradient, required this.size});
@override
void render(Canvas canvas) {
final rect = Rect.fromLTWH(0, 0, size.x, size.y);
final paint = Paint()..shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
}
}
class SettingsOverlay extends StatelessWidget {
final StartScreenGame game;
const SettingsOverlay({super.key, required this.game});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 400,
height: 300,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Settings',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
const ListTile(
leading: Icon(Icons.volume_up, color: Colors.white),
title: Text('Sound', style: TextStyle(color: Colors.white)),
trailing: Icon(Icons.toggle_on, color: Colors.green, size: 40),
),
const ListTile(
leading: Icon(Icons.vibration, color: Colors.white),
title: Text('Vibration', style: TextStyle(color: Colors.white)),
trailing: Icon(Icons.toggle_off, color: Colors.grey, size: 40),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => game.overlays.remove('settings'),
child: const Text('Close'),
),
],
),
),
);
}
}
class SupportOverlay extends StatelessWidget {
final StartScreenGame game;
const SupportOverlay({super.key, required this.game});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 400,
height: 300,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Support',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
const Text('Need help with the game? Contact our support team:',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center),
const SizedBox(height: 10),
const Text('support@myproject.com',
style:
TextStyle(color: Colors.blue, fontWeight: FontWeight.bold)),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () => game.overlays.remove('support'),
child: const Text('Close'),
),
],
),
),
);
}
}
However, when running on Flutter Web, the asset fails to load with an error like:
Error while trying to load an asset: Flutter Web engine failed to fetch "assets/assets/images/start_screen_ui/manual.png". HTTP request succeeded, but the server responded with HTTP status 404.
It appears that a duplicate path segment (assets/images/) is being prepended. I have verified that my assets are indeed under assets/start_screen_ui/ and my pubspec.yaml doesn't mention an extra assets/images/ folder.
My questions are:
Where could this extra "assets/images/" prefix be coming from? How can I fix my configuration/code so that the correct path (assets/start_screen_ui/manual.png) is used both on mobile and Flutter Web?
build/flutter_assets/AssetManifest.json:
{"assets/start_screen_ui/career_mode.png":["assets/start_screen_ui/career_mode.png"],"assets/start_screen_ui/live_mode.png":["assets/start_screen_ui/live_mode.png"],"assets/start_screen_ui/manual.png":["assets/start_screen_ui/manual.png"],"assets/start_screen_ui/shop.gif":["assets/start_screen_ui/shop.gif"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
The default setting for the image cache in Flame is to use assets/images/
.
You can change the static image cache prefix by doing Flame.images.prefix = assets/';
or if you're using a custom cache, either set it in the constructor of the cache when you create it or do game.images.prefix = assets/
.