Add Video Calling to Chess With Flutter, Dyte, and GetX
Published: 2024-01-09 19:26:04

Discover the fusion of tradition and innovation as we explore the world of chess, reimagined through Dyte video calling in the Flutter framework. Break the distance barriers and get your tactical hat on because it's getting real, in real-time, like never before.

In this blog, we'll (grand)master adding video calling to chess, all within the flexible Flutter framework. Prepare to take your strategic battles beyond the board, embracing virtual gameplay and personal connections.

We'll capture your pieces and walk you through every step of this tutorial. You can expect clear, easy-to-follow instructions, short code examples, and valuable tips. We aim to give you the confidence and skills to create your unique Flutter app. Whether you're an experienced coder or starting out doesn't matter. This guide is designed for all sorts of players.

So, let's jump right in and harness the capabilities of Flutter and [Dyte](https://dyte.io/) to give a fresh perspective to the timeless board game. Our journey will infuse joy and connection into players all around the globe. 
## **Requirements**

You'll need critical elements to create your multiplayer chess game using the Dyte video calling API in Flutter. Firstly, make sure you have Flutter installed on your system. You can follow your operating system's [official Flutter installation guide](https://docs.flutter.dev/) to set it up successfully.

To help you hit the ground running, we've prepared an initial project setup with a pre-built user interface (UI) for the chess game. You can access the project code and user interface files [here](https://github.com/dyte-io/collaborative-chess/tree/initial). This initial setup provides a solid foundation to build upon, saving you time and effort in creating the game's basic structure.

Next, you must visit the official [Dyte website](https://dyte.io/) and navigate to the signup page to create an account and explore the various features and functionalities Dyte offers.

You'll then need to incorporate the Dyte video calling SDK into your Flutter project. The Dyte plugin will seamlessly integrate video calling functionality, enabling real-time communication between players.

You can add the `dyte_core` plugin in the `pubspec.yaml` file using the following command.

```bash
flutter pub add dyte_core
```

To add chess functionality, we are using the [`bishop`](https://pub.dev/packages/bishop), [`squares`](https://pub.dev/packages/squares), and [`square_bishop`](https://pub.dev/packages/square_bishop) plugins.

We are going to use `Getx` for state management. GetX is a powerful state management solution for Flutter that offers simplicity, performance, and scalability. With GetX, you can easily manage the state of your chess game and handle navigation, dependencies, and more.

## **Basic chess concepts**

Before we add video calling to our game of wits, we need to understand some chess concepts before writing code.

**1\. Chessboard and Squares**: The chessboard consists of 64 squares arranged in an 8x8 grid. Each square alternates between light and dark colors.

**2\. Pieces and their movement**:

- **King:** Moves one square in any direction.
- **Queen:** Moves diagonally, horizontally, or vertically any number of squares.
- **Rook:** Moves horizontally or vertically any number of squares.
- **Bishop:** Moves diagonally any number of squares.
- **Knight:** Moves in an L-shape; two squares in one direction and one square perpendicular to it.
- **Pawn:** Moves forward one square but captures diagonally.

**3\. Checkmate**: If a player's king is in check and has no legal moves to escape, it's checkmate, and the game ends.

**4\. FEN**: In (Forsyth-Edwards Notation) FEN notation, each chessboard square is represented by a character, with uppercase letters denoting white pieces and lowercase letters denoting black pieces. The characters used are as follows:

- **K/k:** King
- **Q/q:** Queen
- **R/r:** Rook
- **N/n:** Knight
- **B/b:** Bishop
- **P/p:** Pawn
- **1-8:** Empty squares (number of empty squares in a row)

FEN also includes information about the player to move, castling rights, en passant target square, halfmove clock (the number of halfmoves since the last pawn move or capture), and fullmove number (the number of the current move).

FEN notation is commonly used in various chess-related applications, such as recording and sharing positions, analyzing games, and setting up positions for practice or analysis. It provides a standardized way to communicate chess positions regardless of language or platform.

`**Note:** For rendering pieces on board we will require to generate FEN after every move.`

## **Building Game**

Now that you understand the chess terminology, let's move in the right direction and navigate to the lib folder. There, you will discover four files playing crucial roles to bring your game to life. Each file contributes uniquely to the seamless functionality and immersive experience you're crafting. Let's explore the significance of these files in shaping your game's architecture and interaction.

We have the `main.dart` file starting at the beginning. This Dart file serves as the entry point for your application. It's the first thing that gets executed when your app launches. In this file, you'll typically find the setup for your app's main structure, including the landing page. The landing page is what users initially see when they open your app. It's the gateway that sets the tone for the entire user experience (UX), providing a decisive first impression of what your app offers.

The second file is named `game_screen.dart`. This Dart file takes center stage in our chess game's UI aspect. Here, you'll find the design and layout components that visually represent the chess game to the user. This file shows how the game appears on the screen from the chessboard to the player interface. It's the bridge between the underlying game logic and what the user sees, ensuring a visually appealing and user-friendly experience for your players.

Moving on to the third file, called `room_state.dart`. This Dart file is the home for all the essential logic related to Dyte video calling and the state of the chess game. Here, you'll find the backbone of your application's functionality, connecting the dots between video communication and the intricate workings of chess. This file is a pivotal hub where the magic of video interaction and the strategy of chess converge to create a seamless and engaging UX.

The fourth is the `http_request.dart` file. The `HttpRequest` class in this Dart file is designed to manage API requests. It's responsible for handling tasks such as creating rooms and adding participants to these rooms. This integral class plays a crucial role in facilitating interactions with the backend of your application.

To get your project up and running, open your terminal and execute the command `flutter pub get`. This command is essential as it fetches and loads all the required plugins and dependencies for your project. It ensures your project has everything needed to function correctly so you can seamlessly build your engaging multiplayer chess game.

While the game might not be fully functional yet, we're committed to transforming it into a fully operational masterpiece by the time you complete this blog.

Let's focus on two essential features: room creation and participant addition. These functionalities serve as the cornerstone of our game, enabling us to establish fresh gaming rooms and effortlessly invite friends to join in on the fun. Through room creation, we can set the stage for exciting matches. At the same time, the participant addition feature lets us share these rooms with our buddies, fostering a sense of camaraderie and friendly competition.

## **API Request**

Let's navigate to the `http_request.dart` file. Within this file, you'll discover the code responsible for room creation and participant addition through API requests. It's crucial to note that you'll need to include your organization ID and API key from the Dyte dashboard in this process. These credentials are essential for establishing a secure and authenticated connection with the Dyte services.
### **Code snippet for room creation**

```dart
static Future<String?> createMeeting() async {
 String id = DateTime.now().millisecondsSinceEpoch.toString();
 try {
  Response response = await post(Uri.parse("$_baseUrl/meetings"),
   headers: {'Authorization': _token,"Content-Type": "application/json"},
   body: json.encode({"title":"chess-$id"})
  );
  if (response.statusCode == 201) {
    Map map = jsonDecode(response.body);
    return map["data"]["id"];
   }
  } catch (e) {
  Get.showSnackbar(GetSnackBar(
   title: e.toString(),
  ));
 }
 return null;
}
```

### **Code snippet for add participant**

```dart
static Future<String?> addParticipant(String name, String roomId) async {
 String id = DateTime.now().millisecondsSinceEpoch.toString();
 try {
  Response response = await post(
  Uri.parse("$_baseUrl/meetings/$roomId/participants"),
  headers: {'Authorization': _token,"Content-Type": "application/json"},
  body: json.encode({"name":name,
           "Preset_name":"group_call_host",
           "custom_participant_id":"player-$id"
})
  );
 if (response.statusCode == 201) {
  Map map = jsonDecode(response.body);
  return map["data"]["token"];
 }
 } catch (e) {
  Get.showSnackbar(GetSnackBar(
  title: e.toString(),
 ));
 }
 return null;
}
```

For a more comprehensive understanding of the API requests and responses, please refer to the [Dyte API guide](https://docs.dyte.io/api#/). This guide provides detailed insights into working with the Dyte API for various operations.

You can explore [Dyte API: Create a meeting](https://docs.dyte.io/api/#/operations/create_meeting) for room creation and [Dyte API: Add a participant](https://docs.dyte.io/api/#/operations/add_participant) for, well, adding participants.

These resources will provide in-depth information on the API endpoints, parameters, and expected responses, enabling you to seamlessly integrate these functionalities into your Flutter application for a fully-fledged multiplayer chess experience.

Now, let’s open the `main.dart` file for connecting room creation and adding participants to the UI.

Our initial focus is on establishing a room for the game. You'll encounter two buttons in the code and UI. One button lets you join an existing game, while the other enables you to create a new one.

When the "Create Game" button is pressed, we execute the following code to create a room:

```dart
String? roomId = await HttpRequest.createMeeting();
```

This code snippet triggers the creation of a room where the chess game will unfold. It's the starting point for crafting the environment where players engage in strategic battles on the virtual chessboard.

After successfully creating the room, we get the room ID. We will then pass this room ID to the `addParticipant` function, which will facilitate the token acquisition. This token is a vital element for the video calling functionality. Here's how you achieve this:

```dart
String? token = await HttpRequest.addParticipant(roomId);
```

We will transmit this acquired token to the `RoomStateNotifier`.

When joining an existing game, our approach involves prompting the user for a room ID. This room ID will then be utilized in the `addParticipant` function to obtain the necessary token. Once secured, this token is subsequently passed to the `RoomStateNotifier` for effective integration into the ongoing gaming experience.

Let's open the `room_state.dart` file to move forward. Once inside, you'll find a collection of code snippets pivotal to our progress. These snippets hold the key to implementing the Dyte video calling functionality and managing the state of our chess game. So, let's dive in and dissect these snippets to uncover the magic that will bring our multiplayer chess game to life!

As you begin exploring, you'll notice a series of variable initializations. These are accompanied by insightful comments that shed light on their intended roles and how they contribute to the program's functionality.

## **Initiating the Dyte video meeting**

To keep track of updates from the Dyte plugin, we must incorporate listeners into the `RoomStateNotifier` class. Achieving this involves implementing the following code snippets:

```dart
class RoomStateNotifier extends GetxController
implements
DyteMeetingRoomEventsListener,
DyteParticipantEventsListener,
DyteChatEventsListener {
...
```

By incorporating these listeners, we establish a mechanism where our application stays informed about any alterations or updates occurring in the room state through the Dyte plugin. This real-time awareness is crucial to ensure our game stays in sync with the dynamic changes within the video communication framework.

Once you've integrated the listener into the `RoomStateNotifier` class, you might encounter errors that signal the absence of specific override methods. You can use the quick fix feature available in your integrated development environment (IDE) to tackle these errors. This tool can assist you in swiftly resolving the issues by generating the required override methods, ensuring your code remains error-free and fully functional.

The Dyte plugin equips us with various essential override methods that enable us to effectively capture updates concerning our calls. In the context of our application, we will be leveraging the following override methods to enhance our UX:

**1\. onMeetingInitCompleted()**:  
This method triggers when the initialization of a meeting concludes. It furnishes pertinent details about the meeting, facilitating informed handling of meeting-related information.

**2\. onMeetingRoomJoinCompleted()**:  
This method springs into action when a user successfully enters a meeting room. It manages any imperative actions or updates required upon room entry.

**3\. onMeetingRoomLeaveCompleted()**:  
This method is the moment a user departs from a meeting room, offering the opportunity to execute any essential cleanup or follow-up procedures.

**4\. onParticipantJoin(DyteJoinedMeetingParticipant participant)**:  
This method is triggered whenever a new participant joins the meeting, delivering comprehensive information about the recently joined participant.

**5\. onParticipantLeave(DyteJoinedMeetingParticipant participant)**:  
This method comes into play as a participant exits the meeting, providing vital insights into the departing participant's details.

**6\. onNewChatMessage(DyteChatMessage message)**:  
If a fresh chat message arrives, this method is invoked. It empowers us to process and manage incoming messages effectively.

Harnessing these override methods allows us to capture key events within our video calls and enhance our application's overall interactivity and engagement.

Let's attach the listener to the `RoomStateNotifier` class to start receiving updates from the Dyte API. Furthermore, we initiate the video call by invoking the init(meetingInfo) method provided by the `dyteClient` instance. This action triggers the process of starting the video call, enabling users to join the meeting and engage in real-time communication with other participants. The `dyteClient` variable is of type `DyteMobileClient`, which serves as a manager for interacting with a Dyte server.

Here is the code snippet for adding the listener and initiating the meeting:

```dart
RoomStateNotifier(String name, String token, String meetingId) {
username.value = name;
roomId.value = meetingId;
dyteClient.value.addMeetingRoomEventsListener(this);
dyteClient.value.addParticipantEventsListener(this);
dyteClient.value.addChatEventsListener(this);
final meetingInfo = DyteMeetingInfoV2(
authToken: token,
enableAudio: false,
enableVideo: true);
dyteClient.value.init(meetingInfo);
}
```

Once the meeting is successfully initialized, a callback will be triggered in the `onMeetingInitCompleted()` function. This callback indicates that the meeting has been set up properly and is ready for joining the room.

## **Joining the Dyte meeting**

To join the call and configure the username within the room, the following code must be implemented within the `onMeetingInitCompleted()` function.

```dart
@override
void onMeetingInitCompleted() {
dyteClient.value.localUser.setDisplayName(username.value);
dyteClient.value.joinRoom();
}
```

When the room join process is completed, the `onMeetingRoomJoinCompleted()` callback function is triggered. In this callback, we set the `roomJoin` variable to true, indicating that the local user has successfully joined the room.

```dart
@override
void onMeetingRoomJoinCompleted() {
roomJoin.value = true;
}
```

## **Leaving the Dyte meeting**

Similarly, in the `onMeetingRoomLeaveCompleted()` callback, we handle the removal of listeners from this class. This ensures that the necessary clean-up tasks are performed when leaving the meeting, preventing any potential memory leaks or unwanted behaviors. By implementing this callback, we can adequately manage the class's lifecycle and maintain a streamlined and efficient application flow.

```dart
@override
void onMeetingRoomLeaveCompleted() {
roomJoin.value = false;
dyteClient.value.removeMeetingRoomEventsListener(this);
dyteClient.value.removeParticipantEventsListener(this);
dyteClient.value.removeChatEventsListener(this);
Get.delete<RoomStateNotifier>();
Get.back();
}
```

## **Remote peer(s) join the Dyte meeting**

When an opponent joins the room, the `onParticipantJoin()` function is triggered, providing us with a callback. At this point, we can proceed to initiate the game. However, it is necessary to assign color to local and remote peers before starting.

To accomplish this, we will call the existing `assignColour()` function available within the same file. By invoking this function, we can ensure that each player is assigned their respective symbols, setting the stage for an engaging and fair gameplay experience.

```dart
@override
void onParticipantJoin(DyteJoinedMeetingParticipant participant) {
   if (participant.userId != dyteClient.value.localUser.userId) { //not a local user
       remotePeer.value = participant;
       assignColour();
   }
}
```

We use their unique user IDs to assign local and remote peers colors. We can ensure consistent symbol assignment across the room by sorting these IDs. In this implementation, we designate the symbol "White" to the user at the 0 index and "Black" to the other peer. As user IDs are unique and consistent within the room, this approach guarantees that each player receives their designated symbol, enhancing clarity and fairness during gameplay.

If a remote peer leaves the room during an ongoing game, we must ensure that the local peer also leaves to maintain a synchronized and consistent experience. We will use the `onParticipantLeave` override function to handle this scenario.

When a remote peer leaves the room, we will display a dialog to the user, giving them a clear choice to leave the room. The dialog will have the `barrierDismissible` parameter set to false, meaning the user cannot dismiss the dialog by tapping outside.

Once the user clicks the "Leave" button within the dialog, a method will be invoked to trigger the local peer's departure from the room. This ensures that both participants gracefully exit the room, maintaining the integrity of the game session.

```dart
dyteClient.value.leaveRoom();
```

Here is the code for onParticipantLeave:

```dart
@override
void onParticipantLeave(DyteJoinedMeetingParticipant participant) {
  if (participant.userId != dyteClient.value.localUser.userId &&
    remotePeer.value != null) {
      remotePeer.value = null;
      Get.defaultDialog(
        barrierDismissible: false,
        onWillPop: () async {
          return false;
        },
        title: "Opponent Leave this game.",
        textConfirm: "Leave",
        middleText: "",
        confirmTextColor: Colors.white,
        onConfirm: () {
          dyteClient.value.leaveRoom();
          Get.back();
        });
      }
}
```

makes their move, we are harnessing the chat feature provided by the Dyte plugin. Within this strategy, we employ the `makeSquareMove(move)` function from the bishop library, a built-in tool. This function possesses a boolean return type; if the move is invalid, it yields false. Otherwise, it is true. Upon confirming a valid move, we update the ChessBoard's state by fetching the latest `squaresState` via the `bishop` library.

To synchronize the remote user's experience, we transmit the most recent FEN (Forsyth-Edwards Notation) obtained from the bishop library through the Dyte plugin's chat functionality. This data is seamlessly sent to the remote peer's device.

```dart
onUserMove(Move move, {bool? isDrop, bool? isPremove}) {
 if (game.value.makeSquaresMove(move)) {
   state.value = game.value.squaresState(playerColor.value);
   sendMessage(game.value.fen);
   localUserTurn.value = false;
 }else{
   Get.snackbar("Invalid move","",snackPosition: SnackPosition.BOTTOM);
}
 if (game.value.checkmate) {
  Get.defaultDialog(
   barrierDismissible: false,
   title: "You won the game",
   textConfirm: "Leave",
   middleText: "",
   confirmTextColor: Colors.white,
   onConfirm: () {
    dyteClient.value.leaveRoom();
    Get.back();
   });
 }
}
```

## **Using Dyte’s chat component**

To ensure real-time updates of the game whenever a user makes their move, we are harnessing the chat feature provided by the Dyte plugin. Within this strategy, we employ the `makeSquareMove(move)` function from the bishop library, a built-in tool. This function possesses a boolean return type; if the move is invalid, it yields false, otherwise, it is true. Upon confirming a valid move, we update the ChessBoard's state by fetching the latest `squaresState` via the `bishop` library.  
  
To synchronize the remote user's experience, we transmit the most recent FEN obtained from the bishop library through the Dyte plugin's chat functionality. This data is seamlessly sent to the remote peer's device.

```dart
onUserMove(Move move, {bool? isDrop, bool? isPremove}) {
 if (game.value.makeSquaresMove(move)) {
   state.value = game.value.squaresState(playerColor.value);
   sendMessage(game.value.fen);
   localUserTurn.value = false;
 }else{
   Get.snackbar("Invalid move","",snackPosition: SnackPosition.BOTTOM);
}
 if (game.value.checkmate) {
  Get.defaultDialog(
   barrierDismissible: false,
   title: "You won the game",
   textConfirm: "Leave",
   middleText: "",
   confirmTextColor: Colors.white,
   onConfirm: () {
    dyteClient.value.leaveRoom();
    Get.back();
   });
 }
}


sendMessage(String message) {
dyteClient.value.chat.sendTextMessage(message);
}
```

Lastly, we make use of the `onNewChatMessage` override function. This function lets us receive the chess FEN in text format through the chat functionality. We will now load the `bishop` variable (game) with a new FEN and update the `chessBoard` state using the `squareState` function, which enables us to update the UI accordingly.

By implementing this functionality, we ensure that any updates made by the remote peer are received and reflected in real-time on the local user's UI.

```dart
@override
void onNewChatMessage(DyteChatMessage message) {
 if (message.userId == dyteClient.value.localUser.userId) {
  return;
 }
 DyteTextMessage textMessage = message as DyteTextMessage;
 final fen = textMessage.message;
 game.value.loadFen(fen);
 state.value = game.value.squaresState(playerColor.value);
 if (game.value.checkmate) {
  Get.defaultDialog(
   barrierDismissible: false,
   title: "You lose the game",
   textConfirm: "Leave",
   middleText: "",
   confirmTextColor: Colors.white,
   onConfirm: () {
    dyteClient.value.leaveRoom();
    Get.back();
   });
  }
 localUserTurn.value = true;
}
```

## **Managing audio/video of participants**

We will utilize two additional functions — `toggleVideo()` and `toggleAudio()` — to manage the local peer's video and audio within the room. These functions allow us to dynamically control the starting and stopping of the local peer's video and audio streams during the video call session.

By invoking `toggleVideo()`, we can initiate or pause the local peer's video transmission, while `toggleAudio()` enables us to start or mute the local peer's audio feed. These functionalities provide flexibility and control to the user, allowing them to manage their video and audio presence according to their preferences and requirements within the room.

```dart
toogleVideo() {
     if (isVideoOn.value) {
 dyteClient.value.localUser.disableVideo();
     } else {
       dyteClient.value.localUser.enableVideo();
     }
       isVideoOn.toggle();
}
toogleAudio() {
     if (isAudioOn.value) {
        dyteClient.value.localUser.disableAudio();
     } else {
        dyteClient.value.localUser.enableAudio();
     }
        isAudioOn.toggle();
}
```

## **Setting up the chess game**

Now, let's navigate to the `gameScreen.dart` file. This file contains the UI code for our game and the video call controls UI. However, we must add some code to display the video tiles correctly. This code will handle the rendering and layout of the video tiles, ensuring they are displayed correctly on the game screen. By incorporating this code, we can seamlessly integrate the game UI and the video call interface, providing users with a comprehensive and immersive experience.

Search for the comment "// Dyte Meeting Video View" in the `gameScreen.dart` file. Within the corresponding section, we will proceed to check whether the user has joined the meeting or not by using the following code:

```dart
if (roomStateNotifier.roomJoin.value)
```

By implementing this code snippet, we can differentiate between the scenarios where the user has successfully joined the meeting and where they have not. This conditional logic allows us to handle each situation appropriately, ensuring a smooth and coherent user experience throughout the game.

If the `roomJoin` variable is true, it indicates that the user has successfully joined the meeting. We can use the `VideoView` widget the Dyte video calling SDK provides to render the user's video tile.

This widget lets us display the video feed for local and remote peers within the game interface. By incorporating the `VideoView` widget, we can coherently integrate the video streams from the local and remote participants, enriching the interactive experience and fostering real-time communication between the players.

```dart
// Local user video tile and name
Row(
 children: [
  const Spacer(),
  Obx(
  () => Text(
  roomStateNotifier.roomJoin.value == false
  ? ""
  : roomStateNotifier.username.value,
  style: const TextStyle(
  color: Colors.black, fontWeight: FontWeight.bold),
  ),
 ),
 const SizedBox(
  width: 10,
 ),
 Obx(
 () => Container(
  height: 100,
  width: 100,
  decoration: BoxDecoration(
  borderRadius: BorderRadius.circular(17),
  boxShadow: [
   if (roomStateNotifier.localUserTurn.value) ...[
   const BoxShadow(
    color: Colors.red,
    spreadRadius: 4,
    blurRadius: 10,
   ),
  const BoxShadow(
   color: Colors.red,
   spreadRadius: -4,
   blurRadius: 5,
  )]
  ],
 ),
 margin: const EdgeInsets.only(right: 10),
 child: ClipRRect(
 borderRadius: BorderRadius.circular(17),
 // Dyte Meeting Video View
 child: roomStateNotifier.roomJoin.value == false
 ? const SizedBox()
 : const VideoView(
  isSelfParticipant: true,
  ),
 ),
 ),
 )
```

To render the local peer's video feed, we must pass the `isSelfParticipant` parameter as true, as shown in the code snippet above. This informs the `VideoView` widget that the video is rendered from the local participant.

On the other hand, for rendering the remote peer's video feed, we can utilize the following code:

```dart
roomStateNotifier.remotePeer.value == null
// Room Id
? SizedBox(
 child: Center(
  child: Row(
  mainAxisSize: MainAxisSize.min,
  children: [
   Text("Room Id: ${roomStateNotifier.roomId.value.substring(0,10)}...",),
   IconButton(onPressed: () async{
   //`Clipboard.setData` copy string to clipboard
   await Clipboard.setData(ClipboardData(text: roomStateNotifier.roomId.value));
   }, icon: const Icon(Icons.copy)),
  ]),
 ))
// Remote user video tile and name
: Row(
   mainAxisSize: MainAxisSize.min,
   children: [
    Container(
     height: 100,
     width: 100,
     padding: const EdgeInsets.only(left: 10),
     child: ClipRRect(
      borderRadius: BorderRadius.circular(17),
      child: VideoView(
       meetingParticipant:
       roomStateNotifier.remotePeer.value,
      ),
     ),
    ),
 const SizedBox(
 width: 10,
 ),
 Text(
  roomStateNotifier.remotePeer.value!.name,
  style: const TextStyle(
   color: Colors.black, fontWeight: FontWeight.bold),
  ),
 const Spacer()
 ])
```

To meet the specific requirement, it's essential to include a `meetingParticipant` parameter, assigning it the value of `remotePeer`. This value is stored within the `onParticipantJoin` override function.

By incorporating the `meetingParticipant` parameter and accessing the stored `remotePeer` value, we guarantee the accurate rendering of the video stream for the respective participant. This synchronization ensures that the appropriate participant's video feed is correctly displayed within the `VideoView` widget.

## **Conclusion**

Congratulations! If you've reached here, it's checkmate for this blog. You've successfully added live video calling to chess and made it multiplayer using Flutter, Dyte video calling SDK, and GetX for expert state management. By integrating these tools, you're empowered to construct a gaming encounter that's not only immersive but also teeming with interactivity.

[Here's](https://github.com/dyte-io/collaborative-chess) the source code for the full project.

By harnessing the versatile capabilities of Flutter, you've fashioned a visually captivating and responsive UI across different platforms. The Dyte video calling plugin has bridged geographical gaps, enabling players to engage in real-time bouts of strategic prowess, regardless of location. Lastly, the seamless integration of GetX ensures the game's state remains effortlessly manageable, culminating in an immersive gaming escapade for your users.

Yet, this journey into game development is but the opening chapter. The canvas for expanding and enriching your chess prowess is boundless. Think about infusing features like game statistics to track progress, player rankings to heighten competition, or even exploring novel game modes to keep users enthralled and amused. You're bound to gain tempo.

It's time we stop the clock and offer you a handshake. Until next time.

__Get better insights on leveraging Dyte’s technology and discover how it can revolutionize your app’s communication capabilities with its [SDKs](https://dyte.io/video-sdk).__