Skip to content

Commit

Permalink
Add basic multiplayer
Browse files Browse the repository at this point in the history
  • Loading branch information
CodeDoctorDE committed Dec 14, 2023
1 parent f9acec7 commit e11a418
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 70 deletions.
29 changes: 28 additions & 1 deletion app/lib/game/board.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class SpacedSpriteSheet {

class BoardGame extends FlameGame with KeyboardEvents, HasCollisionDetection {
final NetworkingService networkingService;
final BoardPlayer _player = BoardPlayer();
final BoardPlayer _player = BoardPlayer(true);
final Map<int, BoardPlayer> _players = <int, BoardPlayer>{};

final Vector2 _tileSize = Vector2.all(16);
Vector2 get tileSize => _tileSize;
Expand All @@ -47,6 +48,8 @@ class BoardGame extends FlameGame with KeyboardEvents, HasCollisionDetection {
required this.onEscape,
});

StreamSubscription? _networkerSub, _updateSub;

@override
Future<void> onLoad() async {
final component = await TiledComponent.load('map.tmx', _tileSize);
Expand Down Expand Up @@ -80,6 +83,30 @@ class BoardGame extends FlameGame with KeyboardEvents, HasCollisionDetection {
size: Vector2(object.width, object.height),
));
}
_networkerSub?.cancel();
_networkerSub = networkingService.stream.listen((event) {
_updateSub?.cancel();
_updateSub = event?.usersStream.listen((event) {
final removed = _players.keys.toSet()..removeAll(event.keys);
_removePlayers(removed);
final added = event.keys.toSet()..removeAll(_players.keys);
for (final id in added) {
final player = BoardPlayer(false);
world.add(player);
_players[id] = player;
}
});
if (event == null) {
_removePlayers(_players.keys.toSet());
}
});
}

void _removePlayers(Iterable<int> ids) {
for (final id in ids) {
world.remove(_players[id]!);
_players.remove(id);
}
}

@override
Expand Down
36 changes: 35 additions & 1 deletion app/lib/game/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flame/flame.dart';
import 'package:flame/text.dart';
import 'package:qeck/game/board.dart';
import 'package:qeck/game/wall.dart';
import 'package:qeck/models/message.dart';
import 'package:qeck/models/state.dart';

class _PreviousPlayerPositionComponent extends ReadOnlyPositionProvider {
Expand Down Expand Up @@ -50,7 +51,12 @@ extension RendererExtension on PlayerState {
class BoardPlayer
extends SpriteAnimationGroupComponent<(PlayerState, PlayerDirection)>
with HasGameRef<BoardGame>, CollisionCallbacks {
BoardPlayer() : super(current: (PlayerState.idle, PlayerDirection.front));
final bool isSelf;
NetworkingUser _user;

BoardPlayer(this.isSelf, [this._user = const NetworkingUser(name: '')])
: super(current: (PlayerState.idle, PlayerDirection.front));

late final TextComponent _text = TextComponent();
late final SpacedSpriteSheet _spriteSheet;

Expand Down Expand Up @@ -144,6 +150,7 @@ class BoardPlayer
next.y = 0;
}
position.add(next);
_sendUpdate();
if (state != PlayerState.sitting) {
if (velocity.x == 0 && velocity.y == 0) {
current = (PlayerState.idle, direction);
Expand Down Expand Up @@ -241,4 +248,31 @@ class BoardPlayer
_collidesYPos.remove(other);
_collidesYNeg.remove(other);
}

void onUpdate(NetworkingUser user) {
_user = user;
position = Vector2(user.position.$1, user.position.$2);
velocity = Vector2(user.velocity.$1, user.velocity.$2);
current = (user.state, direction);
}

void _sendUpdate() {
if (!isSelf) {
return;
}
if (position.x == _user.position.$1 &&
position.y == _user.position.$2 &&
state == _user.state &&
velocity.x == _user.velocity.$1 &&
velocity.y == _user.velocity.$2) {
return;
}
_user = NetworkingUser(
name: _user.name,
position: (position.x, position.y),
state: state,
velocity: (velocity.x, velocity.y),
);
game.networkingService.value?.sendUpdate(NetworkUpdateMessage(user: _user));
}
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@
"nativeTitleBar": "Native window title bar",
"board": "Board",
"home": "Home",
"disconnect": "Disconnect"
"disconnect": "Disconnect",
"url": "URL"
}
2 changes: 1 addition & 1 deletion app/lib/logic/connection/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io';

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:qeck/models/server.dart';
import 'package:qeck/logic/state.dart';
import 'package:qeck/services/network.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import 'client.dart';
Expand Down
2 changes: 0 additions & 2 deletions app/lib/models/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';

part 'server.freezed.dart';

const kDefaultPort = 10357;

@freezed
class GameServer with _$GameServer {
const factory GameServer.lan({
Expand Down
47 changes: 47 additions & 0 deletions app/lib/pages/board/connect.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:qeck/services/network.dart';

class ConnectGameDialog extends StatelessWidget {
final TextEditingController _urlController = TextEditingController();

ConnectGameDialog({super.key});

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).connect),
scrollable: true,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).url,
filled: true,
),
controller: _urlController,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
context.read<NetworkingService>().startClient(Uri.parse(
_urlController.text,
));
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context).create),
),
],
);
}
}
8 changes: 7 additions & 1 deletion app/lib/pages/board/create.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:qeck/models/server.dart';
import 'package:qeck/services/network.dart';

