In this codelab, you'll learn how to use the Google Cast Game Manager API to create a multiplayer game that you can play on your TV using your Android mobile device.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would rate your experience with building Android apps?

Novice Intermediate Proficient

You can either download all the sample code to your computer...

Download Zip

...or clone the GitHub repository from the command line.

$ git clone https://github.com/googlecodelabs/cast-game-manager.git

First, let's see what the finished sample game looks like. The game is a very simple word guessing game. One of the players is the artist and draws clues about the selected word that the other players have to guess.

With the code downloaded, the following instructions describe how to open and run the completed sample app in Android Studio.

  1. Select the game-done directory from the sample code folder (Quickstart > Import Project... > game-done).
  2. Click the Gradle sync button.
  3. Enable USB debugging on your Android device.
  4. Use the SDK Manager in Android Studio to make sure you have the necessary libraries installed for a Cast enabled app:
  1. android-support-v7-appcompat revision 22 or later
  2. android-support-v7-mediarouter revision 22 or later
  3. google-play-services_lib revision 7.5 or later
  1. Plug in your Android device and click the Run button. You should see the game app home screen appear after a few seconds.
  2. Click the Cast button and select your Google Cast device. The game should now load on your Google Cast device.
  3. Set your name.
  4. Click the "Join" button to join the game lobby.
  5. Other mobile devices can now also join the game.
  6. When all the game participants have joined, click on the "Start" button to start the game.
  7. There are two kinds of players: the artists draws on the mobile device for the word selected and the other game players need to guess the word drawn.
  8. The game alternates between players until all the players disconnect from the Chromecast device.

The first screen prompts the user to connect to the Chromecast device:

The artist screen displays the selected word and a drawing area:

The other participants see the list of words to choose from and a 30 second timer:

Frequently Asked Questions

A Google Cast game enables multi screen gameplay between mobile devices and television. The game can use your mobile device's sensors to create an amazing controller.

One of the most critical design points in making a Cast game, is simply "how does someone find, and join a game?". The way to accomplish this, is to make sure that your game can find nearby cast devices and automatically present the user with the option to cast as part of the game setup flow. Thankfully for you, the developer, this is easy to enable, just set up the game with the Google Cast SDK.

In this codelab we are going to develop a Receiver Cast game.

An App running on the sender, which is your mobile device, connects to a receiver device, such as a Chromecast, and passes along an Application ID to load.

The receiver device loads that application, written in HTML5 and Javascript, from the cloud.

In this model we recommend using the Game Manager APIs for Google Cast, which is part of the Google Cast SDK.

The Game Manager APIs make it faster and easier for game developers like yourself to Cast-enable their apps across all the devices we support: Android, iOS, and Chrome browsers, and get those devices connected to a Chromecast or Android TV.

The Game Manager APIs keep track of players and their states, the game state, the lobby state and provides way to share data between players.

Frequently Asked Questions

We need to add support for Google Cast to the app you downloaded. Here are some Google Cast terminology that we will be using in this codelab:

Now you're ready to build on top of the starter project to add deep links to it.

  1. Select the game-start directory from your sample code download (File > Import Project... > game-start).
  2. Click the Gradle sync button.

App Design

The app consists of one MainActivity and several Fragments for each of the UI's supported by the app:

The exact details on how to add support for Google Cast to an existing app is outside the scope of this codelab. We have a Google Cast codelab which covers that in detail for Android apps and iOS apps. We also have extensive documentation for Android, iOS and Chrome senders.

We have created a useful helper class called CastConnectionManager that implements all of the Google Cast features you will need for the game. CastConnectionManager extends Observable so that other classes can be informed of Cast events.

Cast Integration

Your receiver app needs to be hosted on a web server. Copy the files in the receiver directory to your web server. If you don't have a web server, you can use Google Drive to host the receiver files. Next you need to register your app in the Google Cast Developer Console. Use the "Add New Application" button to create a new Custom Receiver. Fill in the name and the URL for the receiver app index.html file. Add the generated Application ID to the res/values/strings.xml file in the Android project. Also, add your Cast device to the Google Cast Developer Console so that you can debug your receiver app (debugging is discussed in a later section).

You need to create an instance of the CastConnectionManager in the MainActivity so that you can track the state of the connection with the Cast device and also to send messages from the sender to the receiver.

MainActivity.java

private CastConnectionManager mCastConnectionManager;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);

    // Set up Toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    mCastConnectionManager = new CastConnectionManager(this,
            getResources().getString(R.string.app_id));
    mCastConnectionFragment = new CastConnectionFragment();
    mDrawingFragment = new DrawingFragment();
    mLobbyFragment = new LobbyFragment();

    updateFragments();
}

