Adding Live Video Calling to Tic-Tac-Toe With Flutter and Dyte
Published: 2024-01-09 19:32:30

## Introduction

Welcome to an exciting journey into the world of interactive gaming! In this blog post, we'll explore how to develop a captivating multiplayer Tic-Tac-Toe game using Flutter, a powerful and versatile framework for building cross-platform applications. But that's not all — we'll take it a step further by integrating the [Dyte video SDK](https://dyte.io/video-sdk), allowing users to connect with their friends and engage in real-time battles of strategy and wit.

Engage in the timeless classic of Tic-Tac-Toe, reimagined with a modern twist. With Flutter's flexibility and Dyte's seamless video calling capabilities, you can create an immersive gaming experience that brings people together, no matter the distance. So, get ready to delve into the exciting world of game development as we guide you through building a multiplayer Tic-Tac-Toe game with the added thrill of live video communication.

Throughout this tutorial, we'll provide step-by-step instructions, code snippets, and valuable insights to empower you in crafting your Flutter application. Whether you're a seasoned developer or just starting your programming journey, this guide will equip you with the knowledge and tools to create an engaging multiplayer game that captivates and entertains your users.

So, let's dive in and discover how you can leverage the power of Flutter and the Dyte video SDK to breathe new life into the classic game of Tic-Tac-Toe, bringing joy and connection to players worldwide.

## Prerequisites

You'll need a few key elements to start creating your multiplayer Tic-Tac-Toe game using the [Dyte video SDK](https://dyte.io/flutter-video-sdk) in Flutter. Firstly, make sure you have Flutter installed on your system. You can follow your operating system's official Flutter installation guide to set it up successfully.

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

Now visit the official Dyte website at [dyte.io](https://dyte.io/) and navigate to the signup page to create an account and explore the various features and functionalities offered by Dyte.

Next, you must incorporate the Dyte video SDK into your [Flutter](https://dyte.io/flutter-video-sdk) project. The Dyte SDK allows seamless integration of video calling functionality, enabling real-time communication between players.

Using the following command, you can add the `dyte_core` plugin in the `pubspec.yaml` file.

`flutter pub add dyte_core`

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 Tic-Tac-Toe game and handle navigation, dependencies, and more.

In the initial screen, you will find three files.

- The first one is the `main.dart` file, which contains the landing page.
- The second file is `game_screen.dart` — this will hold the UI part for our Tic-Tac-Toe game.
- The third file is `room_state.dart`. This dart file holds all the logic for dyte video calling and the Tic-Tac-Toe game state.

Open project is terminal and runs the `flutter pub get` command to load all the required plugins.

Currently, the game is not in a proper working state, but we will make it happen by the end of this blog.

To proceed, let's open the `room_state.dart` file. Inside this file, you will find a set of code snippets.

Initially, you will come across variable initializations accompanied by comments that provide insights into their purpose and usage.

To receive updates from the [Dyte video SDK](https://dyte.io/flutter-video-sdk), we need to add some listeners to the RoomStateNotifier class. This can be accomplished by implementing the following code:

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

By registering these listeners, we ensure that our application is notified of any changes or updates in the room state from the [Dyte video SDK](https://dyte.io/video-sdk).

Upon adding the listener to the `RoomStateNotifier` class, you may encounter errors indicating that specific override methods are missing. To resolve these errors, you can utilize the quick-fix functionality provided by your IDE.

## Initiating the Dyte meeting

Dyte [video SDK](https://dyte.io/flutter-video-sdk) offers override methods that allow us to receive updates on our call. In this application, we will be utilizing the following override methods:

- `onMeetingInitCompleted()`: This method is called when the meeting initialization is completed, providing us with relevant information about the meeting.
- `onMeetingRoomJoinCompleted()`: When the user successfully joins a meeting room, this method is invoked, allowing us to handle any necessary actions or updates.
- `onMeetingRoomLeaveCompleted()`: Invoked when the user leaves the meeting room. This method enables us to perform any required cleanup or follow-up tasks.
- `onParticipantJoin(DyteJoinedMeetingParticipant participant)`: This method is called when a new participant joins the meeting, providing information about the joined participant.
- `onParticipantLeave(DyteJoinedMeetingParticipant participant)`: When a participant leaves the meeting, this method is triggered, supplying details about the departing participant.
- `onNewChatMessage(DyteChatMessage message)`: Whenever a new chat message is received, this method is invoked, allowing us to process and handle the message accordingly.

First, attach the listener to the `RoomStateNotifier` class to start receiving updates from the [Dyte video SDK](https://dyte.io/flutter-video-sdk). Furthermore, we initiate the video call by invoking the `init(meetingInfo)` method provided by the `dyteClient` instance. This action triggers 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](https://docs.dyte.io/api?v=v2&#/operations/add_participant) and [initiating a Dyte meeting](https://docs.dyte.io/api?v=v2&#/operations/create_meeting).

```dart
RoomStateNotifier(String name) {
username = name;
dyteClient.value.addMeetingRoomEventsListener(this);
dyteClient.value.addParticipantEventsListener(this);
dyteClient.value.addChatEventsListener(this);
final meetingInfo = DyteMeetingInfoV2(
authToken:'---TOKEN HERE—--',
enableAudio: false,
enableVideo: true);
dyteClient.value.init(meetingInfo);
}
```

## Joining the Dyte meeting

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

The following code needs to be implemented within the `onMeetingInitCompleted()` function to facilitate joining the call and configuring the username within the room.

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

The `onMeetingRoomJoinCompleted()` callback function is triggered when the room join process is completed. 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;
}
```

Similarly, in the `onMeetingRoomLeaveCompleted()` callback, we handle the removal of listeners from this class. This ensures that the necessary cleanup tasks are performed when leaving the Dyte meeting, preventing any potential memory leaks or unwanted behaviors. By implementing this callback, we can properly manage the lifecycle of the class 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();
}
```

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, before starting, it is necessary to assign symbols for local and remote peers. We will call the existing `assignSymbol()` function available within the same file to accomplish this. 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;
       assignSymbol();
   }
}
```

We use their unique user IDs to assign local and remote peers symbols. We can ensure consistent symbol assignment across the room by sorting these IDs. In this implementation, we designate the character "O" to the user at the 0 index and "X" 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.

## Leaving the Dyte meeting

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. To handle this scenario, we will utilize the `onParticipantLeave` override function.

When a remote peer leaves the room, we will display a dialog to the user, providing them with 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.

By implementing this flow, we provide a seamless and intuitive experience for users, allowing them to easily handle situations where a remote peer leaves the game unexpectedly.

```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 Left this game.",
        textConfirm: "Leave",
        middleText: "",
        confirmTextColor: Colors.white,
        onConfirm: () {
          dyteClient.value.leaveRoom();
          Get.back();
        });
      }
}
```

## Using Dyte’s chat component

To facilitate real-time updates of the game whenever the user takes their turn, we will leverage the chat feature provided by the [Dyte video SDK](https://dyte.io/flutter-video-sdk). In this approach, we will send the `displayExOh` array as text through the chat functionality, ensuring it is transmitted to the remote peer's device. Utilizing the Dyte SDK's [chat feature](https://dyte.io/chat-sdk), we establish a seamless communication channel between the two peers, enabling the synchronization of game progress and retrieving the updated `displayExOh` array on the remote peer's side. This mechanism ensures a consistent and interactive gaming experience, allowing both players to sync throughout the game.

```dart
sendMessage() {
dyteClient.value.chat.sendTextMessage(displayExOh.toString());
}
```

Lastly, we make use of the `onNewChatMessage` override function. This function lets us receive the `displayExOh` array in text format through the chat functionality. We then convert this text to the original array format, enabling us to update the user interface (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;
List<String> temp = textMessage.message
.replaceFirst("[", "")
.replaceFirst("]", "")
.split(", ");
displayExOh.value = temp;
localUserTurn.toggle();
boxCount.value = displayExOh.where((p0) => p0!="").length;
checkWinner();
}
```

