Adding Live Video Calling to Tic-Tac-Toe With Flutter and Dyte

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, 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 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. 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 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 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, we need to add some listeners to the RoomStateNotifier class. This can be accomplished by implementing the following code:

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.

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 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. 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 and initiating a Dyte meeting.

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.

@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.

@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.

@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.

@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.

dyteClient.value.leaveRoom();

Here is the code for onParticipantLeave:

@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. 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, 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.

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.

@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.

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:

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. 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.

// 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:

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.

Congratulations! You've reached the end of this exciting journey into building a multiplayer Tic-Tac-Toe game using Flutter, the Dyte video SDK, and GetX for state management. The final app will look and feel like this:

0:00
/

And here’s 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 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, 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's technology and discover how it can revolutionize your app's communication capabilities with its SDKs.