public CastConnectionManager getCastConnectionManager() {
        return mCastConnectionManager;
}

The Google Cast button needs to be added to the ActionBar to comply with the Google Cast Design Checklist. The MediaRouteActionProvider which renders the Cast button is already added to the Menu main.xml file. The MainActivity onCreateOptionsMenu() needs to be modified to use the CastConnectionManager MediaRouteSelector to find all the Google Cast devices on the local network that can run our app.

MainActivity.java

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    MenuItem mediaRouteMenuItem = 
                 menu.findItem(R.id.media_route_menu_item);
    MediaRouteActionProvider mediaRouteActionProvider =
                 (MediaRouteActionProvider)
                     
    MenuItemCompat.getActionProvider(mediaRouteMenuItem);
    if (mediaRouteActionProvider == null) {
      Log.w(TAG, "mediaRouteActionProvider is null!");
      return false;
    }
    mediaRouteActionProvider.setRouteSelector(
        mCastConnectionManager.getMediaRouteSelector());
    return true;
}

We also need to enable device discovery and listen to Cast events when the app is resumed:

MainActivity.java

@Override
protected void onResume() {
    super.onResume();
    mCastConnectionManager.startScan();
    mCastConnectionManager.addObserver(this);
    updateFragments();
 }

@Override
protected void onPause() {
    mCastConnectionManager.stopScan();
    mCastConnectionManager.deleteObserver(this);
    super.onPause();
}

When the app starts, a message is displayed to the user by the CastConnectionFragment inside the MainActivity. Once the user has selected a Cast device using the Cast menu, the MainActivity update method is called by the CastConnectionManager since the MainActivity is an Observer. The GameManageClient sendPlayerAvailableRequest is called to tell the Game Manager API that a new player is available.

The receiver will transition this player into the PLAYER_STATE_AVAILABLE state and will confirm the operation with a ResultCallback. If the operation fails, the sender disconnects from the receiver.

MainActivity.java

@Override
public void update(Observable object, Object data) {
    final GameManagerClient gameManagerClient =   
            mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        PendingResult<GameManagerClient.GameManagerResult> result =
                gameManagerClient.sendPlayerAvailableRequest(null);
        result.setResultCallback(
           new ResultCallback<GameManagerClient.GameManagerResult>()      
        {
            @Override
            public void onResult(final 
                GameManagerClient.GameManagerResult 
                    gameManagerResult) {
                if (gameManagerResult.getStatus().isSuccess()) {
                    mPlayerState =  
                      gameManagerClient.getCurrentState().getPlayer(             
                         gameManagerResult.getPlayerId()).getPlayerState();
                } else {
                    mCastConnectionManager.disconnectFromReceiver(false);
                    Utils.showErrorDialog(MainActivity.this,
                       gameManagerResult.getStatus().getStatusMessage());
                }
                updateFragments();
            }
        });
    }
    updateFragments();
}

The MainActivity updateFragments methods will switch between the various fragments based on the state of the connection with the Cast device and also the player state.

MainActivity.java

private void updateFragments() {
    if (isChangingConfigurations() || isFinishing() || isDestroyed()) {
        return;
    }

    Fragment fragment;
    if (!mCastConnectionManager.isConnectedToReceiver()) {
        mPlayerName = null;
        fragment = mCastConnectionFragment;
    } else {
        if (mPlayerState == GameManagerClient.PLAYER_STATE_PLAYING) {
            fragment = mDrawingFragment;
        } else {
            fragment = mLobbyFragment;
        }
    }
    getFragmentManager().beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
            .commitAllowingStateLoss();
}

Fragments

All of the fragments extend GameFragment. Modify GameFragment to keep track of the CastConnectionManager instance in the MainActivity and to observe the Cast events.

GameFragment.java

protected CastConnectionManager mCastConnectionManager;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    mCastConnectionManager = ((MainActivity)   
           getActivity()).getCastConnectionManager();
    mCastConnectionManager.addObserver(this);
    if (mCastConnectionManager.getGameManagerClient() != null) {
        mCastConnectionManager.getGameManagerClient().setListener(this);
    }
}

@Override
public void onDestroy() {
    super.onDestroy();
    mCastConnectionManager.deleteObserver(this);
}

The CastConnectionFragment displays a message to the user to connect to a Cast device since this is a Cast-required game. You can read about the UX requirements for Cast-required games in the Game UX Guidelines.