## Managing audio/video of participants

We will be utilizing 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 Tic-Tac-Toe 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, to display the video tiles correctly, we need to add some additional code. 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.

Let's 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 Dyte 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 Dyte 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 Dyte meeting. To render the user's video tile, we can utilize the `VideoView` widget provided by the [Dyte video SDK](https://dyte.io/video-sdk). This widget lets us display the video feed for local and remote peers within the game interface. By incorporating the `VideoView` widget, we can seamlessly 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
Column(
   mainAxisAlignment: MainAxisAlignment.center,
   children: [
      SizedBox(
        height: 100,
        width: 100,
        child: ClipRRect(
           borderRadius: BorderRadius.circular(17),
           child: const VideoView(
              isSelfParticipant: true,
           ),
        ),
     ),
    Text(roomStateNotifier.dyteClient.value.localUser.name,style: const    TextStyle(color: Colors.white),)
  ],
),
```

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 being rendered is from the local participant.

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

```dart
if (roomStateNotifier.remotePeer.value != null)
// Remote user video tile and name
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
 height: 100,
 width: 100,
  child: ClipRRect(
   borderRadius: BorderRadius.circular(17),
   child: VideoView(
    meetingParticipant:
    roomStateNotifier.remotePeer.value,
    ),
   ),
  ),
    Text(roomStateNotifier.remotePeer.value!.name,style: const TextStyle(color:        Colors.white),)
   ],
),
```

To fulfill the requirement, we need to include a `meetingParticipant` parameter with the value of `remotePeer`, which we have stored in the `onParticipantJoin` override function.

Utilizing the `meetingParticipant` parameter and accessing the `remotePeer` value ensures that the correct participant's video stream is rendered within the VideoView widget.

And [here’s](https://github.com/dyte-io/collaborative-tic-tac-toe) the source code for the project.

Throughout this blog post, we explored the seamless integration of these powerful tools, enabling you to create an immersive and interactive gaming experience.

## Conclusion

You've crafted a visually appealing and responsive user interface by leveraging Flutter's cross-platform capabilities. The [Dyte video SDK](https://dyte.io/video-sdk) has brought players closer together, allowing them to enjoy real-time Tic-Tac-Toe battles of strategy and wit, regardless of their physical location. With GetX, you've efficiently managed the game's state, providing users with a smooth and enjoyable gaming experience.

But this is just the beginning of your game development journey. There are endless possibilities for expanding and enhancing your Tic-Tac-Toe game. Consider implementing game statistics, player rankings, or additional game modes to keep your users engaged and entertained.

As you continue to explore and experiment with Flutter and the [Dyte video SDK](https://dyte.io/flutter-video-sdk), remember to embrace the joy of learning and be open to trying new ideas. The world of game development is constantly evolving, offering limitless opportunities for innovation.

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

Congratulations! You've reached the end of this exciting journey into building a multiplayer Tic-Tac-Toe game using Flutter, the [Dyte video SDK](https://dyte.io/flutter-video-sdk), and GetX for state management.