Añadir soporte multijugador usando Firestore
Cómo utilizar Firebase Cloud Firestore para implementar el modo multijugador en tu juego.
Los juegos multijugador necesitan una forma de sincronizar los estados del juego entre los jugadores. A grandes rasgos, existen dos tipos de juegos multijugador:
-
Alta tasa de actualización (High tick rate). Estos juegos necesitan sincronizar los estados del juego muchas veces por segundo con baja latencia. Esto incluiría juegos de acción, juegos deportivos, juegos de lucha.
-
Baja tasa de actualización (Low tick rate). Estos juegos solo necesitan sincronizar los estados del juego ocasionalmente, teniendo la latencia un menor impacto. Esto incluiría juegos de cartas, juegos de estrategia, juegos de rompecabezas.
Esto se asemeja a la diferenciación entre juegos en tiempo real versus juegos por turnos, aunque la analogía se queda corta. Por ejemplo, los juegos de estrategia en tiempo real se ejecutan —como sugiere el nombre— en tiempo real, pero eso no se correlaciona con una alta tasa de actualización. Estos juegos pueden simular gran parte de lo que sucede entre las interacciones de los jugadores en las máquinas locales. Por lo tanto, no necesitan sincronizar los estados del juego tan a menudo.
Como desarrollador, si puedes elegir tasas de actualización bajas, deberías hacerlo. Una tasa baja disminuye los requisitos de latencia y los costos del servidor. A veces, un juego requiere altas tasas de sincronización. Para esos casos, soluciones como Firestore no son una buena opción. Elige una solución de servidor multijugador dedicada como Nakama. Nakama tiene un paquete de Dart.
Si prevés que tu juego requiere una tasa de sincronización baja, continúa leyendo.
Esta receta demuestra cómo usar el
paquete cloud_firestore
para implementar capacidades multijugador en tu juego.
Esta receta no requiere un servidor.
Utiliza dos o más clientes que comparten el estado del juego usando Cloud Firestore.
1. Preparar tu juego para multijugador
#Escribe el código de tu juego para permitir cambiar el estado del juego en respuesta tanto a eventos locales como a eventos remotos. Un evento local podría ser una acción del jugador o alguna lógica de juego. Un evento remoto podría ser una actualización del mundo proveniente del servidor.
Para simplificar esta receta del libro de recetas, comienza con
la plantilla card que encontrarás
en el repositorio flutter/games.
Ejecuta el siguiente comando para clonar ese repositorio:
git clone https://github.com/flutter/games.git
Abre el proyecto en templates/card.
2. Instalar Firestore
#Cloud Firestore es una base de datos de documentos NoSQL en la nube que se escala horizontalmente. Incluye sincronización en vivo incorporada. Esto es perfecto para nuestras necesidades. Mantiene el estado del juego actualizado en la base de datos de la nube, de modo que cada jugador ve el mismo estado.
Si deseas una introducción rápida de 15 minutos a Cloud Firestore, echa un vistazo al siguiente video:
Para añadir Firestore a tu proyecto de Flutter, sigue los primeros dos pasos de la guía Get started with Cloud Firestore:
Los resultados esperados incluyen:
- Una base de datos de Firestore lista en la nube, en Test mode
- Un archivo
firebase_options.dartgenerado - Los plugins correspondientes añadidos a tu
pubspec.yaml
En este paso no necesitas escribir ningún código Dart. Tan pronto como comprendas el paso de escribir código Dart en esa guía, regresa a esta receta.
3. Inicializar Firestore
#-
Abre
lib/main.darte importa los plugins, así como el archivofirebase_options.dartque fue generado porflutterfire configureen el paso anterior.dartimport 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; -
Añade el siguiente código justo antes de la llamada a
runApp()enlib/main.dart:dartWidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);Esto asegura que Firebase se inicialice al iniciar el juego.
-
Añade la instancia de Firestore a la aplicación. De esa manera, cualquier Widget puede acceder a esta instancia. Los Widgets también pueden reaccionar si falta la instancia, si es necesario.
Para hacer esto con la plantilla
card, puedes usar elpaquete provider(que ya está instalado como dependencia).Reemplaza la plantilla estándar
runApp(MyApp())por lo siguiente:dartrunApp(Provider.value(value: FirebaseFirestore.instance, child: MyApp()));Coloca el provider por encima de
MyApp, no dentro de él. Esto te permite probar la aplicación sin Firebase.:::note En caso de que no estés trabajando con la plantilla
card, debes bien instalar el paqueteprovidero usar tu propio método para acceder a la instancia deFirebaseFirestoredesde varias partes de tu base de código. :::
4. Crear una clase controladora de Firestore
#Aunque puedes comunicarte con Firestore directamente, deberías escribir una clase controladora dedicada para hacer que el código sea más legible y mantenible.
La forma en que implementes el controlador depende de tu juego
y del diseño exacto de tu experiencia multijugador.
Para el caso de la plantilla card,
podrías sincronizar el contenido de las dos áreas de juego circulares.
No es suficiente para una experiencia multijugador completa,
pero es un buen comienzo.
Para crear un controlador, copia
y pega el siguiente código en un nuevo archivo llamado
lib/multiplayer/firestore_controller.dart.
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import '../game_internals/board_state.dart';
import '../game_internals/playing_area.dart';
import '../game_internals/playing_card.dart';
class FirestoreController {
static final _log = Logger('FirestoreController');
final FirebaseFirestore instance;
final BoardState boardState;
/// For now, there is only one match. But in order to be ready
/// for match-making, put it in a Firestore collection called matches.
late final DocumentReference<Map<String, Object?>> _matchRef = instance
.collection('matches')
.doc('match_1');
late final DocumentReference<List<PlayingCard>> _areaOneRef = _matchRef
.collection('areas')
.doc('area_one')
.withConverter<List<PlayingCard>>(
fromFirestore: _cardsFromFirestore,
toFirestore: _cardsToFirestore,
);
late final DocumentReference<List<PlayingCard>> _areaTwoRef = _matchRef
.collection('areas')
.doc('area_two')
.withConverter<List<PlayingCard>>(
fromFirestore: _cardsFromFirestore,
toFirestore: _cardsToFirestore,
);
late final StreamSubscription<void> _areaOneFirestoreSubscription;
late final StreamSubscription<void> _areaTwoFirestoreSubscription;
late final StreamSubscription<void> _areaOneLocalSubscription;
late final StreamSubscription<void> _areaTwoLocalSubscription;
FirestoreController({required this.instance, required this.boardState}) {
// Subscribe to the remote changes (from Firestore).
_areaOneFirestoreSubscription = _areaOneRef.snapshots().listen((snapshot) {
_updateLocalFromFirestore(boardState.areaOne, snapshot);
});
_areaTwoFirestoreSubscription = _areaTwoRef.snapshots().listen((snapshot) {
_updateLocalFromFirestore(boardState.areaTwo, snapshot);
});
// Subscribe to the local changes in game state.
_areaOneLocalSubscription = boardState.areaOne.playerChanges.listen((_) {
_updateFirestoreFromLocalAreaOne();
});
_areaTwoLocalSubscription = boardState.areaTwo.playerChanges.listen((_) {
_updateFirestoreFromLocalAreaTwo();
});
_log.fine('Initialized');
}
void dispose() {
_areaOneFirestoreSubscription.cancel();
_areaTwoFirestoreSubscription.cancel();
_areaOneLocalSubscription.cancel();
_areaTwoLocalSubscription.cancel();
_log.fine('Disposed');
}
/// Takes the raw JSON snapshot coming from Firestore and attempts to
/// convert it into a list of [PlayingCard]s.
List<PlayingCard> _cardsFromFirestore(
DocumentSnapshot<Map<String, Object?>> snapshot,
SnapshotOptions? options,
) {
final data = snapshot.data()?['cards'] as List<Object?>?;
if (data == null) {
_log.info('No data found on Firestore, returning empty list');
return [];
}
try {
return data
.cast<Map<String, Object?>>()
.map(PlayingCard.fromJson)
.toList();
} catch (e) {
throw FirebaseControllerException(
'Failed to parse data from Firestore: $e',
);
}
}
/// Takes a list of [PlayingCard]s and converts it into a JSON object
/// that can be saved into Firestore.
Map<String, Object?> _cardsToFirestore(
List<PlayingCard> cards,
SetOptions? options,
) {
return {'cards': cards.map((c) => c.toJson()).toList()};
}
/// Updates Firestore with the local state of [area].
Future<void> _updateFirestoreFromLocal(
PlayingArea area,
DocumentReference<List<PlayingCard>> ref,
) async {
try {
_log.fine('Updating Firestore with local data (${area.cards}) ...');
await ref.set(area.cards);
_log.fine('... done updating.');
} catch (e) {
throw FirebaseControllerException(
'Failed to update Firestore with local data (${area.cards}): $e',
);
}
}
/// Sends the local state of `boardState.areaOne` to Firestore.
void _updateFirestoreFromLocalAreaOne() {
_updateFirestoreFromLocal(boardState.areaOne, _areaOneRef);
}
/// Sends the local state of `boardState.areaTwo` to Firestore.
void _updateFirestoreFromLocalAreaTwo() {
_updateFirestoreFromLocal(boardState.areaTwo, _areaTwoRef);
}
/// Updates the local state of [area] with the data from Firestore.
void _updateLocalFromFirestore(
PlayingArea area,
DocumentSnapshot<List<PlayingCard>> snapshot,
) {
_log.fine('Received new data from Firestore (${snapshot.data()})');
final cards = snapshot.data() ?? [];
if (listEquals(cards, area.cards)) {
_log.fine('No change');
} else {
_log.fine('Updating local data with Firestore data ($cards)');
area.replaceWith(cards);
}
}
}
class FirebaseControllerException implements Exception {
final String message;
FirebaseControllerException(this.message);
@override
String toString() => 'FirebaseControllerException: $message';
}
Observa las siguientes características de este código:
-
El constructor del controlador recibe un
BoardState. Esto permite al controlador manipular el estado local del juego. -
El controlador se suscribe tanto a los cambios locales para actualizar Firestore como a los cambios remotos para actualizar el estado local y la UI.
-
Los campos
_areaOneRefy_areaTwoRefson referencias a documentos de Firebase. Describen dónde residen los datos de cada área y cómo realizar la conversión entre los objetos locales de Dart (List<PlayingCard>) y los objetos JSON remotos (Map<String, dynamic>). La API de Firestore nos permite suscribirnos a estas referencias con.snapshots()y escribir en ellas con.set().
5. Usar el controlador de Firestore
#-
Abre el archivo responsable de iniciar la sesión de juego:
lib/play_session/play_session_screen.darten el caso de la plantillacard. Instanciarás el controlador de Firestore desde este archivo. -
Importar Firebase y el controlador:
dartimport 'package:cloud_firestore/cloud_firestore.dart'; import '../multiplayer/firestore_controller.dart'; -
Añade un campo que admita valores nulos (nullable) a la clase
_PlaySessionScreenStatepara contener una instancia del controlador:dartFirestoreController? _firestoreController; -
En el método
initState()de la misma clase, añade código que intente leer la instancia de FirebaseFirestore y, si tiene éxito, construya el controlador. Añadiste la instancia deFirebaseFirestoreamain.darten el paso de Inicializar Firestore.dartfinal firestore = context.read<FirebaseFirestore?>(); if (firestore == null) { _log.warning( "Firestore instance wasn't provided. " 'Running without _firestoreController.', ); } else { _firestoreController = FirestoreController( instance: firestore, boardState: _boardState, ); } -
Libera el controlador usando el método
dispose()de la misma clase.dart_firestoreController?.dispose();
6. Probar el juego
#-
Ejecuta el juego en dos dispositivos diferentes o en 2 ventanas distintas en el mismo dispositivo.
-
Observa cómo añadir una carta a un área en un dispositivo hace que aparezca en el otro.
-
Abre la consola web de Firebase y navega a la base de datos Firestore de tu proyecto.
-
Observa cómo se actualizan los datos en tiempo real. Incluso puedes editar los datos en la consola y ver cómo se actualizan todos los clientes en ejecución.