You now need to modify the CastConnectionFragment to change its state to show a progress indicator when the user has selected a Cast device. The CastConnectionFragment update method is invoked by the CastConnectionManager once it has a selected Cast device.

CastConnectionFragment.java

@Override
public void update(Observable object, Object data) {
    if (getView() == null) {
        return;
    }
    if (mCastConnectionManager.getSelectedDevice() != null) {
        mConnectLabel.setVisibility(View.GONE);
        mSpinner.setVisibility(View.VISIBLE);
    } else {
        mConnectLabel.setVisibility(View.VISIBLE);
        mSpinner.setVisibility(View.GONE);
    }
}

You can now run the app on your mobile device. You should see a Cast icon, and when you select the a Cast device, the LobbyFragment should be loaded with a progress indicator. The receiver should display the title "Game". If you click on the Cast icon to disconnect, you should go back to the CastConnectionFragment. The receiver app will eventually close itself.

Frequently Asked Questions

The receiver for the game app is called a custom receiver since it is created and hosted by you. This is required as the receiver needs to contain custom logic to handle the gameplay. Google Cast also supports other Google-hosted receivers but are designed for media playback.

The custom receiver is an HTML app that is loaded on the Google Cast device and needs to include the Google Cast Receiver SDK. You also need to include the Cast GameManager Receiver API to manage the game setup, game and lobby status, and messaging between players.

The index.html file is the entry point for the game's receiver app. A table of sequentially numbered cells are created in HTML to render the grid of cells for the artist's drawing. Include the necessary scripts before the bottom of the BODY tag in index.html.

index.html

<script src="https://dn-gstatic.qbox.me/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
<script src="https://dn-gstatic.qbox.me/cast/sdk/libs/games/1.0.0/cast_games_receiver.js"></script>
<script src="game.js"></script>
<script src="main.js"></script>

The main.js will include the startup logic for the Cast Receiver SDK and the Cast GameManager Receiver API. The CastReceiverManager and the GameManager instances are initialized first.

main.js

var castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
var appConfig = new cast.receiver.CastReceiverManager.Config();

appConfig.statusText = 'Game is starting.';

var gameConfig = new cast.receiver.games.GameManagerConfig();
gameConfig.applicationName = 'Game';

var gameManager = new cast.receiver.games.GameManager(gameConfig);
var game = new Game(gameManager);

The CastReceiverManager is then started and once the onReady event is received, the game is started.

main.js

var startGame = function() {
  game.run(function() {
    console.log('Game running.');
    castReceiverManager.setApplicationState('Game running.');
  });
};

castReceiverManager.onReady = function(event) {
  if (document.readyState === 'complete') {
    startGame();
  } else {
    window.onload = startGame;
  }
}
castReceiverManager.start(appConfig);

You can now run the app on your mobile device. You should see a Cast icon, and when you select the a Cast device, the LobbyFragment should be loaded an edit field and button. This confirms that the receiver manager initialized and started running on the receiver.

Frequently Asked Questions

Once the user has selected a Cast device, the MainActivity loads the LobbyFragment. The LobbyFragment allows the user to enter a player name, join the game lobby and start playing the game. The UI for the LobbyFragment consists of an EditText widget and a Button widget.

When the LobbyFragment is first displayed the edit field is empty and the button has the "Join" label. The player can enter a name by clicking on the edit field. The player then joins the lobby by clicking on the "Join" button. The button label then changes to "Start". Once all the players have joined the lobby, each user can press the "Start" button to join the game and close the lobby to new players.

The Game Manager API keeps track of the player state using PLAYER_STATE_* constants declared in the GameManagerClient class. When the user clicks on the button the LobbyFragment onJoinStartClicked method is called and the player state should be updated. The player is in the PLAYER_STATE_AVAILABLE state when the LobbyFragment is loaded (the MainActivity update method already called the GameManagerClient sendPlayerAvailableRequest method). When the user clicks on the "Join" button the player state is updated to PLAYER_STATE_READY and when the user clicks on the "Start" button, the player state is updated to PLAYER_STATE_PLAYING.

LobbyFragment.java

private void onJoinStartClicked() {
    int playerState = ((MainActivity) getActivity()).getPlayerState();
    if (playerState == GameManagerClient.PLAYER_STATE_AVAILABLE
            || playerState == GameManagerClient.PLAYER_STATE_PLAYING) {
       ((MainActivity) getActivity()).setPlayerName(     
                              mNameEditText.getText().toString());
       sendPlayerReadyRequest();
    } else if (playerState == GameManagerClient.PLAYER_STATE_READY) {
        sendPlayerPlayingRequest();
    }
    updateView();
}

