Quan una aplicació fa servir uns quants sprites, no és recomanable anar carregant les imatges una per una. És molt més eficient posar totes les imatges en un sol fitxer, carregar-lo a l'inici i indicar la posició i la mida de la imatge que correspon a cada sprite.
En aquest cas, la nostra imatge serà la que es mostra a continuació, on trobem la pala, els cinc totxos i la bola.

Per baixar-la, pots picar sobre la imatge amb el botó dret i guardar-la a l'ordinador.
A la taula següent tenim les mides i la posició de cada imatge. La posició correspon a la coordenada superior esquerra.
| Element | Mides | Posició | ||
| Amplada | Alçada | X | Y | |
| Pala | 512 | 64 | 0 | 0 |
| Totxo blau | 256 | 128 | 0 | 64 |
| Totxo verd | 256 | 128 | 256 | 64 |
| Totxo groc | 256 | 128 | 0 | 192 |
| Totxo taronja | 256 | 128 | 256 | 192 |
| Totxo roig | 256 | 128 | 0 | 320 |
| Bola | 64 | 64 | 256 | 320 |
En el fitxer de configuració hem d'afegir les coordenades i les mides de cada una de les imatges dins el fitxer de gràfics. Cal importar el paquet vector_math.
config.dart
import 'package:vector_math/vector_math.dart';
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const numTotxosFilera = 10;
const juntaTotxos = gameWidth * 0.015;
final brickWidth =
(gameWidth - (juntaTotxos * (numTotxosFilera + 1))) / numTotxosFilera;
const brickHeight = gameHeight * 0.03;
const factorDif = 1.03;
// Configuració per al full d'sprites
Vector2 posPalaImg = Vector2(0, 0);
Vector2 midaPalaImg = Vector2(512, 64);
// Definim les coordenades de cada totxo dins de la imatge
final List<Vector2> llistaCoordenades = [
Vector2(0.0, 64.0), // Totxo blau
Vector2(256.0, 64.0), // Totxo verd
Vector2(0.0, 192.0), // Totxo groc
Vector2(256.0, 192.0), // Totxo taronja
Vector2(0.0, 320.0), // Totxo roig
];
Vector2 midaTotxoImg = Vector2(256, 128);
Vector2 posBolaImg = Vector2(256, 320);
Vector2 midaBolaImg = Vector2(64, 64);
Pot passar que ens surti un error lligat al paquet vector_math. En cas que així sigui, haurem d'afegir el paquet al fitxer pubspec.yaml.
dependencies:
flutter:
sdk: flutter
flame: ^1.28.1
flutter_animate: ^4.5.2
google_fonts: ^8.0.2
vector_math: any
Per a la bola, canviem la forma com es posa la imatge. Ens cal el fitxer de configuració, on hi ha la mida i la posició de la bola.
bola.dart
import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:joc_breakout/breakout.dart'; import 'package:joc_breakout/components/pala.dart'; import 'package:joc_breakout/components/totxo.dart'; import 'package:joc_breakout/components/play_area.dart'; import 'package:joc_breakout/config.dart';
class Bola extends SpriteComponent //
with CollisionCallbacks, HasGameReference<Breakout> {
Bola({
required this.velocity,
required super.position,
required double radius,
required this.factorDificultat,
}) : super(
// Posem el diàmetre (doble del radi) en les dues direccions
size: Vector2.all(radius * 2), //
anchor: Anchor.center,
);
final Vector2 velocity;
final double factorDificultat;
// En carregar l'sprite, definim el fitxer i el detector de col·lisions
@override
Future<void> onLoad() async {
super.onLoad();
// Carrega la imatge de la bola des d'assets/images/bola.png
//sprite = await game.loadSprite('bola.png');
// Carreguem la imatge des del fitxer
sprite = Sprite(
game.images.fromCache('sheet.png'),
srcPosition: posBolaImg, // Coordenades inici bola
srcSize: midaBolaImg, // Mida de la bola
);
// Detector de col·lisions circular ajustat a la mida de la imatge
add(CircleHitbox());
}
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(
RemoveEffect(
delay: 0.35,
onComplete: () {
game.playState = PlayState.fi;
},
),
);
}
} else if (other is Pala) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Totxo) {
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * factorDificultat);
}
}
}
Per a la pala, hem de fer una cosa similar.
pala.dart
import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/events.dart'; import 'package:joc_breakout/breakout.dart'; import 'package:joc_breakout/config.dart';
class Pala extends SpriteComponent
with DragCallbacks, HasGameReference<Breakout> {
Pala({
required super.position,
required super.size,
}) : super(
anchor: Anchor.center,
);
@override
Future<void> onLoad() async {
super.onLoad();
// Carrega la imatge de la pala des d'assets/images/pala.png
//sprite = await game.loadSprite('pala.png');
// Carreguem la imatge des del fitxer
sprite = Sprite(
game.images.fromCache('sheet.png'),
srcPosition: posPalaImg, // Coordenades inici pala
srcSize: midaPalaImg, // Mida de la pala
);
// Afegim la caixa de col·lisions rectangular adaptada a la mida del component
add(RectangleHitbox());
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
position.x = (position.x + event.localDelta.x).clamp(size.x / 2, game.width - size.x / 2);
}
void moveBy(double dx) {
add(
MoveToEffect(
Vector2((position.x + dx).clamp(size.x / 2, game.width - size.x / 2), position.y),
EffectController(duration: 0.1),
),
);
}
}
I també per als totxos. Aquí, però, la creació dels totxos es fa des del fitxer breakout.dart i la posició dins el fitxer es passa com a paràmetre, ja que depèn del color del totxo.
totxo.dart
import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:joc_breakout/breakout.dart'; import 'package:joc_breakout/config.dart'; import 'bola.dart'; import 'pala.dart';
class Totxo extends SpriteComponent
with CollisionCallbacks, HasGameReference<Breakout> {
//Totxo({required super.position, required this.imatge})
// Ara demanem la posició en la imatge
Totxo({required super.position, required this.posicio})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
);
// Afegim la variable per al nom de la imatge
//final String imatge;
final Vector2 posicio; // Posició de cada totxo al fitxer
// En carregar l'sprite, definim el fitxer i el detector de col·lisions
@override
Future<void> onLoad() async {
super.onLoad();
// Carrega l'sprite rebut pel constructor
//sprite = await game.loadSprite(imatge);
sprite = Sprite(
game.images.fromCache('sheet.png'),
srcPosition: posicio, // Utilitzem la posició que ens passen
srcSize: midaTotxoImg, // Mida del dibuix del totxo
);
// Afegim la caixa de col·lisions
add(RectangleHitbox());
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
game.punts.value++;
// Mira si era el darrer totxo
if (game.world.children.query<Totxo>().length == 1) {
game.playState = PlayState.guanya;
game.world.removeAll(game.world.children.query<Bola>());
game.world.removeAll(game.world.children.query<Pala>());
}
}
}
I ara falta incorporar els al joc. Ara no carregarem les cinc imatges sinó que enviarem la posició de cada color en l'arxiu gràfic.
breakout.dart
import 'dart:async'; import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:joc_breakout/core/colors.dart'; import 'components/components.dart'; import 'config.dart';
enum PlayState {inici, jugant, fi, guanya}
// Fem una llista amb les imatges dels totxos
//final llistaImatgesTotxos = [
//'totxo_blau.png',
//'totxo_verd.png',
//'totxo_groc.png',
//'totxo_taronja.png',
//'totxo_roig.png',
//];
class Breakout extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapCallbacks {
Breakout()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final ValueNotifier<int> punts = ValueNotifier(0);
final rand = math.Random(); // Generador de valors aleatoris
double get width => size.x;
double get height => size.y;
late PlayState _playState;
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.inici:
case PlayState.fi:
case PlayState.guanya:
overlays.add(playState.name);
case PlayState.jugant:
overlays.remove(PlayState.inici.name);
overlays.remove(PlayState.fi.name);
overlays.remove(PlayState.guanya.name);
}
}
@override
FutureOr<void> onLoad() async {
super.onLoad();
await images.load('sheet.png');
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.inici;
}
void startGame() {
if (playState == PlayState.jugant) return;
world.removeAll(world.children.query<Bola>());
world.removeAll(world.children.query<Pala>());
world.removeAll(world.children.query<Totxo>());
playState = PlayState.jugant;
punts.value = 0;
world.add(Bola(
factorDificultat: factorDif,
radius: ballRadius,
position: size / 2, // Centre de l'àrea de joc
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
));
world.add(
Pala(
size: Vector2(batWidth, batHeight),
position: Vector2(width / 2, height * 0.95),
),
);
// Afegim els totxos amb dos bucles
world.addAll([
for (var i = 0; i < numTotxosFilera; i++)
//for (var j = 0; j < llistaImatgesTotxos.length; j++)
for (var j = 0; j < llistaCoordenades.length; j++)
Totxo(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * juntaTotxos,
(j + 1 + 2.0) * brickHeight + (j + 1) * juntaTotxos,
),
//imatge: llistaImatgesTotxos[j],
posicio: llistaCoordenades[j],
),
]);
}
@override
void onTapDown(TapDownEvent event) {
super.onTapDown(event);
//if(playState == PlayState.inici){
startGame();
//}
}
@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Pala>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Pala>().first.moveBy(batStep);
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.enter:
startGame();
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => ColorsApp.fons;
}
Ara el joc hauria de funcionar igual que abans, però ocupant menys memòria.

Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.