Flutter多人游戏:如何可靠检测App终止并更新Firebase玩家状态?
解决方案:可靠检测App终止并同步玩家状态
优先方案:Firebase Realtime Database .onDisconnect() 机制
这是Firebase官方针对客户端断开连接场景的原生解决方案,不受Flutter生命周期回调的限制,无论App是正常退出、被用户强制关闭还是系统杀进程,只要客户端与Realtime Database的TCP连接断开,服务器就会自动执行你预设的断开操作,完全可靠。
步骤1:Flutter端配置在线状态与断开回调
当玩家进入活跃队伍时,在Realtime Database写入在线状态,同时配置断开连接时的触发操作:
import 'package:firebase_database/firebase_database.dart'; void setupPlayerOnlineStatus(String playerId) async { final playerOnlineRef = FirebaseDatabase.instance.ref('players/$playerId/online'); // 设置当前玩家为在线状态 await playerOnlineRef.set(true); // 配置断开连接时的操作:移除在线节点(或设置为false) await playerOnlineRef.onDisconnect().remove(); // 也可以选择设置为false:await playerOnlineRef.onDisconnect().set(false); }
步骤2:Cloud Function监听状态变化并更新Firestore
编写Cloud Function监听Realtime Database的在线节点变化,当节点被移除(或状态变为false)时,自动从Firestore的活跃队伍中移除该玩家:
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); // 监听在线节点被删除的场景 exports.removePlayerOnDisconnect = functions.database.ref('/players/{playerId}/online') .onDelete(async (snapshot, context) => { const playerId = context.params.playerId; // 查找该玩家所在的活跃队伍 const teamQuery = await admin.firestore() .collection('teams') .where('activePlayers', 'array-contains', playerId) .get(); // 更新所有包含该玩家的队伍 if (!teamQuery.empty) { const batch = admin.firestore().batch(); teamQuery.docs.forEach(teamDoc => { batch.update(teamDoc.ref, { activePlayers: admin.firestore.FieldValue.arrayRemove(playerId) }); }); await batch.commit(); } });
如果选择用.onDisconnect().set(false),可以改为监听节点更新:
exports.removePlayerOnOffline = functions.database.ref('/players/{playerId}/online') .onUpdate(async (change, context) => { const isOnline = change.after.val(); if (!isOnline) { const playerId = context.params.playerId; // 同上执行队伍更新逻辑 const teamQuery = await admin.firestore() .collection('teams') .where('activePlayers', 'array-contains', playerId) .get(); if (!teamQuery.empty) { const batch = admin.firestore().batch(); teamQuery.docs.forEach(teamDoc => { batch.update(teamDoc.ref, { activePlayers: admin.firestore.FieldValue.arrayRemove(playerId) }); }); await batch.commit(); } } });
备选方案:Firestore心跳+定时清理(适合无法使用Realtime Database的场景)
如果必须全程用Firestore,可以采用心跳机制配合定时Cloud Function清理,但存在最多1分钟的延迟,资源消耗也更高:
步骤1:Flutter端发送心跳
玩家在线时每隔30秒向Firestore写入心跳时间戳:
import 'package:cloud_firestore/cloud_firestore.dart'; import 'dart:async'; Timer? _heartbeatTimer; void startPlayerHeartbeat(String playerId) { _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) async { await FirebaseFirestore.instance.collection('players').doc(playerId).update({ 'lastHeartbeat': FieldValue.serverTimestamp(), 'online': true }); }); } // App进入后台时可选取消定时器,不影响最终清理逻辑 void stopPlayerHeartbeat() { _heartbeatTimer?.cancel(); }
步骤2:Cloud Function定时清理离线玩家
创建每分钟执行一次的定时任务,扫描心跳超过1分钟的玩家并从队伍中移除:
exports.cleanupInactivePlayers = functions.pubsub.schedule('every 1 minutes').onRun(async (context) => { const oneMinuteAgo = admin.firestore.Timestamp.fromMillis(Date.now() - 60 * 1000); // 查询心跳超时的玩家 const inactivePlayersQuery = await admin.firestore() .collection('players') .where('lastHeartbeat', '<', oneMinuteAgo) .get(); const batch = admin.firestore().batch(); // 批量更新队伍和玩家状态 for (const playerDoc of inactivePlayersQuery.docs) { const playerId = playerDoc.id; // 查找玩家所在队伍并移除 const teamQuery = await admin.firestore() .collection('teams') .where('activePlayers', 'array-contains', playerId) .get(); teamQuery.docs.forEach(teamDoc => { batch.update(teamDoc.ref, { activePlayers: admin.firestore.FieldValue.arrayRemove(playerId) }); }); // 更新玩家状态为离线 batch.update(playerDoc.ref, {online: false}); } await batch.commit(); return null; });
为什么之前的AppLifecycleState.detached不可靠?
Flutter的生命周期回调仅在App正常退到后台或前台时稳定触发,当App被用户强制关闭、系统因资源不足杀进程时,detached状态可能根本来不及执行,导致Realtime Database的在线状态无法更新,进而Cloud Function无法触发。而.onDisconnect()是由Firebase服务器端检测连接断开,完全不受客户端生命周期的限制,是这类场景的最优解。
内容的提问来源于stack exchange,提问作者Ahmed Bahgat
相关产品推荐
相关产品推荐