The player state is updated to PLAYER_STATE_READY by calling GameManagerClient sendPlayerReadyRequest method. The name of the player is passed to the receiver as a JSONObject.

LobbyFragment.java

public void sendPlayerReadyRequest() {
    final GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        // Send player name to the receiver
        JSONObject jsonMessage = new JSONObject();
        try {
            jsonMessage.put("name", mNameEditText.getText().toString());
        } catch (JSONException e) {
            Log.e(TAG, "Error creating JSON message", e);
            return;
        }
        PendingResult<GameManagerClient.GameManagerResult> result =
              gameManagerClient.sendPlayerReadyRequest(jsonMessage);
        result.setResultCallback(
            new ResultCallback<GameManagerClient.GameManagerResult>() {
            @Override
            public void onResult(
               final GameManagerClient.GameManagerResult 
                    gameManagerResult) {
                   if (gameManagerResult.getStatus().isSuccess()) {
                       ((MainActivity) getActivity()).setPlayerState(
                          gameManagerClient.getCurrentState().getPlayer(
                       gameManagerResult.getPlayerId()).getPlayerState());
                   } else {
                      mCastConnectionManager.disconnectFromReceiver(false);
                      Utils.showErrorDialog(getActivity(),
                         gameManagerResult.getStatus().getStatusMessage());
              }
              updateView();
          }
      });
  }
  updateView();
}

The player state is updated to PLAYER_STATE_PLAYING by calling the GameManagerClient sendPlayerPlayingRequest method.

LobbyFragment.java

public void sendPlayerPlayingRequest() {
    final GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        PendingResult<GameManagerClient.GameManagerResult> result =
                gameManagerClient.sendPlayerPlayingRequest(null);
        result.setResultCallback(
            new ResultCallback<GameManagerClient.GameManagerResult>() {
            @Override
            public void onResult(final GameManagerClient.GameManagerResult 
                gameManagerResult) {
                if (gameManagerResult.getStatus().isSuccess()) {
                    ((MainActivity) getActivity()).setPlayerState(
                        gameManagerClient.getCurrentState().getPlayer(
                             gameManagerResult.getPlayerId())
                             .getPlayerState());
                } else {
                    mCastConnectionManager.disconnectFromReceiver(false);
                    Utils.showErrorDialog(getActivity(),
                        gameManagerResult.getStatus().getStatusMessage());
                }
                updateView();
            }
        });
    }
    updateView();
}

What is left is to update the UI based on the player state value and the lobby state value. If the player state is PLAYER_STATE_AVAILABLE, then the button should have the "Join" label and if the player state is PLAYER_STATE_READY, the button should have the label "Start".

The lobby state is set to LOBBY_STATE_OPEN by the receiver when the game loads. Once the game starts and the player state goes to PLAYER_STATE_PLAYING, the receiver sets the state to LOBBY_STATE_CLOSED. When the lobby is closed, the button should be hidden and a progress indicator should be displayed since the MainActivity will then load the DrawingFragment next (see the MainActivity updateFragments method).

LobbyFragment.java

private void updateView() {
    if (((MainActivity) getActivity()).getPlayerName() == null) {
     /google/cast/samples/games/codel   mNameEditText.setText("");
    }
    int playerState = ((MainActivity) getActivity()).getPlayerState();
    GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        GameManagerState gameManagerState = 
            gameManagerClient.getCurrentState();
        if (gameManagerState.getLobbyState() == 
            GameManagerClient.LOBBY_STATE_OPEN) {
            mJoinStartButton.setVisibility(View.VISIBLE);
            mSpinner.setVisibility(View.GONE);
            if (playerState == GameManagerClient.PLAYER_STATE_AVAILABLE) {
                mJoinStartButton.setText(R.string.button_join);
            } else if (playerState == 
                GameManagerClient.PLAYER_STATE_READY) {
                mJoinStartButton.setText(R.string.button_start);
            }
        } else {
            mJoinStartButton.setVisibility(View.GONE);
            mSpinner.setVisibility(View.VISIBLE);
        }
    }
}

You can now run the app on your mobile device. Connect to a Cast device and then enter your name in the LobbyFragment. Click on the "Join" button to join the lobby. Do the same from another mobile device as the second player. Then click on the "Start" button to load the DrawingFragment.

Frequently Asked Questions

The receiver game.js file contains a JavaScript class to manage the drawing gameplay. The GameManager instance is stored in the Game object. The ReceiverManager is configured for debugging.