class CreateGameDialog extends StatelessWidget {
final TextEditingController _portController = TextEditingController();
Expand All @@ -19,8 +20,10 @@ class CreateGameDialog extends StatelessWidget {
decoration: InputDecoration(
labelText: AppLocalizations.of(context).port,
hintText: kDefaultPort.toString(),
filled: true,
),
controller: _portController,
keyboardType: TextInputType.number,
),
],
),
Expand All @@ -33,6 +36,9 @@ class CreateGameDialog extends StatelessWidget {
),
ElevatedButton(
onPressed: () {
context.read<NetworkingService>().startServer(
int.tryParse(_portController.text),
);
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context).create),
Expand Down
126 changes: 71 additions & 55 deletions app/lib/pages/board/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import 'package:phosphor_flutter/phosphor_flutter.dart';
import 'package:qeck/api/settings.dart';
import 'package:qeck/game/board.dart';
import 'package:qeck/main.dart';
import 'package:qeck/pages/board/connect.dart';
import 'package:qeck/pages/board/create.dart';
import 'package:qeck/services/messenger.dart';
import 'package:qeck/services/network.dart';
import 'package:qeck/widgets/window.dart';

Expand All @@ -18,6 +20,7 @@ class BoardPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
final networkingService = context.read<NetworkingService>();
return Scaffold(
key: _scaffoldKey,
appBar: const WindowTitleBar(
Expand All @@ -26,61 +29,74 @@ class BoardPage extends StatelessWidget {
),
drawer: Drawer(
child: Center(
child: ListView(
shrinkWrap: true,
children: [
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.arrowLeft),
title:
Text(MaterialLocalizations.of(context).backButtonTooltip),
onTap: () {
Navigator.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.house),
title: Text(AppLocalizations.of(context).home),
onTap: () {
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.gear),
title: Text(AppLocalizations.of(context).settings),
onTap: () {
Navigator.of(context).pop();
openSettings(context);
},
),
const Divider(),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.plugsConnected),
title: Text(AppLocalizations.of(context).connect),
onTap: () {
Navigator.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.plus),
title: Text(AppLocalizations.of(context).create),
onTap: () async {
await showDialog(
context: context,
builder: (_) => CreateGameDialog(),
);
if (context.mounted) Navigator.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.x),
title: Text(AppLocalizations.of(context).disconnect),
onTap: () {
Navigator.of(context).pop();
},
),
],
),
child: StreamBuilder<NetworkMessenger?>(
stream: networkingService.stream,
builder: (context, snapshot) {
return ListView(
shrinkWrap: true,
children: [
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.arrowLeft),
title: Text(
MaterialLocalizations.of(context).backButtonTooltip),
onTap: () {
Navigator.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.house),
title: Text(AppLocalizations.of(context).home),
onTap: () {
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.gear),
title: Text(AppLocalizations.of(context).settings),
onTap: () {
Navigator.of(context).pop();
openSettings(context);
},
),
const Divider(),
if (snapshot.hasData) ...[
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.x),
title: Text(AppLocalizations.of(context).disconnect),
onTap: () {
networkingService.closeNetworking();
Navigator.of(context).pop();
},
),
] else ...[
ListTile(
leading: const PhosphorIcon(
PhosphorIconsLight.plugsConnected),
title: Text(AppLocalizations.of(context).connect),
onTap: () async {
await showDialog(
context: context,
builder: (_) => ConnectGameDialog(),
);
if (context.mounted) Navigator.of(context).pop();
},
),
ListTile(
leading: const PhosphorIcon(PhosphorIconsLight.plus),
title: Text(AppLocalizations.of(context).create),
onTap: () async {
await showDialog(
context: context,
builder: (_) => CreateGameDialog(),
);
if (context.mounted) Navigator.of(context).pop();
},
),
],
],
);
}),
),
),
body: GameWidget(
Expand Down
2 changes: 1 addition & 1 deletion app/lib/pages/home/connect.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:phosphor_flutter/phosphor_flutter.dart';
import 'package:qeck/models/server.dart';
import 'package:qeck/services/network.dart';

class ConnectGameDialog extends StatelessWidget {
const ConnectGameDialog({super.key});
Expand Down
2 changes: 1 addition & 1 deletion app/lib/services/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:qeck/services/messenger.dart';

mixin GenericClientMessenger on NetworkMessenger {
void sendUpdate(NetworkUpdateMessage event) {
rpc.sendMessage(RpcRequest(kNetworkerConnectionIdAny, 'move', event));
rpc.sendMessage(RpcRequest(kNetworkerConnectionIdAny, 'update', event));
}
}

Expand Down
Loading

0 comments on commit e11a418

Please sign in to comment.