Resolución de problemas
#Los problemas más comunes que podrías encontrar al probar la integración de Firebase incluyen los siguientes:
-
El juego falla al intentar comunicarse con Firebase.
- La integración de Firebase no se ha configurado correctamente.
Vuelve a revisar el Paso 2 y asegúrate de ejecutar
flutterfire configurecomo parte de ese paso.
- La integración de Firebase no se ha configurado correctamente.
Vuelve a revisar el Paso 2 y asegúrate de ejecutar
-
El juego no se comunica con Firebase en macOS.
- Por defecto, las aplicaciones de macOS no tienen acceso a Internet. Habilita primero el permiso de Internet (internet entitlement).
7. Siguientes pasos
#En este punto, el juego tiene una sincronización de estado casi instantánea y confiable entre los clientes. Carece de reglas reales del juego: qué cartas se pueden jugar, cuándo y con qué resultados. Esto depende del juego en sí y te corresponde a ti probarlo.
En este punto, el estado compartido de la partida solo incluye
las dos áreas de juego y las cartas dentro de ellas.
También puedes guardar otros datos en _matchRef,
como quiénes son los jugadores y de quién es el turno.
Si no estás seguro de por dónde empezar,
sigue uno o dos codelabs de Firestore
para familiarizarte con la API.
Al principio, una sola partida debería ser suficiente
para probar tu juego multijugador con colegas y amigos.
A medida que te acerques a la fecha de lanzamiento,
piensa en la autenticación y el emparejamiento.
Afortunadamente, Firebase proporciona una
forma integrada de autenticar usuarios
y la estructura de la base de datos de Firestore puede manejar múltiples partidas.
En lugar de una única partida match_1,
puedes poblar la colección de partidas con tantos registros como sea necesario.
Una partida en línea puede comenzar en un estado de "espera", con solo el primer jugador presente. Otros jugadores pueden ver las partidas en "espera" en algún tipo de lobby. Una vez que suficientes jugadores se unen a una partida, esta se vuelve "activa". Una vez más, la implementación exacta depende del tipo de experiencia en línea que desees. Los conceptos básicos siguen siendo los mismos: una gran colección de documentos, cada uno representando una partida activa o potencial.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.