game.js

Game = function(gameManager) {
  this.gameManager_ = gameManager;
  cast.receiver.logger.setLevelValue(cast.receiver.LoggerLevel.DEBUG);
  this.debugUi = new cast.receiver.games.debug.DebugUI(this.gameManager_);

  this.loadedCallback_ = null;
  this.isLoaded_ = false;
  this.isRunning_ = false;
  this.boundGameMessageCallback_ = this.onGameMessage_.bind(this);
  this.boundPlayerReadyCallback_ = this.onPlayerReady_.bind(this);
  this.boundPlayerPlayingCallback_ = this.onPlayerPlaying_.bind(this);
  this.boundPlayerQuitCallback_ = this.onPlayerQuit_.bind(this);
};

When the game starts, the game logic needs to listen to various player state events. You also need to set the gameplay state and lobby state. The player names are tracked in an array.

game.js

Game.prototype.start_ = function() {
  if (!this.loadedCallback_) {
    return;
  }

  this.isRunning_ = true;
  this.gameManager_.updateGameplayState(
      cast.receiver.games.GameplayState.RUNNING, null);

  this.loadedCallback_();
  this.loadedCallback_ = null;
  
  this.gameManager_.addEventListener(
      cast.receiver.games.EventType.PLAYER_READY,
      this.boundPlayerReadyCallback_);
  this.gameManager_.addEventListener(
      cast.receiver.games.EventType.PLAYER_PLAYING,
      this.boundPlayerPlayingCallback_);
  this.gameManager_.addEventListener(
      cast.receiver.games.EventType.GAME_MESSAGE_RECEIVED,
      this.boundGameMessageCallback_);
  this.gameManager_.addEventListener(
      cast.receiver.games.EventType.PLAYER_QUIT,
      this.boundPlayerQuitCallback_);
      
  this.gameManager_.updateGameplayState(
      cast.receiver.games.GameplayState.SHOWING_INFO_SCREEN, null);
  this.gameManager_.updateLobbyState(cast.receiver.games.LobbyState.OPEN,
      null);
  this.updateTitle_('Lobby');
  
  this.players_ = [];
  this.wordsMessage_ = null;
};

When the game stops, the game needs to stop listening to the Game Manager player state events.

game.js

Game.prototype.stop = function() {
  if (this.loadedCallback_ || !this.isRunning_) {
    this.loadedCallback_ = null;
    return;
  }

  this.isRunning_ = false;

  this.gameManager_.removeEventListener(
      cast.receiver.games.EventType.PLAYER_READY,
      this.boundPlayerReadyCallback_);
  this.gameManager_.removeEventListener(
      cast.receiver.games.EventType.PLAYER_PLAYING,
      this.boundPlayerPlayingCallback_);      
  this.gameManager_.removeEventListener(
      cast.receiver.games.EventType.GAME_MESSAGE_RECEIVED,
      this.boundGameMessageCallback_);
  this.gameManager_.removeEventListener(
      cast.receiver.games.EventType.PLAYER_QUIT,
      this.boundPlayerQuitCallback_);
};

When the sender changes a player's state to PLAYER_READY, the onPlayerReady_ function is called. The player ID and name is extracted from the event and the receiver UI is updated with the player name.

game.js

Game.prototype.onPlayerReady_ =
    function(event) {
  if (!this.isSuccessEvent_(event)) {
    return;
  }
  var playerId = /** @type {string} */ (event.playerInfo.playerId);
  var playerName = event.requestExtraMessageData.name;
  console.log('Player is ready: ' + playerName);
  this.updateInfo_(playerName + ' has joined.');
  this.players_[playerId] = playerName;
};

When the sender changes a player's state to PLAYER_PLAYING, the onPlayerPlaying_ function is called. All the players in the READY state are updated to the PLAYING state. The lobby is closed for any new players.

game.js

Game.prototype.onPlayerPlaying_ =
    function(event) {
  if (!this.isSuccessEvent_(event)) {
    return;
  }
  var playerId = /** @type {string} */ (event.playerInfo.playerId);
  // Update all ready players to playing state.
  var players = this.gameManager_.getPlayers();
  for (var i = 0; i < players.length; i++) {
    if (players[i].playerState == cast.receiver.games.PlayerState.READY) {
      this.gameManager_.updatePlayerState(players[i].playerId,
          cast.receiver.games.PlayerState.PLAYING, null);
    }
  }
  this.gameManager_.updateGameplayState(
     cast.receiver.games.GameplayState.RUNNING, null);
  this.gameManager_.updateLobbyState(
     cast.receiver.games.LobbyState.CLOSED, null);
  this.updateTitle_('Playing');
  this.updateInfo_(this.players_[playerId] + ' is playing.');
};

