import 'package:flame/game.dart'; import 'package:flame_realtime_shooting/game/game.dart'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; void main() async { await Supabase.initialize( url: 'supabaseUrl', anonKey: 'supabaseAnonKey', realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40), ); runApp(const MyApp()); } final supabase = Supabase.instance.client; class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'UFO Shooting Game', debugShowCheckedModeBanner: false, home: GamePage(), ); } } class GamePage extends StatefulWidget { const GamePage({Key? key}) : super(key: key); @override State createState() => _GamePageState(); } class _GamePageState extends State { late final MyGame _game; /// Holds the RealtimeChannel to sync game states RealtimeChannel? _gameChannel; @override Widget build(BuildContext context) { return Scaffold( body: Stack( fit: StackFit.expand, children: [ Image.asset('assets/images/background.jpg', fit: BoxFit.cover), GameWidget(game: _game), ], ), ); } @override void initState() { super.initState(); _initialize(); } Future _initialize() async { _game = MyGame( onGameStateUpdate: (position, health) async { ChannelResponse response; do { response = await _gameChannel!.sendBroadcastMessage( event: 'game_state', payload: {'x': position.x, 'y': position.y, 'health': health}, ); // wait for a frame to avoid infinite rate limiting loops await Future.delayed(Duration.zero); setState(() {}); } while (response == ChannelResponse.rateLimited && health <= 0); }, onGameOver: (playerWon) async { await showDialog( barrierDismissible: false, context: context, builder: ((context) { return AlertDialog( title: Text(playerWon ? 'You Won!' : 'You lost...'), actions: [ TextButton( onPressed: () async { Navigator.of(context).pop(); await supabase.removeChannel(_gameChannel!); _openLobbyDialog(); }, child: const Text('Back to Lobby'), ), ], ); }), ); }, ); // await for a frame so that the widget mounts await Future.delayed(Duration.zero); if (mounted) { _openLobbyDialog(); } } void _openLobbyDialog() { showDialog( context: context, barrierDismissible: false, builder: (context) { return _LobbyDialog( onGameStarted: (gameId) async { // await a frame to allow subscribing to a new channel in a realtime callback await Future.delayed(Duration.zero); setState(() {}); _game.startNewGame(); _gameChannel = supabase.channel(gameId, opts: const RealtimeChannelConfig(ack: true)); _gameChannel! .onBroadcast( event: 'game_state', callback: (payload, [_]) { final position = Vector2( payload['x'] as double, payload['y'] as double); final opponentHealth = payload['health'] as int; _game.updateOpponent( position: position, health: opponentHealth, ); if (opponentHealth <= 0) { if (!_game.isGameOver) { _game.isGameOver = true; _game.onGameOver(true); } } }, ) .subscribe(); }, ); }); } } class _LobbyDialog extends StatefulWidget { const _LobbyDialog({ required this.onGameStarted, }); final void Function(String gameId) onGameStarted; @override State<_LobbyDialog> createState() => _LobbyDialogState(); } class _LobbyDialogState extends State<_LobbyDialog> { List _userids = []; bool _loading = false; /// Unique identifier for each players to identify eachother in lobby final myUserId = const Uuid().v4(); late final RealtimeChannel _lobbyChannel; @override void initState() { super.initState(); _lobbyChannel = supabase.channel( 'lobby', opts: const RealtimeChannelConfig(self: true), ); _lobbyChannel .onPresenceSync((payload, [ref]) { // Update the lobby count final presenceStates = _lobbyChannel.presenceState(); setState(() { _userids = presenceStates .map((presenceState) => (presenceState.presences.first) .payload['user_id'] as String) .toList(); }); }) .onBroadcast( event: 'game_start', callback: (payload, [_]) { // Start the game if someone has started a game with you final participantIds = List.from(payload['participants']); if (participantIds.contains(myUserId)) { final gameId = payload['game_id'] as String; widget.onGameStarted(gameId); Navigator.of(context).pop(); } }) .subscribe( (status, _) async { if (status == RealtimeSubscribeStatus.subscribed) { await _lobbyChannel.track({'user_id': myUserId}); } }, ); } @override void dispose() { supabase.removeChannel(_lobbyChannel); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Lobby'), content: _loading ? const SizedBox( height: 100, child: Center(child: CircularProgressIndicator()), ) : Text('${_userids.length} users waiting'), actions: [ TextButton( onPressed: _userids.length < 2 ? null : () async { setState(() { _loading = true; }); final opponentId = _userids.firstWhere((userId) => userId != myUserId); final gameId = const Uuid().v4(); await _lobbyChannel.sendBroadcastMessage( event: 'game_start', payload: { 'participants': [ opponentId, myUserId, ], 'game_id': gameId, }, ); }, child: const Text('start'), ), ], ); } }