Files
supabase/examples/realtime/flutter-multiplayer-shooting-game/lib/main.dart
Stojan Dimitrovski 93ba2a312c docs: indicate publishable key instead of anon in many examples (#37411)
* docs: indicate publishable key instead of anon in many examples

* replace your-anon-key to string indicating publishable or anon

* fix your_...

* apply suggestion from @ChrisChinchilla

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>

* Update keys in code examples

* Prettier fix

* Update apps/docs/content/guides/functions/schedule-functions.mdx

---------

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>
2025-08-18 13:47:48 +02:00

258 lines
7.1 KiB
Dart

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: 'supabasePublishableKey',
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<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
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<void> _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<String> _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<String>.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'),
),
],
);
}
}