When a player quits the game by disconnecting from the Cast device, the onPlayerQuit_ function is called. If there are no more players connected, then the receiver app is closed.

game.js

Game.prototype.onPlayerQuit_ =
    function(event) {
  if (!this.isSuccessEvent_(event)) {
    return;
  }
  var connectedPlayers = this.gameManager_.getConnectedPlayers();
  if (connectedPlayers.length == 0) {
    console.log('No more players connected. Tearing down game.');
    cast.receiver.CastReceiverManager.getInstance().stop();
  }
};

Update the following utility method to check if the incoming gaming event status is SUCCESS.

game.js

Game.prototype.isSuccessEvent_ = function(event) {
  if (event.statusCode != cast.receiver.games.StatusCode.SUCCESS) {
    console.log('Error: Event status code: ' + event.statusCode);
    console.log('Reason for error: ' + event.errorDescription);
    return false;
  }
  return true;
}

You can now run the app on your mobile device. Connect to a Cast device and then enter your name in the LobbyFragment. Click on the "Join" button to join the lobby. The receiver should display a message when each player joins the lobby. Then click on the "Start" button. The receiver doesn't show the drawing board yet. Clicking on the Cast icon on the sender will close the receiver app.

Frequently Asked Questions

Once the user joins the game lobby and starts the game, the DrawingFragment is loaded by MainActivity. The DrawingFragment is based on an existing open source game called "8 Bit Artist".

The DrawingFragment displays one of these UI's at a time:

Any user action in the DrawingFragment is communicated with the receiver using messages. Add a utility method to DrawingFragment to send messages to the receiver using the GameManagerClient sendGameMessage.

DrawingFragment.java

private void sendGameMessage(JSONObject jsonObject) {
    if (mCastConnectionManager.getGameManagerClient() != null) {
        mCastConnectionManager.getGameManagerClient()
            .sendGameMessage(jsonObject);
    }
}

The various kinds of messages are declared in DrawingFragment.

DrawingFragment.java

private static final String MESSAGE_TURN = "turn";
private static final String MESSAGE_WORDS = "words";
private static final String MESSAGE_st-game-manager/tree/mINDEX = "index";
private static final String MESSAGE_GUESS = "guess";
private static final String MESSAGE_ARTIST = "artist";
private static final String MESSAGE_CLEAR = "clear";
private static final String MESSAGE_PLAYER = "player";
private static final String MESSAGE_GRID = "grid";

Each player needs to determine if it is her turn. The number of players in the PLAYER_STATE_PLAYING is used to pick the next artist in a sequential order.

DrawingFragment.java

private boolean isMyTurn() {
    GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        GameManagerState state = gameManagerClient.getCurrentState();
        int numParticipants = state.getPlayersInState(
                GameManagerClient.PLAYER_STATE_PLAYING).size();
        if (numParticipants <= 1) {
            Log.w(TAG, "isMyTurn: no participants - default to true.");
            return true;
        }
        int participantTurnIndex = mMatchTurnNumber % numParticipants;

        Log.d(TAG, String.format("isMyTurn: %d participants, " + 
            "turn #%d, my turn is #%d",
                numParticipants, mMatchTurnNumber, mMyTurnIndex));
        return (mMyTurnIndex == participantTurnIndex);
    }
    return true;
}

private void updateTurnIndices() {
    GameManagerClient gameManagerClient =  
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        GameManagerState state = gameManagerClient.getCurrentState();
        ArrayList<String> ids = new ArrayList<>();
        for (PlayerInfo playerInfo : state.getPlayersInState(
                GameManagerClient.PLAYER_STATE_PLAYING)) {
            ids.add(playerInfo.getPlayerId());
        }
        Collections.sort(ids);
        mMyTurnIndex = 
            ids.indexOf(gameManagerClient.getLastUsedPlayerId());
    }
}

The player whose turn it is to be the artist selects words randomly and then sends them to the receiver. The receiver will pass the list of words on to all the players in the PLAYER_STATE_PLAYING state that were in the game lobby and pressed the "Start" button.

DrawingFragment.java

private void sendTurnMessage(int matchTurnNumber) {
    // Send turn message to others
    JSONObject jsonMessage = new JSONObject();
    try {
        jsonMessage.put(MESSAGE_TURN, matchTurnNumber);
        jsonMessage.put(MESSAGE_WORDS, 
            TextUtils.join(",", mTurnWords.toArray()));         
        jsonMessage.put(MESSAGE_INDEX, mWordIndex);
    } catch (JSONException e) {
        Log.e(TAG, "Error creating JSON message", e);
        return;
    }
    sendGameMessage(jsonMessage);
}

