Atès que aquest programa té molts fitxers, fem un resum final de la versió més completa. Aquest és l'arbre de carpetes:
assets
images - Imatges
sheet.png - Full on hi ha totes les imatges del joc
lib
main.dart - Programa principal
config.dart - Configuració
breakout.dart - Part principal del joc
core - Dades de tipus general
colors.dart - Definició dels colors que fem servir
components - Elements de les vistes
components.dart - Exportació de components
play_area.dart - Àrea de joc
bola.dart - Bola
pala.dart - Pala
totxo.dart - Totxo
widgets - Ginys
game_app.dart - Estructura general del joc
puntuacio.dart - Lloc on mostrar la puntuació
sobreposat.dart - Gestiona els textos sobreposats
Comencem pels fitxers de la carpeta lib.
main.dart
import 'package:flutter/material.dart'; import 'widgets/game_app.dart';
void main() {
runApp(const GameApp());
}
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 posBolaImg1 = Vector2(256, 320);
Vector2 posBolaImg2 = Vector2(320, 320);
Vector2 midaBolaImg = Vector2(64, 64);
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}
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,
),
posicio: llistaCoordenades[j],
colorParticules: ColorsApp.colorsTotxos[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;
}
A continuació el de la carpeta core.
colors.dart
import 'dart:ui';
class ColorsApp {
static const Color fons = Color(0xFFFFEECC);
static const Color tema = Color(0xFF114477);
static const gradient = [ // Gradient del marc
Color(0xFFAADDEE),
Color(0xFFFFEECC)
];
static const colorsTotxos = [
Color(0xFF2277AA),
Color(0xFF99BB66),
Color(0xFFFFCC44),
Color(0xFFFF9911),
Color(0xFFFF4444),
];
}
Ara la carpeta components.
components.dart
export 'bola.dart'; export 'pala.dart'; export 'totxo.dart'; export 'play_area.dart';
play_area.dart
import 'dart:async'; import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:flame/collisions.dart'; import 'package:joc_breakout/core/colors.dart'; import '../breakout.dart';
class PlayArea extends RectangleComponent with HasGameReference<Breakout> {
PlayArea() : super(
paint: Paint()
..color = ColorsApp.fons,
children: [RectangleHitbox()],
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
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;
// Declarem els dos sprites com a variables de la classe per no recrear-los constantment
late final Sprite spriteDreta;
late final Sprite spriteEsquerra;
// En carregar l'sprite, definim el fitxer i el detector de col·lisions
@override
Future<void> onLoad() async {
super.onLoad();
// Carreguem la imatge des del fitxer
spriteDreta = Sprite(
game.images.fromCache('sheet.png'),
srcPosition: posBolaImg1, // Coordenades inici bola
srcSize: midaBolaImg, // Mida de la bola
);
spriteEsquerra = Sprite(
game.images.fromCache('sheet.png'),
srcPosition: posBolaImg2, // Coordenades inici bola
srcSize: midaBolaImg, // Mida de la bola
);
// Definim l'sprite inicial segons la velocitat de partida
sprite = velocity.x >= 0 ? spriteDreta : spriteEsquerra;
// 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;
// Actualitzem l'sprite dinàmicament a cada frame segons la direcció actual
if (velocity.x >= 0) {
sprite = spriteDreta;
} else {
sprite = spriteEsquerra;
}
}
@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);
}
}
}
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();
// 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),
),
);
}
}
totxo.dart
import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/particles.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> {
// Afegim 'this.colorParticules' al constructor
// Ara demanem la posició en la imatge
Totxo({
required super.position,
required this.posicio,
required this.colorParticules,
})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
);
final Vector2 posicio; // Posició de cada totxo al fitxer
final Color colorParticules;
// En carregar l'sprite, definim el fitxer i el detector de col·lisions
@override
Future<void> onLoad() async {
super.onLoad();
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);
// Només generem l'efecte si el xoc és amb la bola
if (other is Bola) {
_creaExplosioParticules();
}
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>());
}
}
// Efecte de partícules en explosió
void _creaExplosioParticules() {
final random = Random();
// Afegim el sistema de partícules directament al món del joc
game.world.add(
ParticleSystemComponent(
position: position.clone(), // Sortiran des del centre del totxo actual
particle: Particle.generate(
count: 15, // Nombre de partícules de l'explosió
lifespan: 0.4, // Durada de l'efecte en segons
generator: (i) {
// Calculem una direcció i velocitat aleatòria (360 graus)
final angle = random.nextDouble() * 2 * pi;
final velocitat = random.nextDouble() * 120 + 60;
return AcceleratedParticle(
acceleration: Vector2(0, 250), // Gravetat cap avall perquè caiguin
speed: Vector2(cos(angle), sin(angle)) * velocitat,
child: CircleParticle(
radius: 2.0 + random.nextDouble() * 3.0, // Mida aleatòria de la partícula
paint: Paint()..color = colorParticules,
),
);
},
),
),
);
}
}
I, per acabar, la carpeta widgets.
game_app.dart
import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:joc_breakout/core/colors.dart'; import '../breakout.dart'; import '../config.dart'; import 'sobreposat.dart'; import 'puntuacio.dart';
class GameApp extends StatefulWidget {
const GameApp({super.key});
@override
State<GameApp> createState() => _GameAppState();
}
class _GameAppState extends State<GameApp> {
late final Breakout game;
@override
void initState() {
super.initState();
game = Breakout();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: ColorsApp.tema,
displayColor: ColorsApp.tema, // Color del text
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: ColorsApp.gradient, // Gradient del marc
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
children: [
Puntuacio(punts: game.punts),
Expanded(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget(
game: game,
overlayBuilderMap: {
PlayState.inici.name: (context, game) =>
const Sobreposat(
title: 'PICA PER COMENÇAR',
subtitle: 'Empra les fletxes o fes lliscar',
),
PlayState.fi.name: (context, game) =>
const Sobreposat(
title: 'F I D E L J O C',
subtitle: 'Pica per tornar a jugar',
),
PlayState.guanya.name: (context, game) =>
const Sobreposat(
title: 'H A S G U A N Y A T !',
subtitle: 'Pica per tornar a jugar',
),
}
)
)
)
),
],
),
),
),
),
),
),
);
}
}
puntuacio.dart
import 'package:flutter/material.dart';
class Puntuacio extends StatelessWidget {
const Puntuacio({super.key, required this.punts});
final ValueNotifier<int> punts;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: punts,
builder: (context, punts, child) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
child: Text(
'Punts: $punts'.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge!,
),
);
},
);
}
}
sobreposat.dart
import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart';
class Sobreposat extends StatelessWidget {
const Sobreposat({super.key, required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
alignment: const Alignment(0, -0.15),
child: IgnorePointer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
).animate().slideY(duration: 750.ms, begin: -3, end: 0),
const SizedBox(height: 16),
Text(subtitle, style: Theme.of(context).textTheme.headlineSmall)
.animate(onPlay: (controller) => controller.repeat())
.fadeIn(duration: 1.seconds)
.then()
.fadeOut(duration: 1.seconds),
],
),
),
);
}
}

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