After each player presses the "Start" button, they need to get the list of words from the receiver. The "player" message is sent by each player that isn't the artist. The receiver will then send the list of words to the players that need to guess the currently selected word.

DrawingFragment.java

private void sendPlayerMessage() {
    GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        JSONObject jsonMessage = new JSONObject();
        try {
            jsonMessage.put(MESSAGE_PLAYER,  
                gameManagerClient.getLastUsedPlayerId());
        } catch (JSONException e) {
            Log.e(TAG, "Error creating JSON message", e);
            return;
        }
        sendGameMessage(jsonMessage);
    }
}

At each turn as a new artist is selected, the drawing area on the receiver needs to be cleared for the next word.

DrawingFragment.java

private void sendClearMessage() {
    JSONObject jsonMessage = new JSONObject();
    try {
        jsonMessage.put(MESSAGE_CLEAR, 1);
    } catch (JSONException e) {
        Log.e(TAG, "Error creating JSON message", e);
        return;
    }
    sendGameMessage(jsonMessage);
}

The other players need to be informed when a new artist is selected.

DrawingFragment.java

private void sendArtistMessage() {
    JSONObject jsonMessage = new JSONObject();
    GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        jsonMessage = new JSONObject();
        try {
            jsonMessage.put(MESSAGE_ARTIST, 
                gameManagerClient.getLastUsedPlayerId());
        } catch (JSONException e) {
            Log.e(TAG, "Error creating JSON message", e);
            return;
        }
        sendGameMessage(jsonMessage);
    }
}

When a player selects a word to match the artists drawing, the guessed word index is sent to the receiver. The receiver will pass the guessed word index on to all the players.

DrawingFragment.java

private void sendGuessMessage(int position) {
    JSONObject jsonMessage = new JSONObject();
    try {
        jsonMessage.put(MESSAGE_GUESS, position);
    } catch (JSONException e) {
        Log.e(TAG, "Error creating JSON message", e);
        return;
    }
    sendGameMessage(jsonMessage);
}

DrawingFragment listens to events generated by DrawView when the artist draws. The onDrawEvent method is invoked and then the new position the artist touched is passed on to the receiver to display that to all the players on the TV. The drawing area consists of a grid of 20x20 pixels.

DrawingFragment.java

@Override
public void onDrawEvent(int gridX, int gridY, short colorIndex) {
    JSONObject jsonMessage = new JSONObject();
    try {
        jsonMessage.put("grid", (gridX + 1) + gridY * DrawView.GRID_SIZE);
    } catch (JSONException e) {
        Log.e(TAG, "Error creating JSON message", e);
        return;
    }
    sendGameMessage(jsonMessage);
}

Remember that GameFragment is the parent class of DrawingFragment and it listens to game state messages. When the game state changes due to a player quitting, the turn index has to be updated. If the current player is the only player left in the game, then we disconnect from the receiver.

DrawingFragment.java

@Override
public void onStateChanged(GameManagerState newState,
        GameManagerState oldState) {
    List<PlayerInfo> players = 
        newState.getPlayersInState(GameManagerClient.PLAYER_STATE_QUIT);
    if (players.size() > 1) {
        updateTurnIndices();
    }
    players = newState.getPlayersInState(
        GameManagerClient.PLAYER_STATE_PLAYING);
    if (players.size() == 1) {
        mCastConnectionManager.disconnectFromReceiver(true);
    }
}

Finally, you have to handle the incoming messages from the receiver. If another player guesses the word for the drawing, a dialog is displayed with the result. If the current artist sends out the list of words, then the words to be displayed has to be updated.

DrawingFragment.java

@Override
public void onGameMessageReceived(String playerId, JSONObject message) {
    if (message.has(MESSAGE_GUESS)) {
        try {
            int guess = message.getInt(MESSAGE_GUESS);
            createOpponentGuessDialog(playerId);

            if (guess == mWordIndex) {
                Log.i(TAG, "Player guessed correctly!");
            }
        } catch (JSONException e) {
            Log.e(TAG, "onGameMessageReceived", e);
        }
    } else if (message.has(MESSAGE_WORDS)) {
        try {
            mMatchTurnNumber = message.getInt(MESSAGE_TURN);
            mTurnWords = Arrays.asList(
                message.getString(MESSAGE_WORDS).split("\\s*,\\s*"));
            mWordIndex = message.getInt(MESSAGE_INDEX);
            mGuessersThisTurn.clear();

            beginMyTurn();
        } catch (JSONException e) {
            Log.e(TAG, "onGameMessageReceived", e);
        }
    }
}

Add a utility method to display a dialog with the result of another player's guess.

DrawingFragment.java

private void createOpponentGuessDialog(String guesserId) {
    mGuessersThisTurn.add(guesserId);

    GameManagerClient gameManagerClient = 
        mCastConnectionManager.getGameManagerClient();
    if (mCastConnectionManager.isConnectedToReceiver()) {
        GameManagerState state = gameManagerClient.getCurrentState();
        int numParticipants = state.getPlayersInState(
                GameManagerClient.PLAYER_STATE_PLAYING).size();
        boolean allHaveGuessed = mGuessersThisTurn.size() >= 
            numParticipants - 1;
        if (isMyTurn() && allHaveGuessed) {
            // All guesses entered
            DialogInterface.OnClickListener onClickListener
                    = new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    onDoneClicked();
                }
            };
            showDialog(getString(R.string.all_guesses_entered),
                       getString(R.string.all_other_players_have_guessed),
                       onClickListener);
        }
    }
}

You can now run the app on your mobile device. After you enter you name and click the "Start" button, the receiver should display a message that you are playing the game.

Frequently Asked Questions

When the receiver gets a message from the sender, the onGameMessage_ function is called. The various messages are handled to keep the receiver UI in sync with the current artist drawing. For some of the messages like "player", "words" and "guess", the messages are forwarded to the connected players. When the artists draws on the grid, the associated table element in the HTML DOM has its background color changed.

game.js

Game.prototype.onGameMessage_ =
    function(event) {
  if (!this.isSuccessEvent_(event)) {
    return;
  }
  var playerId = /** @type {string} */ (event.playerInfo.playerId);
  var message = event.requestExtraMessageData;
  console.log("message="+message);
  if (message.clear) {
    this.clearGrid_();
    return;
  }
  if (message.artist) {
    this.updateInfo_(this.players_[message.artist] + ' is drawing.');
    return;
  }
  if (message.player) {
    if (this.wordsMessage_) {
      this.gameManager_.sendGameMessageToPlayer(playerId, 
          this.wordsMessage_);
    }
    return;
  }
  if (message.words) {
    this.gameManager_.sendGameMessageToAllConnectedPlayers(message);
    this.wordsMessage_ = message;
    return;
  }
  if (message.guess) {
    this.gameManager_.sendGameMessageToAllConnectedPlayers(message);
    return;
  }
  var element = document.getElementById(message.grid);
  if (element) {
   element.style.backgroundColor = 'blue'; 
  }
};

You can now run the app on your mobile device. Once you have started the game and you are picked as the artist player, then you can draw on the mobile drawing area and the drawing should be displayed on the receiver. Click on the "Clear" button on the sender and the receiver should clear the drawing. You can click on the "Done" button if no players can guess the word. If another player does guess the word, a dialog will be displayed on the sender and the next word is selected. Another player will be selected to be the artist. To end the game, click on the Cast button to disconnect.

Frequently Asked Questions

Debugging a Game Manager API app can be done in 3 ways:

Game Debugging UI

The Game Manager Receiver API includes a DebugUI utility class that is initialized in the game receiver logic.

game.js

this.debugUi = new cast.receiver.games.debug.DebugUI(this.gameManager_);

The DebugUI can then be displayed from the code.

game.js

this.debugUi.open();

Or the DebugUI can be opened with the Chrome Remote Debugger console using a reference to your GameManager instance.

game.js

gameManager.debugUi.open();

You can now use the Chrome Remote Debugger console to invoke the methods of your GameManager instance and see the results in the debug UI.

game.js

gameManager.getGameplayState()

Game Debugger Sample App

To use the Game Debugger Sample app, clone the GitHub repository from the command line.

$ git clone https://github.com/googlecast/GameManagerSamples.git

Now you're ready to build the Game Debugger Sample app.

  1. Select the game-debugger/android-sender directory from your game samples repository (File > Import Project... > game-debugger/android-sender).
  2. Click the Gradle sync button.

You can now run the Game Debugger Sample. The Game Debugger Sample app provide a UI for you to add and manipulate player state. When the app starts, enter your receiver app ID and then your can add a new player.

The drawing game is now using the Game Manager API for both the sender and receiver.

What we've covered

Next Steps

Learn More